泛型构造器
Java允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据参数类型来推断泛型形参类型,也可以显式地为构造器中的泛型形参指定实际类型。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Foo { public <T> Foo(T t) { System.out.println(t); } } public class GenericConstructor { public static void main(String[] args) { new Foo("疯狂Java讲义"); new Foo(200); new <String> Foo("疯狂Android讲义"); } }
|
前面说过的菱形语法,它允许调用构造器时在构造器后使用尖括号来代表泛型信息,但是如果程序显式指定了泛型构造器声明的泛型形参的实际类型,则不可以使用菱形语法。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class MyClass<E> { public <T> MyClass(T t) { System.out.println("t参数的值为:" + t); } } public class GenericDiamondTest { public static void main(String[] args) { MyClass<String> mc1 = new MyClass<>(5); MyClass<String> mc2 = new <Integer> MyClass<String>(5);
} }
|
泛型方法与方法重载
因为泛型既允许设定通配符上限,也允许设定通配符下限,所以允许在一个类包含以下两个方法定义
1 2 3 4
| public class MyUtils { public static <T> void copy(Collection<T> dest, Collection<? extends T> src){} public static <T> T copy(Collection<? super T> dest, Collection<T> src){} }
|
这两个方法参数列表存在一定区别,但区别不明显,两个方法的两个参数都是Collection对象,前一个集合元素类型是后一个集合元素类型的父类。在这里定义这两个方法不会有任何错误,但是如果调用这个方法就会出现错误。
例如
1 2 3
| List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); MyUtils.copy(ln, li);
|
两个方法都匹配,编译器无法确定到底调用哪个copy方法,所以会出现编译错误。
Java 8 改进了泛型方法的类型推断能力,主要有两个方面
- 可以通过调用方法的上下文来推断泛型的目标类型
- 可在方法调用链中,将推断得到的泛型传递到最后一个方法
例子
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
| class MyUtil<E> { public static <Z> MyUtil<Z> nil() { return null; } public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) { return null; } E head() { return null; } } public class InferenceTest { public static void main(String[] args) { MyUtil<String> ls = MyUtil.nil(); MyUtil<String> mu = MyUtil.<String>nil(); MyUtil.cons(42, MyUtil.nil()); MyUtil.cons(42, MyUtil.<Integer>nil());
String s = MyUtil.<String>nil().head(); } }
|
上面前两行代码作用完全一样,系统会推断出泛型参数为String,后两行代码作用也完全一样,系统会推断出Z的实参为Integer。
但是这种推断并不是万能的,例如下面代码就是错误的
String s = MyUtil.nil().head();
,希望系统可以推断出来,但是推断不出来,代码报错,需要改为String s = Mytil.<String>nil().head();
。
擦除和转换
在严格泛型代码中,带泛型声明的类应该总带着类型参数,但是为了和老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际类型,此时被称为原始类型,默认是声明该泛型形参时指定的第一个上限类型。
当把一个指定了泛型实参的对象赋给一个没有指定泛型实参的对象时,所有在尖括号之间的类型信息被扔掉,比如一个List类型被转换为List,该List对集合元素的类型检查变成了泛型参数的上限即Object,这叫做擦除。
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
| class Apple<T extends Number> { T size; public Apple() { } public Apple(T size) { this.size = size; } public void setSize(T size) { this.size = size; } public T getSize() { return this.size; } } public class ErasureTest { public static void main(String[] args) { Apple<Integer> a = new Apple<>(6); Integer as = a.getSize(); Apple b = a; Number size1 = b.getSize();
} }
|
上面代码定义一个带泛型形参的类Apple,上限是Number,当把a对象赋给不带泛型信息的b变量,编译器就会丢失a对象的泛型信息,因为Apple泛型形参的上限是Number,所以编译器知道b的getSize方法返回Number类型,但具体是Number的哪个子类,编译器不清楚。
从逻辑上看,List是List的子类,如果直接把List对象赋给List对象,应该会编译错误,但实际上不会,编译器仅会提示"未经检查的转换"。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import java.util.*; public class ErasureTest2 { public static void main(String[] args) { List<Integer> li = new ArrayList<>(); li.add(6); li.add(9); List list = li; List<String> ls = list; System.out.println(ls.get(0)); } }
|
Java允许把list赋给ls,但是list变量此时引用的是List集合,所以当试图把集合元素当成String类型取出来的时候,会引发ClassCastException异常。