答:泛型是 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 list = new ArrayList ();//JDK 1.7 推荐使用的写法List llist = new ArrayList<>();//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告List list = new ArrayList();//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告List list = new ArrayList ();
2. 问:Java 泛型是如何工作的?什么是类型擦除?
答:泛型是通过类型擦除来实现的,编译器在编译时擦除了所有泛型类型相关的信息,所以在运行时不存在任何泛型类型相关的信息,譬如 List<Integer> 在运行时仅用一个 List 来表示,这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。
3.问:Java 泛型类、泛型接口、泛型方法有什么区别?
答:泛型类是在实例化类的对象时才能确定的类型,其定义譬如 class Test<T> {},在实例化该类时必须指明泛型 T 的具体类型。
泛型接口与泛型类一样,其定义譬如 interface Generator<E> { E dunc(E e); }。
因为 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 { private void func1(Object arg) { if (arg instanceof T) {} } private void func2() { T var = new T(); } private void func3() { T[] vars = new T[3]; }}
List [] lsa = new List [10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List li = new ArrayList (); 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.
List [] lsa = new List [10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List li = new ArrayList (); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK
答:这个无论我们通过 new ArrayList[10] 的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式,我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下:
public class ArrayWithTypeToken { private T[] array; public ArrayWithTypeToken(Class 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 arrayToken = new ArrayWithTypeToken (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 newTclass(Class clazz) throws InstantiationException, IllegalAccessException { T obj = clazz.newInstance(); return obj; }
由于在运行期间无法通过 getClass() 得知 T 的具体类型,所以 Gson 通过借助 TypeToken 类来解决这个问题,使用样例如下:
ArrayList list = new ArrayList ();list.add("java");Type type = new TypeToken >(){}.getType();String gStr = new Gson().toJson(list, type);ArrayList gList = new Gson().fromJson(gStr, type);
通过上面的使用样例我们会发现使用 Gson 解析转换的 Bean 不存在特殊的构造方法,因此可以排除在泛型类的构造方法中显示地传入泛型类的 Class 类型作为这个泛型类的私有属性来保存泛型类的类型信息的实现方案,所以通过源码分析发现 Gson 使用了另一种方式来获取泛型的类型参数,其方法依赖 Java 的 Class 字节码中存储的泛型参数信息,Java 的泛型机制虽然在编译期间进行了擦除,但是在编译 Java 源代码成 class 文件中还是保存了泛型相关的信息,这些信息被保存在 class 字节码的常量池中,使用了泛型的代码处会生成一个 signature 签名字段,通过签名 signature 字段指明这个常量池的地址,JDK 提供了方法去读取这些泛型信息的方法,然后再借助反射就可以获得泛型参数的具体类型,具体实现原理如下:
Type mySuperClass = new ArrayList (){}.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://stackoverflow.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 { public List children = new ArrayList (); public List foo(List foo) { return null; } public void bar(List param) { //empty } } class Bar extends Foo {}}
答:其运行结果如下。
class java.lang.Stringclass Demo$Barclass java.lang.Stringinterface java.lang.CharSequence
DynamicArray ints = new DynamicArray<>();DynamicArray numbers = ints;Integer a = 200;numbers.add(a); //这三行add现象?numbers.add((Number)a);numbers.add((Object)a);public void copyTo(DynamicArray dest){ for(int i=0; i
答:上面代码段注释行执行情况解释如下。
三个 add 方法都是非法的,无论是 Integer,还是 Number 或 Object,编译器都会报错。因为 ? 表示类型安全无知,? extends Number 表示是 Number 的某个子类型,但不知道具体子类型, 如果允许写入,Java 就无法确保类型安全性,所以直接禁止。
最后方法的 add 是合法的,因为 <? super E> 形式与 <? extends E> 正好相反,超类型通配符表示 E 的某个父类型,有了它我们就可以更灵活的写入了。
本题特别重要:一定要注意泛型类型声明变量 ?时写数据的规则。
26. 问:请说说下面代码片段中注释行执行结果和原因?
Vector x1 = new Vector (); //正确Vector x2 = new Vector (); //编译错误Vector y1 = new Vector (); //正确Vector y2 = new Vector (); //编译错误
答:上面代码编译运行情况如注释所示,本题主要考察泛型中的 ? 通配符的上下边界扩展问题。
通配符对于上边界有如下限制: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 { //TODO }
答:编译时报错,因为 Java 类型参数限定只有 extends 形式,没有 super 形式。
28. 问:下面程序有什么问题?该如何修复?
public class Test { public static void main(String[] args) throws Exception{ List listInteger = new ArrayList (); printCollection(listInteger); } public static void printCollection(Collection collection) { for(Object obj:collection){ System.out.println(obj); } } }
public class Test{ public static 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. add(10, 20); int t5 = Test. add(100, 22.2); Number t6 = Test. 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 类型。