Java 泛型
Reference:
Java编程思想
---------------------------------------------------------------------------------------------------------
Why泛型?
在考虑代码重用的时候, 自然的想法是, 希望写一份逻辑可以用于不同的场景, 比如写一份算法逻辑可以使用于各种类型, 这就是泛化的需求
类多态
对于面向对象语言而言, 首先想到的泛化的方法, 肯定是多态, 使用基类reference, 可以代码所有的子类
尤其对于Java这样的单根继承体系, 多态应该可以解决大部分问题, 因为object reference可以代表所有类
这是不是也是Java开始设计时, 没有考虑泛型的原因?
但这样做的问题是, 编译器无法做有效的类型检查, 并且在效率上也有问题
所以往往还是会限定在具体的基类上
接口
当然有时候通过类多态来实现泛化的限制太强了, 而Java中支持接口, 如果通过接口来实现泛化, 会更灵活一些
因为不需要严格的继承关系, 只需要实现了该接口就ok
泛型
这是一种更为高的泛化要求, 在编写逻辑时对适用类型没有任何假设(基类, 接口), 但在使用时, 却需要指定一种类型, 以便于类型的安全检查和效率
泛型产生的最主要的动因是由于容器类对泛型的需求
泛型, 即参数化类型, 达到解耦类或方法与所使用类型间的约束的目的
//没有任何泛化 class Automobile {} public class Holder1 { private Automobile a; public Holder1(Automobile a) { this.a = a; } Automobile get() { return a; } } //使用object来实现泛化, 可以放入任意对象, 无法进行类型检查 public class Holder2 { private Object a; public Holder2(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get() { return a; } public static void main(String[] args) { Holder2 h2 = new Holder2(new Automobile()); Automobile a = (Automobile)h2.get(); h2.set("Not an Automobile"); String s = (String)h2.get(); h2.set(1); // Autoboxes to Integer Integer x = (Integer)h2.get(); } } //泛型的实现, 会做类型检查, 只接收Automobile类 public class Holder3<T> { private T a; public Holder3(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile()); Automobile a = h3.get(); // No cast needed // h3.set("Not an Automobile"); // Error // h3.set(1); // Error } }
Java泛型
泛型, 即参数化类型, 我接触到最早的应用例子是C++模板库STL, 模板是一种设计模式, 而泛型可以说是模板模式的一种典型的应用
Java的设计是参考C++, 所以可以说Java的泛型和C++的类似, 不过是大大弱化的
ArrayList apples = new ArrayList(); //默认为object apples.add(new Apple()); Apple a = (Apple)apples.get(0); //手动转型, 如类型不对会抛ClassCastException异常
Java泛型的主要目的: 在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率
可以看到Java泛型的主要目的是类型检查和自动类型转换, 而非更好的泛化, 所以有人说Java的泛型不是真正的泛型, 只是伪泛型
对比上下的代码,
List<String> list = new ArrayList<String>(); list.add("value"); //[类型安全的写入数据] 编译器检查该值,该值必须是String类型才能通过编译 String str = list.get(0); //[类型安全的读取数据] 不需要手动转换
泛型的规则和限制
1、泛型的类型参数只能是类类型(包括自定义类), 不能是简单类型, 如<int>不可以 2、同一种泛型可以对应多个版本(因为参数类型是不确定的), 不同版本的泛型类实例是不兼容的, 因为在Java看来, 他们是完全不同的类型 3、泛型的类型参数可以有多个, 比如<T, P>, 同时还可以嵌套泛型, 例如<List<String>> 4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上成为“有界类型”。 5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(Java.lang.String);
泛型使用
泛型定义的例子
//声明了一个泛型类 class GenericsFoo<T> //实际上相当于 class GenericsFoo<T extends Object> //使用泛型类 GenericsFoo<Double> douFoo=new GenericsFoo<Double>(new Double("33"));
泛型接口
泛型除了可以应用于类, 还能用于接口, 比如下面的generator的例子,
// 泛型接口 public interface Generator<T> { T next(); }
public class CoffeeGenerator implements Generator<Coffee> { //实现泛型接口 //实现泛型接口中的method public Coffee next() { try { return (Coffee) types[rand.nextInt(types.length)].newInstance(); // Report programmer errors at run time: } catch(Exception e) { throw new RuntimeException(e); } } }
泛型方法
相对比较简单, 不管类, 单纯只是方法支持泛型, 应该尽量使用泛型方法来替代泛型类, 这样更简单明了
并且对于static方法而言, 无法访问到类的泛型参数, 所以如果static方法需要使用泛型, 则必须使用泛型方法
泛型方法和一般方法使用时没有区别, 编译器会自动判断类型, 称为type argument inference
public class GenericMethods { //定义泛型方法 public <T> void f(T x) { System.out.println(x.getClass().getName()); } public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f(""); //和普通方法在使用上没有任何区别 gm.f(1); //对于基本类型, 会自动打包 gm.f(1.0); gm.f(1.0F); gm.f(‘c’); gm.f(gm); } }
Java泛型的局限, 泛型擦除
先来看个现象, 称为泛型擦除, 即在运行时, 无法获得任何关于类型参数的信息, 因为这些信息在编译的时候就已经被抹去了
故JVM根本就不知道泛型的存在
类型擦除指的是通过类型参数合并, 将泛型类型实例关联到同一份字节码上, 编译器只为泛型类型生成一份字节码, 并将其实例关联到这份字节码上.
类型擦除的关键在于从泛型类型中清除类型参数的相关信息, 并且再必要的时候添加类型检查和类型转换的方法
public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } } //Output: true 运行时, 泛型参数已经抹去, 所以看到的c1和c2都只是ArrayList而已
那么泛型擦除是怎样实现的, 其实也很简单, 所谓的擦除其实是一种替换, 用泛型的上界来替换类型参数T
在一般情况下, 不特定的指定上界, 那么上界默认肯定是object, 所以对于一般的泛型而言, 在运行期的时候其实都可以当作object
所以下面的例子一定会报错, 因为在运行时, JVM认为obj是Object, 而Object是没有f()的
但在C++中, 这样写是可以的, 因为C++的泛型信息会保留到运行期, 所以只要你指定的T类型确实有f()即可
class Manipulator<T> { private T obj; public Manipulator(T x) { obj = x; } // Error: cannot find symbol: method f(): public void manipulate() { obj.f(); } }
解决这个问题的方法就是特别指定泛型上界, 如下面例子,
这样Manipulator2的上界就由Object窄化为HasF, 这样编译的时候就会使用HasF来替换T
HasF本身是有f()的, 所以编译通过
class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } }
但其实你可以自己完成擦除替换的过程, 直接把T换成HasF, 根本就不用泛型
class Manipulator3 { private HasF obj; public Manipulator3(HasF x) { obj = x; } public void manipulate() { obj.f(); } }
所以如果你只是想泛化类及其子类, 那么Java的泛型是无用的, 因为用多态就足够
也就是说, 泛型应该用于更加泛化的场景, 比如用于跨多个独立类, 或比较复杂的场景
总结就是说, 因为Java泛型的实现, 伪泛型, 其实是比较鸡肋的功能, 食之无味, 弃之可惜……
所以你要理解, 泛型擦除不是Java的一个牛比功能, 这是一个缺陷而已, 泛型实现中的一种折衷
为什么要这样实现? 迁移兼容性
因为在Java1.0的时候并没有考虑泛型的需求, 也许是因为单根继承, 所以多态已经可以比较好进行泛化, 所以开始没有强烈的需求
但是到了想加泛化的时候, Java已经有了庞大的现有的库
如果问Java现在最有价值的地方是什么, 毫无疑问是大量的库, 否则真是没有什么理由继续用这样繁琐的语言
当时, 此时他最大从财富, 也成了语言进化最大的包袱, 为了兼容现有的大量的非泛化的库, 所以采取了泛型擦除实现. 当然, 尽量减少对JVM架构的改动也应该是一个理由.
泛型擦除带来的问题
1. Generic types cannot be used in operations that explicitly refer to runtime types, such as casts, instanceof operations, and new expressions.
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { if(arg instanceof T) {} // Error T var = new T(); // Error T[] array = new T[SIZE]; // Error, 可以使用private List<T> array = new ArrayList<T>();来替换 T[] array = (T)new Object[SIZE]; // Unchecked warning } }
对于上面问题的解决方法, 往往解决class对象, 虽然T在运行期不可用, 但是可以将T.class对象做为参数传入
public class ClassTypeCapture<T> { Class<T> kind; public ClassTypeCapture(Class<T> kind) { //T本身无法保留到运行期, 故传入class对象 this.kind = kind; } public boolean f(Object arg) { return kind.isInstance(arg); //使用class.isInstance } } class ClassAsFactory<T> { T x; public ClassAsFactory(Class<T> kind) { try { x = kind.newInstance(); //通过class对象的newInstance来创建对象 } catch(Exception e) { throw new RuntimeException(e); } } }
2. Use of generics is not enforced
class Derived1<T> extends GenericBase<T> {} class Derived2 extends GenericBase {} // No warning, 可以忽略泛型部分
3. 不能基于泛型重载
在JVM看来, 两个test的参数都是List, 所以报错
public class Erasure{ public void test(List<String> ls){ System.out.println("Sting"); } public void test(List<Integer> li){ System.out.println("Integer"); } }
4. 不能catch同一个泛型异常类的多个实例
原因同上, 比如下面两个对于JVM是一样的
catch GenericException<Integer>
catch GenericException<String>
5. 泛型类的静态变量是共享的
由于经过类型擦除, 所有的泛型类实例都关联到同一份字节码上, 泛型类的所有静态变量是共享的
public class StaticTest{ public static void main(String[] args){ GT<Integer> gti = new GT<Integer>(); gti.var=1; GT<String> gts = new GT<String>(); gts.var=2; System.out.println(gti.var); //等于2,而不是1 } } class GT<T>{ public static int var=0; public void nothing(T x){} }
泛型边界
看过上面的例子, 应该知道其实所有泛型都是有上界的, 那就是object
而在擦除中, 所有泛型都会被擦除到上界, 所以在普通泛型中, 只能使用object中的method
可以看泛型擦除中给的例子, 如果要使用其他的method, 必须把上界窄化到包含该method的基类
所以在Java中, 指定边界的最大意义就是可以调用边界上的method
//T类型是实现superclass接口的类型,或者T是继承了superclass类的类型 <T extends superclass> //上界 //例子 class GenericsFoo<T extends Collection>//泛型T只能是Collection接口的实现类,传入非Collection接口编译会出错
还能指定下界
<T super Class> //super是限制泛型参数只能是指定该class的上层父类
例子, 看出可以指定多个边界
interface HasColor { java.awt.Color getColor(); } class Dimension { public int x, y, z; } interface Weight { int weight(); } //指定多个边界, 需要注意的是必须先类(Dimension), 后接口(HasColor & Weight) //由于指定多边界, 所以可以在item上调用getColor, weight等 class Solid<T extends Dimension & HasColor & Weight> { T item; Solid(T item) { this.item = item; } T getItem() { return item; } java.awt.Color color() { return item.getColor(); } int getX() { return item.x; } int getY() { return item.y; } int getZ() { return item.z; } int weight() { return item.weight(); } }
但是上面的Solid不符合高聚合, 低耦合的原则, 应该把不同的接口封装在不同层次的类中
//这个设计就是把各个接口封装到不同的类中 //最终通过逐层继承来实现Solid2 class HoldItem<T> { T item; HoldItem(T item) { this.item = item; } T getItem() { return item; } } class Colored2<T extends HasColor> extends HoldItem<T> { Colored2(T item) { super(item); } java.awt.Color color() { return item.getColor(); } } class ColoredDimension2<T extends Dimension & HasColor> extends Colored2<T> { ColoredDimension2(T item) { super(item); } int getX() { return item.x; } int getY() { return item.y; } int getZ() { return item.z; } } class Solid2<T extends Dimension & HasColor & Weight> extends ColoredDimension2<T> { Solid2(T item) { super(item); } int weight() { return item.weight(); } }
通配符
协变(covariant)
协变(covariant)问题, 即回答这样的问题
如果Apple是Fruit的子类, 那么Apple容器是否也是Fruit容器的子类?
答案是, No. Java的泛型容器是不支持协变的
一个例外是, 数组类型, Java中数组是支持协变(covariant)的, 当然严格上说数组本身就不属于容器类class Fruit {} class Apple extends Fruit {} class Jonathan extends Apple {} class Orange extends Fruit {} public class CovariantArrays { public static void main(String[] args) { Fruit[] fruit = new Apple[10]; //正常的协变, Apple是Fruit的子类, 所以Apple[]是Fruit[]的子类 fruit[0] = new Apple(); // OK fruit[1] = new Jonathan(); // OK try { //编译时,没有问题, 你当然可以把一个Fruit对象放到一个Fruit数组中 //运行时会报错, 因为运行时会发现其实是一个Apple数组, 会涉及向下转型, 那是不安全的 fruit[0] = new Fruit(); // ArrayStoreException } catch(Exception e) { System.out.println(e); } } }
和数组同样的例子, 证明了上面的结论, 泛型容器不支持协变
在泛型容器中, List<Fruit>和List<Apple>是两种不同的List类型, 并不存在内在的继承关系
public class NonCovariantGenerics { // Compile Error: incompatible types: List<Fruit> flist = new ArrayList<Apple>(); }
这里自然产生一种需要, 如果需要协变, 需要在两个容器类型中建立这种向上转型的继承关系, 怎么办?
通配符出场, 它可以解决这个问题
可以看到, 加上通配符后, 就不会报错, 因为通配符就是表示支持协变
<? extends Fruit>, 表示可以接收Fruit或Fruit的任意子类, 但是现在还不确定是什么
这种不确定性带来的问题, 无法写入
如下, 就是你无法往list中加入任何对象, 因为在编译时期, 我只知道他是Fruit或Fruit的任意子类, 所以加入任意对象都有可能会导致向下转型
有人问, 前面不是new ArrayList<Apple>(), 问题是new只有在运行态的时候才会被执行, 现在编译器无法知道这个flist其实指向Apple list
那协变有什么用? 用于读取
因为协变是指定上界, 所以从容器中读出的任何对象都可以用上界来表示
比如下面的例子, Fruit f = flist.get(0); 没有任何问题
public class GenericsAndCovariance { public static void main(String[] args) { // Wildcards allow covariance: List<? extends Fruit> flist = new ArrayList<Apple>(); // Compile Error: can’t add any type of object: // flist.add(new Apple()); // flist.add(new Fruit()); // flist.add(new Object()); flist.add(null); // Legal but uninteresting // We know that it returns at least Fruit: Fruit f = flist.get(0); //ok, 这个没有疑问,因为flist至少是Fruit的子类 flist.contains(new Apple()); //ok flist.indexOf(new Apple()); //ok } }
但是你会发现, 如果调用flist.contains或flist.indexOf就没有问题, 不会报错, 为什么add偏偏不行?
答案其实很简单, 看看ArrayList的API, 因为add的参数类型是泛型, 而其他的是Object
这是设计者在设计的时候, 判断哪些method是安全的, 所以直接用Objectpublic class ArrayList<E> boolean add(E e) //参数类型是泛型E boolean contains(Object o) //参数类型是Object int indexOf(Object o)
逆变(Contravariance)
和协变相反, 指定了通配符的下界, <? super MyClass> 或 <? super T>
用于解决泛型写入问题
public class SuperTypeWildcards { static void writeTo(List<? super Apple> apples) { //指定下界为Apple apples.add(new Apple()); //ok, 放心的写入Apple或子类 apples.add(new Jonathan()); // apples.add(new Fruit()); // Error, 写入Fruit会导致向下转型, 不安全 } }
总之, 就是我们在考虑设定通配符的上界和下界问题时, 其实就是在考虑写入和读取的问题, 根据不同写入和读取限制来设定上下界
无界通配符(Unbounded wildcards)
The unbounded wildcard <?> appears to mean "anything," and so using an unbounded wildcard seems equivalent to using a raw type.
虽然下面三个在运行期, 没啥区别, 擦除完都是List<Object>
但在编译的时候, 还是有区别的,
原生List, 就是object的List, 类型确定, 而且由于是object, 所以其实里面放什么对象都可以, 没有任何编译检查
无界通配符List, 类型不确定, 虽然可以接收任何类型, 但是用的时候会确定为某一具体的类型, 编译器会进行类型检查
static List list1; static List<?> list2; static List<? extends Object> list3
无界通配符的应用, 类型部分确定
static Map<?,?> map2; static Map<String,?> map3;