java 泛型基础问题汇总

时间:2023-03-09 02:51:17
java 泛型基础问题汇总

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

3、泛型的类型参数可以有多个。

4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。

5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName("java.lang.String");

泛型擦除以及相关的概念

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

类型擦除引起的问题及解决方法

 1、先检查,在编译,以及检查编译的对象和引用传递的问题

2、自动类型转换

3、类型擦除与多态的冲突和解决方法

4、泛型类型变量不能是基本数据类型

5、运行时类型查询

6、异常中使用泛型的问题

7、数组(这个不属于类型擦除引起的问题)

9、类型擦除后的冲突

10、泛型在静态方法和静态类中的问题

1. 问:Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别?

答:泛型是 Java SE 1.5 的新特性,泛型的本质是参数化类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。在 Java SE 1.5 之前没有泛型的情况的下只能通过对类型 Object 的引用来实现参数的任意化,其带来的缺点是要做显式强制类型转换,而这种强制转换编译期是不做检查的,容易把问题留到运行时,所以 泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率,避免在运行时出现 ClassCastException。

JDK 1.5 引入了泛型来允许强类型在编译时进行类型检查;JDK 1.7 泛型实例化类型具备了自动推断能力,譬如 List<String> list = new ArrayList<String>(); 可以写成 List<String> llist = new ArrayList<>(); 了,JDK 具备自动推断能力。下面几种写法可以说是不同版本的兼容性了:

//JDK 1.5 推荐使用的写法
List<String> list = new ArrayList<String>();
//JDK 1.7 推荐使用的写法
List<String> llist = new ArrayList<>(); //可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告 List<String> list = new ArrayList(); //可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告 List list = new ArrayList<String>(); 

2. 问:Java 泛型是如何工作的?什么是类型擦除?

答:泛型是通过类型擦除来实现的,编译器在编译时擦除了所有泛型类型相关的信息,所以在运行时不存在任何泛型类型相关的信息,譬如 List<Integer> 在运行时仅用一个 List 来表示,这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

3. 问:Java 泛型类、泛型接口、泛型方法有什么区别?

答:泛型类是在实例化类的对象时才能确定的类型,其定义譬如 class Test<T> {},在实例化该类时必须指明泛型 T 的具体类型。

泛型接口与泛型类一样,其定义譬如 interface Generator<E> { E dunc(E e); }。

泛型方法所在的类可以是泛型类也可以是非泛型类,是否拥有泛型方法与所在的类无关,所以在我们应用中应该尽可能使用泛型方法,不要放大作用空间,尤其是在 static 方法时 static 方法无法访问泛型类的类型参数,所以更应该使用泛型的 static 方法(声明泛型一定要写在 static 后返回值类型前)。泛型方法的定义譬如 <T> void func(T val) {}。

4. 问:Java 如何优雅的实现元组?

答:元组其实是关系数据库中的一个学术名词,一条记录就是一个元组,一个表就是一个关系,纪录组成表,元组生成关系,这就是关系数据库的核心理念。很多语言天生支持元组,譬如 Python 等,在语法本身支持元组的语言中元组是用括号表示的,如 (int, bool, string) 就是一个三元组类型,不过在 Java、C 等语言中就比较坑爹,语言语法本身不具备这个特性,所以在 Java 中我们如果想优雅实现元组就可以借助泛型类实现,如下是一个三元组类型的实现:

Triplet<A, B, C> {

   private A a;
private B a;
private C a;
public Triplet(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
}

5. 问:下面程序块的运行结果是什么,为什么?

Class c1 = new ArrayList<String>().getClass();

Class c2 = new ArrayList<String>().getClass();

System.out.println(c1 == c2);

答:上面代码段结果为 true,解释如下。

因为 load 的是同一个 class 文件,存在 ArrayList.class 文件但是不存在 ArrayList<String>.class 文件,即便是通过 class.getTypeParameters() 方法获取类型信息也只能获取到 [T] 一样的泛型参数占位符。泛型是通过擦除来实现的,所以编译后任何具体的泛型类型都被擦除了(替换为非泛型上边界,如果没有指定边界则为 Object 类型),泛型类型只有在静态类型检查期间才出现,上面都被擦除成了 ArrayList 类型,所以运行时加载的是同一个 class 文件。

6. 问:为什么 Java 泛型要通过擦除来实现?擦除有什么坏处或者说代价?

答:可以说 Java 泛型的存在就是一个不得已的妥协,正因为这种妥协导致了 Java 泛型的混乱,甚至说是 JDK 泛型设计的失败。Java 之所以要通过擦除来实现泛型机制其实是为了兼容性考虑,只有这样才能让非泛化代码到泛化代码的转变过程建立在不破坏现有类库的实现上。正是因为这种兼容也带来了一些代价,譬如泛型不能显式地引用运行时类型的操作之中(如向上向下转型、instanceof 操作等),因为所有关于参数的信息都丢失了,所以任何时候使用泛型都要提醒自己背后的真实擦除类型到底是什么;此外擦除和兼容性导致了使用泛型并不是强制的(如 List<String> list = new ArrayList(); 等写法);其次擦除会导致我们在编写代码时十分谨慎(如不想被擦除为 Object 类型时不要忘了添加上边界操作等)。

7. 问:下面三个 funcX 方法有问题吗,为什么?

class Product<T> {
private void func1(Object arg) {
if (arg instanceof T) {}
}
private void func2() {
T var = new T();
}
private void func3() {
T[] vars = new T[3];
} }

答:func1、func2、func3 三个方法均无法编译通过。

因为泛型擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都将无法工作。

8. 问:下面代码段有问题吗,运行效果是什么,为什么?

ArrayList<Integer> arrayList = new ArrayList<Integer>();

arrayList.add(1);

arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "abc");

for (int i=0; i<arrayList.size(); i++) {

  System.out.println(arrayList.get(i));

}

答:由于在程序中定义的 ArrayList 泛型类型实例化为 Integer 的对象,如果直接调用 add 方法则只能存储整形数据,不过当我们利用反射调用 add 方法时就可以存储字符串,因为 Integer 泛型实例在编译之后被擦除了,只保留了原始类型 Object,所以自然可以插入。

9. 问:请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识?

答:Java 的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦除掉,譬如 List<Integer> 在运行时仅用一个 List 来表示(所以我们可以通过反射 add 方法来向 Integer 的泛型列表添加字符串,因为编译后都成了 Object),这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法,如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如 class Prd<T extends Comparable & Serializable> {} 的原始类型就是 Comparable),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

先检查再擦除的类型检查是针对引用的,用引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。可以说这是为了兼容带来的问题,如下:

ArrayList<String> arrayList1 = new ArrayList<String>();  

arrayList1.add("123");    //编译通过
arrayList1.add(123); //编译错误
String str1 = arrayList1.get(0); //返回类型是 String
ArrayList<String> arrayList2 = new ArrayList();
arrayList2.add("123"); //编译通过
arrayList2.add(123); //编译错误
String object2 = arrayList2.get(0); //返回类型是 String ArrayList arrayList3 = new ArrayList<String>(); arrayList3.add("123"); //编译通过 arrayList3.add(123); //编译通过 Object object3 = arrayList3.get(0); //返回类型是 Object

所以说擦除前的类型检查是针对引用的,用这个引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象

先检查再擦除带来的另一个问题就是泛型中参数化类型无法支持继承关系,因为泛型的设计初衷就是为了解决 Object 类型转换的弊端而存在,如果泛型中参数化类型支持继承操作就违背了设计的初衷而继续回到原始的 Object 类型转换弊端。也同样可以说这是为了兼容带来的问题,如下:

ArrayList<Object> arrayList1 = new ArrayList<Object>();  

arrayList1.add(new Object());  

arrayList1.add(new Object());
ArrayList<String> arrayList2 = arrayList1; //编译错误 ArrayList<String> arrayList3 = new ArrayList<String>();
arrayList3.add("abc"); arrayList3.add(new String()); ArrayList<Object> arrayList4 = arrayList3; //编译错误 ArrayList<String> arrayList5 = new ArrayList<Object>(); //编译错误 ArrayList<Object> arrayList6 = new ArrayList<String>(); //编译错误

之所以这样我们可以从反面来论证,假设编译不报错则当通过 arrayList2 调用 get() 方法取值时返回的是 String 类型的对象(因为类型检测是根据引用来决定的),而实际上存放的是 Object 类型的对象,这样 get 出来就会 ClassCastException 了,所以这违背了泛型的初衷。对于 arrayList4 同样假设编译不报错,当调用 arrayList4 的 get() 方法取出来的 String 变成了 Object 虽然不会出现 ClassCastException,但是依然没有意义啊,泛型出现的原因就是为了解决类型转换的问题,其次如果我们通过 arrayList4 的 add() 方法继续添加对象则可以添加任意类型对象实例,这就会导致我们 get() 时更加懵逼不知道加的是什么类型了,所以怎么说都是个死循环。

擦除带来的另一个问题就是泛型与多态的冲突,其通过子类中生成桥方法解决了多态冲突问题,这个问题的验证也很简单,可以通过下面的例子说明:

class Creater<T> {  

   private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class StringCreater extends Creater<String> {
@Override
public void setValue(String value) {
super.setValue(value);
}
@Override
public String getValue() {
return super.getValue();
} }
StringCreater stringCreater =new StringCreater();
stringCreater.setValue("abc");
stringCreater.setValue(new Object()); //编译错误

上面代码段的运行情况很诧异吧,按理来说 Creater 类被编译擦除后 setValue 方法的参数应该是 Object 类型了,子类 StringCreater 的 setValue 方法参数类型为 String,看起来父子类的这组方法应该是重载关系,所以调用子类的 setValue 方法添加字符串和 Object 类型参数应该都是合法才对,然而从编译来看子类根本没有继承自父类参数为 Object 类型的 setValue 方法,所以说子类的 setValue 方法是对父类的重写而不是重载(从子类添加 @Override 注解没报错也能说明是重写关系)。关于出现上面现象的原理其实我们通过 javap 看下两个类编译后的本质即可:

class StringCreater extends Creater<java.lang.String> {

 ......

 public void setValue(java.lang.String);    //重写的setValue方法

   Code:
0: aload_0
1: aload_1
2: invokespecial #2 // Method Creater.setValue:(Ljava/lang/Object;)V
5: return public java.lang.String getValue(); //重写的getValue方法 Code:
0: aload_0
1: invokespecial #3 // Method Creater.getValue:()Ljava/lang/Object;
4: checkcast #4 // class java/lang/String
7: areturn public void setValue(java.lang.Object); //编译器生成的桥方法,调用我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: checkcast #4 // class java/lang/String
5: invokevirtual #5 // Method setValue:(Ljava/lang/String;)V
8: return public java.lang.Object getValue(); //编译器生成的桥方法,调用我们重写的getValue方法
Code:
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/lang/String;
4: areturn
}

通过编译后的字节码我们可以看到 Creater 泛型类在编译后类型被擦除为 Object,而我们子类的本意是进行重写实现多态,可类型擦除后子类就和多态产生了冲突,所以编译后的字节码里就出现了桥方法来实现多态。可以看到桥方法的参数类型都是 Object,也就是说子类中真正覆盖父类方法的是桥方法,而子类 String 参数 setValue、getValue 方法上的 @Oveerride 注解只是个假象,桥方法的内部实现是直接调用了我们自己重写的那两个方法;不过上面的 setValue 方法是为了解决类型擦除与多态之间的冲突生成的桥方法,而 getValue 是一种协变,之所以子类中 Object getValue() 和 String getValue() 方法可以同时存在是虚拟机内部的一种区分(我们自己写的代码是不允许这样的),因为虚拟机内部是通过参数类型和返回类型来确定一个方法签名的,所以编译器为了实现泛型的多态允许自己做这个看起来不合法的实现,实质还是交给了虚拟机去区别。

先检查再擦除带来的另一个问题就是泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

关于这个可以通过 javap 去查看使用 List 的 add、get 方法后的字节码指令,你会发现 checkcast 指令不是在 get 方法里面强转的(虽然 get 方法里面返回值在代码里面是被转换成了 T,实际编译擦除了),而是在调用处强转的。

擦除带来的另一个问题是泛型类型参数不能是基本类型,比如 ArrayList<int> 是不合法的,只有 ArrayList<Integer> 是合法的,因为当类型擦除后 ArrayList 的原始类型是 Object,而 Object 是引用类型而不是基本类型。

擦除带来的另一个问题是无法进行具体泛型参数类型的运行时类型检查,譬如 arrayList instanceof ArrayList<String> 是非法的,Java 对于泛型运行时检查的支持仅限于 arrayList instanceof ArrayList<?> 方式。

擦除带来的另一个问题是我们不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除掉,擦除后两个 catch 会变成一样的东西。也不能在 catch 子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如 catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch 子句中使用则违背了异常的捕获优先级顺序。

这一个题目能说明白和全面泛型基本就掌握百分之九十了。

10. 问:为什么 Java 的泛型数组不能采用具体的泛型类型进行初始化?

答:这个问题可以通过一个例子来说明。

List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.

由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。而对于下面的代码来说是成立的:

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK

所以说采用通配符的方式初始化泛型数组是允许的,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。综述就是说 Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。

关于这道题的答案其 Oracle 官方文档给出了原因:https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html

11. 问:下面语句哪些是有问题,哪些没有问题?

List<String>[] list1 = new ArrayList<String>[10];    //编译错误,非法创建
List<String>[] list2 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list3 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 List<?>[] list4 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list5 = new ArrayList<?>[10]; //OK
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告

答:上面每个语句的问题注释部分已经阐明了,因为在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。

12. 问:如何正确的初始化泛型数组实例?

答:这个无论我们通过 new ArrayList[10] 的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式,我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下:

public class ArrayWithTypeToken<T> {  

   private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
} public T get(int index) {
return array[index];
}
public T[] create() {
return array;
} }
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T 在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。

13. 问:Java 泛型对象能实例化 T t = new T() 吗,为什么?

答:不能,因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于 T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。如果要实例化一个泛型 T 则可以通过反射实现(实例化泛型数组也类似),如下:

static <T> T newTclass(Class<T> clazz) throws InstantiationException, IllegalAccessException {  

   T obj = clazz.newInstance();  

   return obj;  

}

原因就不解释了,姑且可以认为和上面泛型数组创建一个原因,至于本质深层次原因请关注后边关于泛型反射面试题的推送。

14. 问:什么是 Java 泛型中的限定通配符和非限定通配符?有什么区别?

答:限定通配符对类型进行限制,泛型中有两种限定通配符,一种是 <? extends T> 来保证泛型类型必须是 T 的子类来设定泛型类型的上边界,另一种是 <? super T> 来保证泛型类型必须是 T 的父类来设定类型的下边界,泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。非限定通配符 <?> 表示可以用任意泛型类型来替代,可以在某种意义上来说是泛型向上转型的语法格式,因为 List<String> 与 List<Object> 不存在继承关系。

15. 问:简单说说 List<Object> 与 List 原始类型之间的区别?

答:主要区别有两点。

  • 原始类型和带泛型参数类型 <Object> 之间的主要区别是在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用 Object 作为类型可以告知编译器该方法可以接受任何类型的对象(比如 String 或 Integer)。

  • 我们可以把任何带参数的类型传递给原始类型 List,但却不能把 List<String> 传递给接受 List<Object> 的方法,因为会产生编译错误。

16. 问:简单说说 List<Object> 与 List<?> 类型之间的区别?

答:这道题跟上一道题看起来很像,实质上却完全不同。List<?> 是一个未知类型的 List,而 List<Object> 其实是任意类型的 List,我们可以把 List<String>、List<Integer> 赋值给 List<?>,却不能把 List<String> 赋值给 List<Object>。譬如:

List<?> listOfAnyType;

List<Object> listOfObject = new ArrayList<Object>();

List<String> listOfString = new ArrayList<String>();

List<Integer> listOfInteger = new ArrayList<Integer>();

listOfAnyType = listOfString; //legal

listOfAnyType = listOfInteger; //legal

listOfObjectType = (List<Object>) listOfString; //compiler error 

所以通配符形式都可以用类型参数的形式来替代,通配符能做的用类型参数都能做。 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。 如果类型参数之间有依赖关系或者返回值依赖类型参数或者需要写操作则只能用类型参数。

17. 问:List<? extends T>和List <? super T>之间有什么区别?

答:有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符,这两个 List 的声明都是限定通配符的例子,List<? extends T> 可以接受任何继承自 T 的类型的 List,而 List<? super T> 可以接受任何 T 的父类构成的 List。例如 List<? extends Number> 可以接受 List<Integer> 或 List<Float>。Java 容器类的实现中有很多这种用法,比如  Collections 中就有如下一些方法:

public static <T extends Comparable<? super T>> void sort(List<T> list)

public static <T> void sort(List<T> list, Comparator<? super T> c)

public static <T> void copy(List<? super T> dest, List<? extends T> src)

public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)

18. 问:说说 <T extends E> 和 <? extends E> 有什么区别?

答:它们用的地方不一样,<T extends E> 用于定义类型参数,声明了一个类型参数 T,可放在泛型类定义中类名后面、接口后面、泛型方法返回值前面。 <? extends E> 用于实例化类型参数,用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是 E 或 E 的某个子类型。虽然它们不一样,但两种写法经常可以达到相同的目的,譬如:

public void addAll(Bean<? extends E> c)

public <T extends E> void addAll(Bean<T> c)

19. 问:说说 List<String> 与 List<Object> 的关系和区别?

答:这两个东西没有关系只有区别。

因为也许很多人认为 String 是 Object 的子类,所以 List<String> 应当可以用在需要 List<Object> 的地方,但是事实并非如此,泛型类型之间不具备泛型参数类型的继承关系,所以 List<String> 和 List<Object> 没有关系,无法转换。

20. 问:下面两个代码片段有问题吗,为什么?

//Part 1

List<Object> obj = new ArrayList<Long>(); obj.add("I love Android!");

//Part 2

Object[] objArray = new Long[1]; objArray[0] = "I love Android!";

答:上面 Part 1 编译出错,Part 2 编译 OK,运行出错。

因为 List<Object> 和 ArrayList<Long> 没有继承关系,而 Java 的数组是在运行时类型检查的。

21. 问:如何把 int 值放入 ArrayList<String> list = new ArrayList<String>(); 的 list 列表中?

答:本题实质为泛型的擦除,不过答案比较多,常见的一种是通过兼容性,一种是通过反射的特性来处理。

通过泛型擦除兼容性实现如下:

ArrayList<String> list = new ArrayList<String>();

ArrayList list1 = list;

list1.add(12);
System.out.println(list1.get(0)); 

通过反射实现如下:

ArrayList<String> list = new ArrayList<String>();

Class clazz = list.getClass();

Method m = clazz.getMethod("add", Object.class);

m.invoke(list, 100); 

22. 问:泛型擦除到底擦除了哪些信息?

答:这道题就比较有意思和深度了,很多没有深入了解泛型的人可能听到这道题就觉得题出的有问题,因为在他们的认识里泛型信息都被擦除了,怎么还分擦除了哪些信息?难道还分情况?答案是确定的,泛型擦除其实是分情况擦除的,不是完全擦除,一定要消除这个误区。

Java 在编译时会在字节码里指令集之外的地方保留部分泛型信息,泛型接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除。

感兴趣的可以自己编写各种场景的泛型代码然后编译后反编译查看即可发现。

23. 问:既然泛型类型在编译时就被擦除了,那类似 Gson 这种 json 解析框架是如何解析数据到泛型类型 Bean 结构的呢?

答:本题其实一箭双雕,即考察了对于 Gson 框架是否熟悉又考察了 Java 泛型与反射的关系及泛型的实质。

由于在运行期间无法通过 getClass() 得知 T 的具体类型,所以 Gson 通过借助 TypeToken 类来解决这个问题,使用样例如下:

ArrayList<String> list = new ArrayList<String>();

list.add("java");

Type type = new TypeToken<ArrayList<String>>(){}.getType();

String gStr = new Gson().toJson(list, type);

ArrayList<String> gList = new Gson().fromJson(gStr, type);

可以看到 TypeToken 的使用非常简单,只用将需要获取类型的泛型类作为 TypeToken 的泛型参数构造一个匿名的子类就可以通过 getType() 方法获取到我们使用的泛型类的泛型参数类型。

通过上面的使用样例我们会发现使用 Gson 解析转换的 Bean 不存在特殊的构造方法,因此可以排除在泛型类的构造方法中显示地传入泛型类的 Class 类型作为这个泛型类的私有属性来保存泛型类的类型信息的实现方案,所以通过源码分析发现 Gson 使用了另一种方式来获取泛型的类型参数,其方法依赖 Java 的 Class 字节码中存储的泛型参数信息,Java 的泛型机制虽然在编译期间进行了擦除,但是在编译 Java 源代码成 class 文件中还是保存了泛型相关的信息,这些信息被保存在 class 字节码的常量池中,使用了泛型的代码处会生成一个 signature 签名字段,通过签名 signature 字段指明这个常量池的地址,JDK 提供了方法去读取这些泛型信息的方法,然后再借助反射就可以获得泛型参数的具体类型,具体实现原理如下:

Type mySuperClass = new ArrayList<String>(){}.getClass().getGenericSuperclass();
Type type = ((ParameterizedType) mySuperClass).getActualTypeArguments()[0];
System.out.println(type);

所以获取泛型参数类型的实质就是通过 Class 类的 getGenericSuperClass() 方法返回一个 ParameterizedType 对象(对于 Object、接口和原始类型返回 null,对于数组 class 返回 Object.class),ParameterizedType 表示带有泛型参数类型的 Java 类型,JDK1.5 引入泛型后 Java 中所有的 Class 都实现了 Type 接口,ParameterizedType 继承了 Type 接口,所有包含泛型的 Class 类都会自动实现这个接口。

关于 class 文件中存储泛型参数类型的详细信息可以参考:http://*.com/questions/937933/where-are-generic-types-stored-in-java-class-files

24. 问:下面程序的输出是什么?为什么?

public class Demo {

   public static void main(String[] args) throws Exception {

       ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
System.out.println(type.getActualTypeArguments()[0]);
ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
System.out.println(fieldType.getActualTypeArguments()[0]);
ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0]; System.out.println(paramType.getActualTypeArguments()[0]);
System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]); } class Foo<T extends CharSequence> { public List<Bar> children = new ArrayList<Bar>(); public List<StringBuilder> foo(List<String> foo) {
return null;
}
public void bar(List<? extends String> param) {
//empty
}
}
class Bar extends Foo<String> {}
}

答:其运行结果如下。

class java.lang.String
class Demo$Bar
class java.lang.String
interface java.lang.CharSequence 

通过上面例子会发现泛型类型的每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到,因为泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)。

25. 问:请说说下面代码片段中注释行执行结果和原因?

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); //这三行add现象?
numbers.add((Number)a);
numbers.add((Object)a); public void copyTo(DynamicArray<? super E> dest){ for(int i=0; i<size; i++){ dest.add(get(i)); //这行add现象?
}

答:上面代码段注释行执行情况解释如下。

三个 add 方法都是非法的,无论是 Integer,还是 Number 或 Object,编译器都会报错。因为 ? 表示类型安全无知,? extends Number 表示是 Number 的某个子类型,但不知道具体子类型, 如果允许写入,Java 就无法确保类型安全性,所以直接禁止。

最后方法的 add 是合法的,因为 <? super E> 形式与 <? extends E> 正好相反,超类型通配符表示 E 的某个父类型,有了它我们就可以更灵活的写入了。

本题特别重要:一定要注意泛型类型声明变量 ?时写数据的规则。

26. 问:请说说下面代码片段中注释行执行结果和原因?

Vector<? extends Number> x1 = new Vector<Integer>();    //正确
Vector<? extends Number> x2 = new Vector<String>(); //编译错误 Vector<? super Integer> y1 = new Vector<Number>(); //正确
Vector<? super Integer> y2 = new Vector<Byte>(); //编译错误

答:上面代码编译运行情况如注释所示,本题主要考察泛型中的 ? 通配符的上下边界扩展问题。

通配符对于上边界有如下限制:Vector<? extends 类型1> x = new Vector<类型2>(); 中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的子类。

通配符对于下边界有如下限制:Vector<? super 类型1> x = new Vector<类型2>(); 中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的父类。

27. 问:下面程序合法吗?

class Bean<T super Student> { //TODO }

答:编译时报错,因为 Java 类型参数限定只有 extends 形式,没有 super 形式。

28. 问:下面程序有什么问题?该如何修复?

public class Test {  

   public static void main(String[] args) throws Exception{
List<Integer> listInteger = new ArrayList<Integer>();
printCollection(listInteger);
}
public static void printCollection(Collection<Object> collection) {
for(Object obj:collection){
System.out.println(obj);
}
}
}

答:语句 printCollection(listInteger); 编译报错,因为泛型的参数是没有继承关系的。修复方式就是使用 ?通配符,printCollection(Collection<?> collection),因为在方法 printCollection(Collection<?> collection) 中不可以出现与参数类型有关的方法,譬如 collection.add(),因为程序调用这个方法的时候传入的参数不知道是什么类型的,但是可以调用与参数类型无关的方法,譬如

 collection.size()。 

29. 问:请解释下面程序片段的执行情况及原因?

public class Test{
public static <T> T add(T x, T y){
return y;
}
public static void main(String[] args) {
int t0 = Test.add(10, 20.8);
int t1 = Test.add(10, 20); Number t2 = Test.add(100, 22.2);
Object t3 = Test.add(121, "abc");
int t4 = Test.<Integer>add(10, 20); int t5 = Test.<Integer>add(100, 22.2);
Number t6 = Test.<Number>add(121, 22.2);
}
}

答:t0 编译直接报错,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型,而 t0 类型为 int,所以类型错误。

  • t1 执行赋值成功,add 的两个参数都是 Integer,所以 T 为 Integer 类型。

  • t2 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型。

  • t3 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Object,故 T 为 Object 类型。

  • t4 执行赋值成功,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数。

  • t5 编译直接报错,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数,不能为 Float。

  • t6 执行赋值成功,add 指定了泛型类型为 Number,所以只能 add 为 Number 类型或者其子类的参数,Integer 和 Float 均为其子类,所以可以 add 成功。

t0、t1、t2、t3 其实演示了调用泛型方法不指定泛型的几种情况,t4、t5、t6 演示了调用泛型方法指定泛型的情况。 在调用泛型方法的时可以指定泛型,也可以不指定泛型;在不指定泛型时泛型变量的类型为该方法中的几种类型的同一个父类的最小级(直到 Object),在指定泛型时该方法中的几种类型必须是该泛型实例类型或者其子类。切记,java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。

30. 问:下面两个方法有什么区别?为什么?

public static <T> T get1(T t1, T t2) {  

   if(t1.compareTo(t2) >= 0);

   return t1;  

}  

public static <T extends Comparable> T get2(T t1, T t2) {

   if(t1.compareTo(t2) >= 0);  

   return t1;  

}

答:get1 方法直接编译错误,因为编译器在编译前首先进行了泛型检查和泛型擦除才编译,所以等到真正编译时 T 由于没有类型限定自动擦除为 Object 类型,所以只能调用 Object 的方法,而 Object 没有 compareTo 方法。

get2 方法添加了泛型类型限定可以正常使用,因为限定类型为 Comparable 接口,其存在 compareTo 方法,所以 t1、t2 擦除后被强转成功。所以类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过不管该限定是类还是接口都使用 extends 和 & 符号,如果限定类型既有接口也有类则类必须只有一个且放在首位,如果泛型类型变量有多个限定则原始类型就用第一个边界的类型变量来替换。