Java ArrayList引起的思考及泛型原理分析

前言

在学习Java的时候,泛型作为Java的一个重大特性,是一个不可避及的知识点.但在学习过程中发现子类对象存到ArrayList<父类>里面,读出来的时候,子类特有属性还在,这也就是说,当元素存储进ArrayList时,并不是将子类对象强制转换成父类对象(舍弃特有属性存储的).而Java支持<? super A类>,<? extends A类>(这是一个A类泛型或者其子类泛型)也加深了我这一疑惑.

验证猜测

脚本

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.ArrayList;

public class Test {
public static void main(String[] args) {
ArrayList<Father> test=new ArrayList<>();
Father f = new Father();
Son1 son1 = new Son1();
son1.c=3;
Son2 son2 = new Son2();
son2.d=4;
System.out.printf("%d %d\n",f.a,f.b);
System.out.printf("%d %d %d\n",son1.a,son1.b,son1.c);
System.out.printf("%d %d %d\n",son2.a,son2.b,son2.d);
System.out.println("---------------------------------------");
test.add(f);
test.add(son1);
test.add(son2);
Father test1 = test.get(0);
Son1 test2 = (Son1) test.get(1);
Son2 test3 = (Son2) test.get(2);
System.out.printf("%d %d\n",test1.a,test1.b);
System.out.printf("%d %d %d\n",test2.a,test2.b,test2.c);
System.out.printf("%d %d %d\n",test3.a,test3.b,test3.d);
}
}

Father类

1
2
3
4
5
6
7
8
9
public class Father {
int a;
int b;
public Father()
{
a=1;
b=2;
}
}

Son1类

1
2
3
4
5
6
7
public class Son1 extends Father{
int c;
public Son1()
{
super();
}
}

Son2类

1
2
3
4
5
6
7
public  class Son2 extends  Father{
int d;
public Son2()
{
super();
}
}

测试结果

1
2
3
4
5
6
7
1 2
1 2 3
1 2 4
---------------------------------------
1 2
1 2 3
1 2 4

将ArrayList换成LinkedList类测试,结果一样

承上启下

在学习途中,我突然感觉这一现象并不是泛型引起的,而是Java的特性.
将子类对象存到ArrayList里面时,他并没有进行copy操作,而是简单的映射了地址,如果我们修改对象的值,ArrayList中的值也会改变.
验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;

public class Test {
public static void main(String[] args) {
ArrayList<Father> test=new ArrayList<>();
Father f = new Father();
Son1 son1 = new Son1();
son1.c=3;
Son2 son2 = new Son2();
son2.d=4;
System.out.printf("%d %d\n",f.a,f.b);
System.out.printf("%d %d %d\n",son1.a,son1.b,son1.c);
System.out.printf("%d %d %d\n",son2.a,son2.b,son2.d);
System.out.println("---------------------------------------");
test.add(f);
test.add(son1);
test.add(son2);
f.a=12;
son1.c=12;
son2.d=12;
Father test1 = test.get(0);
Son1 test2 = (Son1) test.get(1);
Son2 test3 = (Son2) test.get(2);
System.out.printf("%d %d\n",test1.a,test1.b);
System.out.printf("%d %d %d\n",test2.a,test2.b,test2.c);
System.out.printf("%d %d %d\n",test3.a,test3.b,test3.d);
}
}

输出结果

1
2
3
4
5
6
7
1 2
1 2 3
1 2 4
---------------------------------------
12 2
1 2 12
1 2 12

对于产生这种误解的原因,大概是C++写多了.C++中将变量加到vector<类>时,是通过复制来操作的.
验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <bits/stdc++.h>

using namespace std;

class A{
public:
int a;
int b;
};
class B: public A{
public:
int c;
};
int main()
{
stack<A> s;
A aa;
B bb;
bb.a=1;
bb.b=2;
bb.c=3;
s.push(bb);
bb.a=2;
cout<<s.top().a;
return 0;
}

输出结果:

1
1

虽然问题解决了,但是C++和Java不同处理方式还有泛型原理我觉得学习一下还是很有意义的.

泛型分析

Java

Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。

泛型擦除

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
import java.util.ArrayList;

public class Test {
public static void main(String[] args) {
Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1);
System.out.println(class2);
System.out.println(class1.equals(class2));
}
}

输出结果

1
2
3
class java.util.ArrayList
class java.util.ArrayList
true

可以看到虽然他们的泛型不一样,但class1和class2还是属于同一个类型,在运行时我们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。
我们再来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class Test {
public static void main(String[] args) {
class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}

List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();

System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
}
}

输出结果

1
2
3
4
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]

我们构造了几个类并将他们实例化,但在获取这些对象类的类型参数时,我们发现打印的结果都是”形参”,没有任何关于它类型参数的信息.


编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HashF类型来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.*;

public class Test {
public interface HashF {
void f();
}
public class A implements HashF{
@Override
public void f() {

}
}
public static void main(String[] args) {
class Manipulator<T extends HashF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
Manipulator<A> test = new Manipulator<>();
System.out.println(Arrays.toString(test.getClass().getTypeParameters()));
}
}

输出结果

1
[T]

extend关键字后的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HashF类型。

擦除原理

这边直接引用dreamGong写的博客了
我们通过例子来看一下,先看一个非泛型的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String) holder.getObj();
}
}
// SimpleHolder.class
public class SimpleHolder {
public SimpleHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn

public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return

public static void main(java.lang.String[]);
Code:
0: new #3 // class SimpleHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}

下面我们给出一个泛型的版本,从字节码的角度来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//GenericHolder.java
public class GenericHolder<T> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.setObj("Item");
String s = holder.getObj();
}
}

//GenericHolder.class
public class GenericHolder<T> {
T obj;

public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public T getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn

public void setObj(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return

public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}

在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。
所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。

C++

函数模板

C++为我们提供了函数模板机制。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
在c++中为每个模板的实例化产生不同的类型,而这样会产生大量的重复代码,这一现象又叫”模板代码膨胀”,感性趣的可以自行了解.

参考博客

深入理解Java泛型
Java泛型的实现原理