泛型
泛型有关的术语中英对照
术语 | 中文 | 例子 |
---|---|---|
Generic Types | 泛型 | Box<T> |
Parameterized Type | 参数化泛型 | Box<Integer> |
Type Parameter | 类型参数 | T |
Raw Types | 原生类型 | Box |
Generic Methods | 泛型方法 | public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) |
Bounded Type Parameters | 有界类型参数 | <E extends Number> |
Multiple Bounds | 多上界 | class D <T extends A & B & C> { /* ... */ } |
Recursive type bound | 递归类型上界 | <T extends Comparable<T>> |
Type Inference | 类型推断 | |
Target Types | 目标类型 | |
Wildcards | 通配符 | ? |
Upper Bounded Wildcards | 上界通配符 | <? extends A> |
Unbounded Wildcards | 无界通配符 | ? |
Lower Bounded Wildcards | 下界通配符 | <? super A> |
Wildcard Capture | 通配符捕获 | |
Type Erasure | 泛型擦除 |
泛型的好处
- 编译期类型检查,提前发现问题
-
提供代码的灵活性和复用性
image.png
Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types, type variables and primitive types.
Type
是java语言中所有类型的公共父接口, 这些类型包括raw types
,parameterized types
,array types
,type variables
和primitive types
Generic Types
A generic type is a generic class or interface that is parameterized over types
Generic Types(泛型)是类型被参数化的类或者接口,比如:
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
Type Parameter 命名规范
- 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
泛型实例化
Box<Integer> integerBox = new Box<>();
Raw Types
-
Raw Type
是去掉所有泛型参数的泛型,比如Box
是Box<T>
的raw type
-
raw type
是为了向后兼容老的代码,可以将parameterized type
赋值给raw type
:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
- 将
raw type
赋值给parameterized type
,会出现warning
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
- 使用
raw type
调用带有泛型的方法也会出现warning
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
应该尽量避免使用raw type
Generic Methods
泛型方法的泛型参数只在方法内有效,静态泛型方法、非静态泛型方法以及泛型构造方法都是允许的
如下面的例子所示,Util
类有个compare
的泛型方法用于比较两个Pair
对象是否相等
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());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
显式调用泛型方法:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
类型推断调用泛型方法
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
Bounded Type Parameters
使用extends
关键字声明类型参数的上界,extends
是一个广义的概念,可以是类继承,也可以是方法实现。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
Multiple Bounds
<T extends B1 & B2 & B3>
多上界类型变量(type variable)是所有声明的上界的子类,如果其中有一个类和多个接口,那么类必须写在最前面。
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
否则会出现编译错误
class D <T extends B & A & C> { /* ... */ } // compile-time error
泛型方法和有界类型参数
有界类型参数是实现泛型算法的关键,考虑如下方法计算数组T[]
中大于elem
的元素个数
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
上述实现虽然很直观,但是无法通过编译,这是因为>
只能用于比较诸如 short, int, double, long, float, byte, 和 char这些原子数据类型而无法比较对象
为了解决这个问题可以使用Comparable<T>
接口作为上界
public interface Comparable<T> {
public int compareTo(T o);
}
最终结果如下
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;
}
泛型继承和子类
面向对象中的继承是一种IS A
的关系,子类向父类转换是合法的:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在泛型中,也同样适用:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
考虑如下方法
public void boxTest(Box<Number> n) { /* ... */ }
这个参数接受什么类型呢,它可以接受Box<Integer>
或Box<Double>
吗,答案是否定的,因为它们不是Box<Number>
的子类
️无论类A
和类B
有什么关系,MyClass<A>
和MyClass<B>
都没有任何关系,它们的公共父类是Object
通过extends 和implements来维持继承关系在泛型中依然有效,以ArrayList
为例,
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
public interface List<E> extends Collection<E>
ArrayList<E>
实现了List<E>
,List<E>
继承了 Collection<E>
. 因此 ArrayList<String>
是List<String>
的子类,List<String>
是Collection<String>
的子类. 因此只要类型参数E保持一致(在本例中是String),继承关系就是有效的:
现在假设我们定义了自己的接口,并且添加了额外的泛型参数 P
:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
那么泛型类继承关系如下:
Type Inference
java 编译器通过方法调用和相应声明执行类型推断
如下面例子所示,第二个参数会被推断为Serializable
:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
类型推断和泛型方法
public class BoxDemo {
public static <U> void addBox(U u,
java.util.List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
new java.util.ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);//类型推断
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
输出结果如下:
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
类型推断和泛型类的实例化
泛型类的实例化使用<>
来启用类型推断
Map<String, List<String>> myMap = new HashMap<>();
泛型和非泛型类的泛型构造器类型推断
️无论是泛型类还是非泛型类都可以拥有泛型方法,对于泛型类,构造器的泛型参数可以和类声明的泛型参数不同,比如:
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
考虑MyClass
的实例化:
MyClass<Integer> myObject = new MyClass<>("");
在这个例子中类的泛型参数X
为Integer
,构造器的类型参数T
被推断为String
It is important to note that the inference algorithm uses only invocation arguments, target types, and possibly an obvious expected return type to infer types. The inference algorithm does not use results from later in the program.
类型推断并不能在调用过程中传递
Target Types
java编译器使用目标类型来推断泛型方法调用的类型参数,考虑Collections.emptyList
:
static <T> List<T> emptyList();
考虑如下赋值语句,泛型参数T
被推断为String
:
List<String> listOne = Collections.emptyList();
//List<String> listOne = Collections.<String>emptyList();
考虑如下场景:
void processStringList(List<String> stringList) {
// process stringList
}
在java SE7,如下语句无法编译通过:
processStringList(Collections.emptyList());
并且会给出如下的错误
List<Object> cannot be converted to List<String>
因此在java SE7中必须显示指定泛型参数的类型
processStringList(Collections.<String>emptyList());
Java SE 8强化了类型推断, target type也可以应用于 method arguments上,因此不用显示的指定
processStringList(Collections.emptyList());
Wildcards
在泛型中?
是通配符,表示未知类型。通配符可以被用在很多地方比如:
- 类型参数
- 字段
- 本地变量
- 返回值(不推荐)
通配符不能用于泛型方法调用的类型参数,或者泛型类以及子类的实例化
Upper Bounded Wildcards
如果你想写一个方法能够接受List<Integer>, List<Double>, List<Number>
作为参数,可以使用上界通配符:
List<? extends Number>
相对于List<Numeber>严格接受Number类型元素,List<? extends Number>可以接受Number及其子类作为元素。
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
如下代码打印sum = 6.0
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
如下代码打印 sum = 7.0:
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
Unbounded Wildcards
List<?>
这种形式称之为无界通配符,代表一个未知类型的list,有两种类型比较适合使用无界通配符:
- 方法的功能可以使用
Object
类中提供的方法实现 - 方法不依赖泛型类的类型参数,比如
List.size
,实际上 Class<?> 这种无界通配符经常使用因为一般Class<T>
的大多数方法不依赖于T
.
看下面这个例子:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
printList
本意是打印任何类型list中的元素,但是上述代码却只能打印List<Object>
无法打印 List<Integer>, List<String>, List<Double>
,因为这些不是List<Object>的子类。想要实现这个功能可以使用List<?>
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
因为对应任意的一个具体A类型, List<A>
是 List<?>的子类,所以可以打印任何类型的list
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
️List<Object> 和List<?>
是不同的。List<Object>
可以插入Object
及其子类,而List<?>
只能插入null
Lower Bounded Wildcards
下界通配符写法<? super A>
假设你想写一个方法用于在list中存放一些整数,为了最大化灵活性,希望可以支持任何可以存放Integer的List,比如List<Integer>, List<Number>, List<Object>
:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
通配符和子类
虽然Integer
是Number
的子类,List<Integer>
却不是List<Number>
的子类,事实上这个两个类没有关系,它们的公共父类是List<?>
.
更一般的:
Wildcard Capture and Helper Methods
编译器对通配符的推断称之为 wildcard capture
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}
在java7的环境中使用javac编译会报错:
WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error
增加hleper类修复错误
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
Type Erasure
java编译器在运行代码时会进行泛型擦处:
- 无界泛型使用Object替换,有界泛型使用边界替换
- 为了维持类型安全必要时进行类型强制转换
- Generate bridge methods to preserve polymorphism in extended generic types.
类型参数保证了泛型类在运行时不会参数额外的Class
对象,因此泛型不会增加运行时的负担。如下例所示,不同的泛型化A
类的实例是同一个Class
对象
public class Test {
static class A<T>{
}
public static void main(String[] args) {
System.out.println(new A<Integer>().getClass()==new A<String>().getClass());//true
}
}
Erasure of Generic Types
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; }
// ...
}
java 编译器会将T
替换为Object
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; }
// ...
}
对于如下代码:
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; }
// ...
}
Java编译器会将T
替换为第一个有界类,也就是Comparable
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; }
// ...
}
Erasure of Generic Methods
java编译器同样会对泛型方法上的类型参数进行擦除
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
擦除后的结果如下:
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
考虑如下代码:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
定义如下泛型方法:
public static <T extends Shape> void draw(T shape) { /* ... */ }
java编译器擦除后的结果如下:
public static void draw(Shape shape) { /* ... */ }
Effects of Type Erasure and Bridge Methods
有时候直接泛型擦除会出现问题,这时候编译器会创建Bridge Methods
作为泛型擦除的补充
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);
}
}
考虑如下代码:
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = mn.data; // Causes a ClassCastException to be thrown.
,编译器生成的代码自动添加了一个Bridge method
:
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;
}
}
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);
}
// ...
}
Non-Reifiable Types
java 泛型的一些限制
实战
fastjosn中使用TypeReference处理泛型反序列化
测试一下:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import java.util.Map;
public class TypeReferenceTest {
public static <K, V> Map<K, V> parseToMap(String json,
Class<K> keyType,
Class<V> valueType) {
return JSON.parseObject(json,
new TypeReference<Map<K, V>>(keyType, valueType) {
});
}
public static <K, U, V> Map<K, Map<U, V>> parseToMapMap(String json,
Class<K> keyType,
Class<U> uType,
Class<V> valueType) {
return JSON.parseObject(json,
new TypeReference<Map<K, Map<U, V>>>(keyType, uType, valueType) {
});
}
public static class Model {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Model{" +
"name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
String json = "{1:{name:\"ddd\"},2:{name:\"zzz\"}}";
Map<Integer, Model> map = parseToMap(json, Integer.class, Model.class);
System.out.println(map);//{1=Model{name='ddd'}, 2=Model{name='zzz'}}
//嵌套泛型
Map<Integer, Map<String, String>> mapMap = parseToMapMap(json, Integer.class, String.class, String.class);
System.out.println(mapMap);//{1={name=ddd}, 2={name=zzz}}
}
}