Java-泛型
标签(空格分隔): Java
1. 泛型基础
1.1 泛型类
在编写普通类的时候,我们往往会固定一种类型,例如:
public class Box { private String object; public void set(String object) { this.object = object; } public String get() { return object; } }
这个类中只能添加String类型的,如果是Integer,那么就得修改或重写这个类,代码得不到复用。
引入泛型来解决上述问题,泛型类是普通类的工厂:
public class Box<T> { // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } } // 实例化泛型类 Box<Integer> integerBox = new Box<Integer>(); Box<Double> doubleBox = new Box<Double>(); Box<String> stringBox = new Box<String>();
一般来讲,E代表集合中的元素类型,K和V代表键值对的类型,T、U、S类型参数表示任意类型。
其中任意类型参数都不能是基本类型的,如果要用基本类型就要使用包装类,让它使用自动装箱和拆箱。
1.2 泛型方法
除了泛型类(在定义类的时候添加类型参数),还有泛型方法:
public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } // 调用泛型方法 boolean same = Util.<Integer, String>compare(p1, p2);
在Java1.7开始利用type inference,让Java自动推导出相应的类型参数:
boolean same = Util.compare(p1, p2);
1.3 边界符
上述了单独使用类型参数的方式,但是如果要比较类型参数,如果直接比较肯定是错误的,因为只有基本类型才能直接比较:
public static <T> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e > elem) // error ++count; return count; }
而要解决这个问题可以用边界符来解决,我们知道所有实现Comparable接口的类都可以使用Compare进行比较:
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) > 0) ++count; return count; }
extends和super:
这两个关键字都是类型参数的边界符,extends用来限制其上界,表示T是绑定类型的子类型;super用来限制其下界,表示T是绑定类型的父类型。界定的接口或类可有多个中间用&
隔开。
2. 通配符
通配符即?
表示任意未知类型。
2.1 通配符类型限定
2.1.1 子类型限定
可以知道的是,类型参数之间的继承关系和带有该类型参数的类的继承关系没有任何关系:
public class GenericReading { static List<Apple> apples = Arrays.asList(new Apple()); static List<Fruit> fruit = Arrays.asList(new Fruit()); static class Reader<T> { T readExact(List<T> list) { return list.get(0); } } static void f1() { Reader<Fruit> fruitReader = new Reader<Fruit>(); Fruit f = fruitReader.readExact(apples); //Errors: List<Fruit> cannot be applied to List<Apple>. } public static void main(String[] args) { f1(); } }
二者毫无疑问肯定是有关系,可以用通配符子类型限定? extends T
来体现其关系:
public class GenericReading { static List<Apple> apples = Arrays.asList(new Apple()); static List<Fruit> fruit = Arrays.asList(new Fruit()); static class CovariantReader<T> { T readCovariant(List<? extends T> list) { return list.get(0); } } static void f2() { CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>(); Fruit f = fruitReader.readCovariant(fruit); Fruit a = fruitReader.readCovariant(apples); } public static void main(String[] args) { f2(); } }
这样就相当与告诉编译器, fruitReader的readCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。
2.1.2 超类型限定
除了上述使用子类型限定? extends T
外,还有超类型限定? super T
,表示参数是类型参数T的父类。
2.1.3 无限定通配符
无限定的通配符?
和原生类型看似没有区别,在运行时都看成Object
类,但二者仍有区别,例如List<?>和List相比,前者表示“持有任何Object类型的原生List”,后者表示“具有某种特定类型的非原生List,只是不知道确切类型”。
示例:
public class Pair<T> { public T first; public T getFirst() { return first; } public void setFirst(T t) { first = t; } public static void main(String[] args) { Pair<?> p = new Pair<>(); p.setFirst(2); // compile error System.out.println(p.getFirst()); Pair p1 = new Pair(); p1.setFirst(2); // compile non-error System.out.println(p1.getFirst()); } }
可以得出无界通配符提供了编译检查,在setFirst方法中不能添加Object对象,但是可以添加null
;而原生类型则不提供,任意Object类型都可添加,这是极其不安全的操作。
2.2 PECS原则
PECS原则全称:Producer Extends, Consumer Super,说明类型限定的一些规则。
2.2.1 Producer Extends
使用<? extends T>
从list里面get和set元素:
public class GenericsAndCovariance { public static void main(String[] args) { List<? extends Fruit> flist = new ArrayList<>(); //Compile Error: can't add any type of object: flist.add(new Apple()) flist.add(new Orange()) flist.add(new Fruit()) flist.add(new Object()) // Compile non-error Fruit f = flist.get(0); } }
1)add错误:
编译器不允许我们这样做,因为List<? extends Fruit> flist它自身可以有多种含义:
List<? extends Fruit> flist = new ArrayList<Fruit>(); List<? extends Fruit> flist = new ArrayList<Apple>(); List<? extends Fruit> flist = new ArrayList<Orange>();
- 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错;
- 当我们尝试add一个Apple的时候,flist可能指向new ArrayList<orange>();</orange>
- 当我们尝试add一个Orange的时候,flist可能指向new ArrayList<apple>();</apple>
2)get正确:
get是正确的,因为可以确切地知道传入的类型的父类(多态)。
2.2.2 Consumer Super
使用<? super T>
从list里面get和set元素:
public class GenericsAndCovariance { public static void main(String[] args) { List<? super Apple> flist1 = new ArrayList<>(); flist1.add(new Apple()); // compile non-error flist1.add(new Fruit()); // compile error flist1.add(new Orange()); // compile error Fruit f1 = flist1.get(0); // compile error } }
1)add错误与正确:
发现add方法中,添加下界对象正确,添加其它对象编译错误,因为编译器无法知道其确切类型。
2)get错误:
从编译器的角度考虑这个问题,对于List<? super Apple> list,它可以有下面几种含义:
List<? super Apple> list = new ArrayList<Apple>(); List<? super Apple> list = new ArrayList<Fruit>(); List<? super Apple> list = new ArrayList<Object>();
当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
2.2.3 总结
- “Producer Extends” –如果你需要一个只读List,用它来produce T,那么使用? extends T。
- “Consumer Super” –如果你需要一个只写List,用它来consume T,那么使用? super T。
- 如果需要同时读取以及写入,那么我们就不能使用通配符了。
结合使用的示例(集合源码):
public class Collections { public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i=0; i<src.size(); i++) dest.set(i, src.get(i)); } }
3. 类型擦除
类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。
示例1:
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } // ... }
编译器做完相应类型检查之后,运行时上述代码转换为:
public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } // ... }
示例2:
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } // ... }
运行时:
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; } // ... }
通过上述的示例可以得出结论:
- 对于没有限定类型的类型参数在运行时都用
Object
来替换 - 对于有限定类型的类型参数在运行时都用其
限定类型
来替换
4. 类型擦除带来的问题
4.1 不允许创建参数化类型的数组
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
以上的代码编译是会错误的,因为类型擦除的原因,再来看一个例子:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed stringLists[0] = new ArrayList<String>(); // OK // An ArrayStoreException should be thrown, but the runtime can't detect it. stringLists[1] = new ArrayList<Integer>();
第二个赋值本来是要抛出异常的,但是实际上却没有,假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayList<string>()和new ArrayList<integer>()的区别,所以编译器不允许创建参数化类型的数组。但是可以声明参数化数组但是不能实例化。</integer></string>
利用集合解决:
ArrayList<ArrayList<string>>;
强制类型转换解决:
List[] ga = (List<integer>[])new ArrayList[10];</integer></string>
4.2 不允许实例化类型变量
public static <E> void append(List<E> list) { E elem = new E(); // compile-time error list.add(elem); }
泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,上述的E被擦除成Object,故实例化类型变量在编译时会出错。
实例化类型变量有几种方法可以解决:
反射:
public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.getDeclaredConstructor().newInstance(); // OK list.add(elem); }
构造器表达式:
public class Pair<T> { public T first; public Pair(T first) { this.first = first; } public T getFirst() { return first; } public void setFirst(T t) { first = t; } public static <T> Pair<T> makePairInstance(Supplier<T> constr) { return new Pair<>(constr.get()); } public static void main(String[] args) { Pair<String> pair = Pair.makePairInstance(String::new); } }
4.3 不能创建泛型数组
4.4 擦除后的冲突
类型擦除会导致在多态场景中,多态被破坏的可能,而编译器会引入桥方法解决这个问题。
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException异常,提示String无法转换成Integer:
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning n.setData("Hello"); // Causes a ClassCastException to be thrown.
如果按照认为的生成的代码,运行到setData应该不会报错,因为多态MyNode只能调用父类Node的setData(Object data)方法,Object类可以接收String类型,看似没有问题,但却抛出ClassCastException。
实际上,编译器引入桥方法处理,其真实执行过程:
class MyNode extends Node { // Bridge method generated by the compiler public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } // ... }
可以看到,setData((Integer) data)是无法将String无法转换成Integer,所以上面第2行编译器提示unchecked warning的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。
4.5 运行时类型查询只适用于原始类型
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // compile-time error // ... } }
所有的类型查询只适用于原始类型,不适用于泛型,我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息。
但是可以通过界定符来进行解决:
public static void rtti(List<?> list) { if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type // ... } }
4.6 不允许基本类型作为类型参数
由于擦除的原因,擦除之后只有Object类,而Object无法存储基本类型值。
4.7 泛型类的静态类型变量无效
public class Singleton<T> { private static T singleInstance; // compile error public static T getSingleInstance() { // compile error if (singleInstance == null) return singleInstance; } }
由于静态变量在对象之间共享,因此编译器无法确定要使用的类型,故编译器禁止使用带有类型变量的静态域和方法。
4.8 不能捕获和抛出泛型类的实例
public static <T extends Throwable> void doWork(Class<T> t) { try { ... } catch (T e) { // error ... } }
不能捕获和抛出类型变量,也不能泛型类扩展Throwable。
但是在异常规范的中使用类型变量还是合法的。
5. 参考资料
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2007.
- Cay S. Horstmann.JAVA核心技术(卷1)[M]. 机械工业出版社, 2008.