泛型详解
1. 为什么使用泛型(Why Use Generics?)
- 更强的编译时类型检查
Java编译器对泛型代码应用强类型检查,如果代码违反了类型安全,将会提示错误。解决编译时错误比运行时错误更容易,后者更难发现。
- 消除类型转换
如下代码未使用泛型,需要类型转换:
1 List list = new ArrayList(); 2 list.add("hello"); 3 String s = (String) list.get(0);
当用泛型重写后,不再需要类型转换
1 List<String> list = new ArrayList<String>(); 2 list.add("hello"); 3 String s = list.get(0); // no cast
- 开发者可实现泛型机制
通过使用泛型,开发者可以使用泛型机制,定制化不同类型的集合,同时也是类型安全和更容易阅读。
1 public class Test { 2 public static void main(String[] args) { 3 List<String> list = new ArrayList<>(); 4 String val = "str"; 5 list.add(val); 6 String str = list.get(0); 7 } 8 }
反编译Test.class文件可以看到,返回对象增加了类型cast
1 public class Test { 2 public static void main(String[] args) { 3 List<String> list = new ArrayList<String>(); 4 String val = "str"; 5 list.add(val); 6 String str = (String)list.get(0); 7 } 8 }
2. 泛型(Generic Types)
2.1 简单类(A Simple Box Class)
1 public class Box { 2 private Object object; 3 4 public void set(Object object) { 5 this.object = object; 6 } 7 8 public Object get() { 9 return object; 10 } 11 }
2.2 泛型类(A Generic Version of the Box Class)
泛型类的定义格式,T1,T2...可以是具体类型,也是是参数化类型
1 class name<T1, T2, ..., Tn> { /* ... */ }
例如:
1 /** 2 * Generic version of the Box class. 3 * @param <T> the type of the value being boxed 4 */ 5 public class Box<T> { 6 // T stands for "Type" 7 private T t; 8 9 public void set(T t) { this.t = t; } 10 public T get() { return t; } 11 }
类型参数命名管理(Type Parameter Naming Conventions)
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
参数化的类型(Parameterized Types)
1 OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
2.3 原始类型(Raw Types)
参数化类型
1 Box<Integer> intBox = new Box<>();
原始类型
1 Box rawBox = new Box();
允许将参数化类型指向原始类型
1 Box<String> stringBox = new Box<>(); 2 Box rawBox = stringBox; // OK
将原始类型指向参数化类型,会引起警告
1 Box rawBox = new Box(); // rawBox is a raw type of Box<T> 2 Box<Integer> intBox = rawBox; // warning: unchecked conversion
原始类型调用泛型方法,也会引起警告
1 Box<String> stringBox = new Box<>(); 2 Box rawBox = stringBox; 3 rawBox.set(8); // warning: unchecked invocation to set(T)
3. 泛型方法
可以是静态方法和非静态方法,也可以使泛型构造函数
3.1 静态泛型方法:
1 public class Util { 2 public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { 3 return p1.getKey().equals(p2.getKey()) && 4 p1.getValue().equals(p2.getValue()); 5 } 6 }
3.2 泛型构造函数:
1 public class Pair<K, V> { 2 3 private K key; 4 private V value; 5 6 public Pair(K key, V value) { 7 this.key = key; 8 this.value = value; 9 } 10 11 public void setKey(K key) { this.key = key; } 12 public void setValue(V value) { this.value = value; } 13 public K getKey() { return key; } 14 public V getValue() { return value; } 15 }
完整的方法调用如下:
1 Pair<Integer, String> p1 = new Pair<>(1, "apple"); 2 Pair<Integer, String> p2 = new Pair<>(2, "pear"); 3 boolean same = Util.<Integer, String>compare(p1, p2);
同样支持类型推断,可简写如下
1 boolean same = Util.compare(p1, p2);
Java API 中典型的泛型方法有
1 public static <T> List<T> asList(T... a) { 2 return new ArrayList<>(a); 3 }
1 List<Integer> li = Arrays.asList(1, 2, 3);
泛型的使用限制
- 不能使用泛型的形参创建对象
实例化可传入Class<T>类型
1 static <T> void testGeneric(Class<T> clazz) throws Exception { 2 T t = clazz.newInstance(); 3 }
- 不能在静态环境中使用泛型类的类型参数
- 不能初始化一个泛型数组,但是可以声明泛型数组
4. 有界类型参数(Bounded Type Parameters)
extends关键字限定上界
4.1 有界方法
1 public class Box<T> { 2 3 private T t; 4 5 public void set(T t) { 6 this.t = t; 7 } 8 9 public T get() { 10 return t; 11 } 12 13 public <U extends Number> void inspect(U u){ 14 System.out.println("T: " + t.getClass().getName()); 15 System.out.println("U: " + u.getClass().getName()); 16 } 17 18 public static void main(String[] args) { 19 Box<Integer> integerBox = new Box<Integer>(); 20 integerBox.set(new Integer(10)); 21 integerBox.inspect("some text"); // error: this is still String! 22 } 23 }
4.2 有界类
1 public class NaturalNumber<T extends Integer> { 2 3 private T n; 4 5 public NaturalNumber(T n) { this.n = n; } 6 7 public boolean isEven() { 8 return n.intValue() % 2 == 0; 9 } 10 11 // ... 12 }
4.3 多个限定参数
限定方式如下:
<T extends B1 & B2 & B3>
限定类型参数中,有类,需要将类放在第一位置,否则编译错误
1 Class A { /* ... */ } 2 interface B { /* ... */ } 3 interface C { /* ... */ } 4 5 class D <T extends A & B & C> { /* ... */ }
4.4 泛型方法和有界类型参数(Generic Methods and Bounded Type Parameters)
有界类型参数是实现泛型机制的关键。如下方法,对数组T[]中的数字元素计数,其中数字元素需要大于指定元素elem。
1 public static <T> int countGreaterThan(T[] anArray, T elem) { 2 int count = 0; 3 for (T e : anArray) 4 if (e > elem) // compiler error 5 ++count; 6 return count; 7 }
因为大于操作符(>)只能应用于原始类型如,short, int, double, long, float, byte, char。不能直接用于对象的比较。为此需要用接口Comparable<T>,来限定类型参数。
1 public interface Comparable<T> { 2 public int compareTo(T o); 3 }
最终代码如下:
1 public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { 2 int count = 0; 3 for (T e : anArray) 4 if (e.compareTo(elem) > 0) 5 ++count; 6 return count; 7 }
5. 泛型、继承和子类型(Generics, Inheritance, and Subtypes)
如果类型兼容,可以将一个类型的对象指向另一个类型的对象。例如,Object是Integer的父类,可以将Integer指向Object。
1 Object someObject = new Object(); 2 Integer someInteger = new Integer(10); 3 someObject = someInteger; // OK
对于泛型也一样。可以执行泛型类型调用,传递Number给类型参数,后续兼容Number类型的都被允许调用。
1 Box<Number> box = new Box<Number>(); 2 box.add(new Integer(10)); // OK 3 box.add(new Double(10.1)); // OK
泛型类和子类型化(Generic Classes and Subtyping)
可以继承一个泛型类或者实现一个泛型接口。一个类或接口的类型参数和另外一个的关系,通过extends和implems语句来实现。
下面使用Collections类为例。 ArrayList<E> implements List<E>, and List<E> extends Collection<E>. 因此 ArrayList<String> 是 List<String>的子类型, 也是 Collection<String>的子类型。只要不改变类型参数,类型指定了子类关系。
Collection的层级关系样例
假定,我们要定义自己的list接口,PayloadList,加入了另外一个泛型参数P,声明如下:
1 interface PayloadList<E,P> extends List<E> { 2 void setPayload(int index, P val); 3 ... 4 }
如下的PayloadList参数化实例都是List<String>的子类型
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>
PayloadList 层级样例
6. 类型推断
类型推断和泛型方法
1 public class BoxDemo { 2 3 public static <U> void addBox(U u, java.util.List<Box<U>> boxes) { 4 Box<U> box = new Box<>(); 5 box.set(u); 6 boxes.add(box); 7 } 8 9 public static <U> void outputBoxes(java.util.List<Box<U>> boxes) { 10 int counter = 0; 11 for (Box<U> box: boxes) { 12 U boxContents = box.get(); 13 System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]"); 14 counter++; 15 } 16 } 17 18 public static void main(String[] args) { 19 java.util.ArrayList<Box<Integer>> listOfIntegerBoxes = new java.util.ArrayList<>(); 20 BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); 21 BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes); 22 BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes); 23 BoxDemo.outputBoxes(listOfIntegerBoxes); 24 } 25 }
输出如下:
Box #0 contains [10] Box #1 contains [20] Box #2 contains [30]
7. 通配符(Wildcards)
泛型代码中,问号(?)称为通配符,代表未知类型。通配符用于以下场景:作为参数、字段和局部变量的类型;有时也可作为返回类型。通配符不能作为调用泛型方法、实例化泛型类和子类型的类型参数。
上界通配符
上界通配符可以缩小对变量的限制
1 public static void process(List<? extends Foo> list) { 2 for (Foo elem : list) { 3 // ... 4 } 5 }
1 public static double sumOfList(List<? extends Number> list) { 2 double s = 0.0; 3 for (Number n : list) 4 s += n.doubleValue(); 5 return s; 6 }
Integer可以调用上述方法
1 List<Integer> li = Arrays.asList(1, 2, 3); 2 System.out.println("sum = " + sumOfList(li));
Double可以调用上述方法
1 List<Double> ld = Arrays.asList(1.2, 2.3, 3.5); 2 System.out.println("sum = " + sumOfList(ld));
无界通配符
问号(?)用来表示无界通配类型,如List<?>,叫做未知类型列表。主要使用场景有两个:
- 如果您正在编写一个可以使用Object类中提供的功能来实现的方法。
- 当代码使用不依赖于类型参数的泛型类中的方法时。例如,List.size() 或者 List.clear()。事实上,Class<?>经常使用,是因为Class<T>中的大多数方法都不依赖于T。
考虑如下方法
1 public static void printList(List<Object> list) { 2 for (Object elem : list) 3 System.out.println(elem + " "); 4 System.out.println(); 5 }
上述方法不能接收List<Integer>, List<String>, List<Double>作为参数,因为他们不是List<Object>的子类型。泛型方法可写成如下形式:
1 public static void printList(List<?> list) { 2 for (Object elem: list) 3 System.out.print(elem + " "); 4 System.out.println(); 5 }
可以进行如下调用
1 List<Integer> li = Arrays.asList(1, 2, 3); 2 List<String> ls = Arrays.asList("one", "two", "three"); 3 printList(li); 4 printList(ls);
List<Object> 和 List<?>的区别
可以将Object或者他的子类插入List<Object>。但是不能将null插入List<?>。
思考:
当实际类型参数为?。它代表某种未知的类型。我们传递添加的任何参数都必须是这种未知类型的子类型。因为我们不知道那是什么类型,所以我们不能传递任何东西。唯一的例外是null,它是每种类型的成员。
思考2:
1 List <String> l1 = new ArrayList<String>(); 2 List<Integer> l2 = new ArrayList<Integer>(); 3 System.out.println(l1.getClass() == l2.getClass());
打印:true
下界通配符
1 public static void addNumbers(List<? super Integer> list) { 2 for (int i = 1; i <= 10; i++) { 3 list.add(i); 4 } 5 }
通配符和子类型
有如下普通类
1 class A { /* ... */ } 2 class B extends A { /* ... */ }
可以实例化如下:
1 B b = new B(); 2 A a = b;
如下代码编译报错
1 List<B> lb = new ArrayList<>(); 2 List<A> la = lb; // compile-time error
公共父类为List<?>
尽管Integer是Number的子类,List<Integer>却不是List<Number>的子类。事实上,它们没有任何关系。List<Integer>和List<Number>的公共父类为List<?>。
1 List<? extends Integer> intList = new ArrayList<>(); 2 List<? extends Number> numList = intList; // OK. List<? extends Integer>是List<? extends Number>的子类型
因为Integer是Number的子类,numList和intList存在一定的关系
几个泛型List声明的层次结构。
8. 类型擦除
泛型被引入java语言,以便在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,java编译器将类型擦除应用于:
- 如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数。因此,生成的字节码只包含普通的类、接口和方法。
- 如有必要,插入类型转换以保持类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
8.1 泛型类型的擦除(Erasure of Generic Types)
在类型擦除过程中,Java编译器擦除所有类型参数,如果类型参数是有界的,则用第一个边界替换每个类型参数,如果类型参数是无界的,则用Object替换每个类型参数。
考虑以下表示单链接列表中节点的泛型类:
1 public class Node<T> { 2 3 private T data; 4 private Node<T> next; 5 6 public Node(T data, Node<T> next) { 7 this.data = data; 8 this.next = next; 9 } 10 11 public T getData() { return data; } 12 // ... 13 }
因为类型参数T是无界的,Java编译器用Object替换它:
1 public class Node { 2 3 private Object data; 4 private Node next; 5 6 public Node(Object data, Node next) { 7 this.data = data; 8 this.next = next; 9 } 10 11 public Object getData() { return data; } 12 // ... 13 }
在以下示例中,泛型类Node使用有界类型参数:
1 public class Node<T extends Comparable<T>> { 2 3 private T data; 4 private Node<T> next; 5 6 public Node(T data, Node<T> next) { 7 this.data = data; 8 this.next = next; 9 } 10 11 public T getData() { return data; } 12 // ... 13 }
Java编译器用第一个绑定类Comparable替换绑定类型参数T,类似于:
1 public class Node { 2 3 private Comparable data; 4 private Node next; 5 6 public Node(Comparable data, Node next) { 7 this.data = data; 8 this.next = next; 9 } 10 11 public Comparable getData() { return data; } 12 // ... 13 }
8.2 泛型方法的擦除(Erasure of Generic Methods)
Java编译器还会擦除泛型方法参数中的类型参数。考虑以下泛型方法:
1 // Counts the number of occurrences of elem in anArray. 2 // 3 public static <T> int count(T[] anArray, T elem) { 4 int cnt = 0; 5 for (T e : anArray) 6 if (e.equals(elem)) 7 ++cnt; 8 return cnt; 9 }
因为T是无界的,Java编译器用Object替换它:
1 public static int count(Object[] anArray, Object elem) { 2 int cnt = 0; 3 for (Object e : anArray) 4 if (e.equals(elem)) 5 ++cnt; 6 return cnt; 7 }
假设定义了以下类:
1 class Shape { /* ... */ } 2 class Circle extends Shape { /* ... */ } 3 class Rectangle extends Shape { /* ... */ }
您可以编写一个泛型方法来绘制不同的形状:
1 public static <T extends Shape> void draw(T shape) { /* ... */ }
Java编译器用Shape替换T:
1 public static void draw(Shape shape) { /* ... */ }
8.3 类型擦除和桥接方法的影响(Effects of Type Erasure and Bridge Methods)
有时类型擦除会导致您可能没有预料到的情况。以下示例显示了这是如何发生的。这个例子(在Bridge Methods中描述)展示了编译器有时如何创建一个合成方法,称为bridge方法,作为类型擦除过程的一部分。
给定以下两个类:
1 public class Node<T> { 2 3 public T data; 4 5 public Node(T data) { this.data = data; } 6 7 public void setData(T data) { 8 System.out.println("Node.setData"); 9 this.data = data; 10 } 11 } 12 13 public class MyNode extends Node<Integer> { 14 public MyNode(Integer data) { super(data); } 15 16 public void setData(Integer data) { 17 System.out.println("MyNode.setData"); 18 super.setData(data); 19 } 20 }
考虑以下代码:
1 MyNode mn = new MyNode(5); 2 Node n = mn; // A raw type - 编译抛出未检查警告 3 n.setData("Hello"); 4 Integer x = mn.data; // 抛出异常ClassCastException
类型擦除后,该代码变为:
1 MyNode mn = new MyNode(5); 2 Node n = (MyNode)mn; // A raw type - 编译抛出为检查警告 3 n.setData("Hello"); 4 Integer x = (String)mn.data; // 抛出异常ClassCastException
下面是代码执行时发生的情况:
- n.setData("Hello");导致方法setdata(object)在MyNode类的对象上执行。(MyNode从Node继承了setData(Object)。)
- 在setData(Object)的主体中,由n引用的对象的字段data被分配给一个String。
- 通过mn引用的同一对象的字段data可以被访问,并且预期是整数(因为mn是MyNode类型的,是Node<Integer>)。
- 试图将String分配给Integer会导致Java编译器在分配时插入的强制转换产生ClassCastException。
桥接方法(Bridge Methods)
当编译继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。您通常不需要担心桥接方法,但是如果堆栈跟踪中出现桥接方法,您可能会感到困惑。
类型擦除后,Node和MyNode类变为:
1 public class Node { 2 3 public Object data; 4 5 public Node(Object data) { this.data = data; } 6 7 public void setData(Object data) { 8 System.out.println("Node.setData"); 9 this.data = data; 10 } 11 } 12 13 public class MyNode extends Node { 14 15 public MyNode(Integer data) { super(data); } 16 17 public void setData(Integer data) { 18 System.out.println("MyNode.setData"); 19 super.setData(data); 20 } 21 }
类型擦除后,方法签名不匹配。Node的方法变成setData(Object),MyNode方法变成setData(Integer)。因此,MyNode setData方法不会重写Node setData方法。
为了解决这个问题并在类型擦除后保持泛型类型的多态性,Java编译器生成一个桥接方法来确保子类型按预期工作。对于MyNode类,编译器为setdata生成以下桥接方法:
1 class MyNode extends Node { 2 3 // Bridge method generated by the compiler 4 // 5 public void setData(Object data) { 6 setData((Integer) data); 7 } 8 9 public void setData(Integer data) { 10 System.out.println("MyNode.setData"); 11 super.setData(data); 12 } 13 14 // ... 15 }
如您所见,桥接方法在类型擦除后与类Node的setData方法具有相同的方法签名,委托给原始的setData方法。
9. 泛型的高级用法
9.1 泛型类父类为子类定义公共方法
父类:
1 public class Parent<Sub extends Parent<Sub>> { 2 3 public Sub get() { 4 return (Sub) this; 5 } 6 }
子类:
1 public class Children extends Parent<Children> { 2 3 }
测试:
1 public class Test { 2 public static void main(String[] args) { 3 Children children = new Children(); 4 String name = children.get().getClass().getName(); // Children 5 } 6 }
参考代码如下:
MasterNodeRequest的子类
9.2 参数化类型作为泛型,编译检查强类型校验
Action:
红圈中的Request和Response为有界参数化类型。
GenericAction<Request, Response>中的泛型Request和Response取自有界参数化类型。
ActionRequestBuilder:
同理,红圈中的Request和Response为有界参数化类型,参数化类型RequestBuilder为限定为自身的子类。
GenericAction:
GenericAction的参数化类型为ActionRequest和ActionResponse的子类。Action继承GenericAction时,泛型符合限定条件。
ClusterAllocationExplainAction中的ClusterAllocationExplainRequest等均为对应的子类。