Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然而 Java 中的泛型使用了类型擦除,所以只是伪泛型。
Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型,而且无法直接使用基本值类型作为泛型类型参数。Java编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。
由于运行时会消除泛型的对象实例类型信息等缺陷经常被人诟病,Java及JVM的开发方面也尝试解决这个问题,例如:Java通过在生成字节码时添加类型推导辅助信息,从而可以通过反射接口获得部分泛型信息;通过改进泛型在JVM的实现,使其支持基本值类型泛型和直接获得泛型信息等。
协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
只读数据类型(源)是协变的;只写数据类型(汇/sink)是逆变的。可读可写类型应是“不变”的。
Java 把数组类型处理为协变,在Java中,String[]是Object[]的子类型。
返回值的协变, Java 允许返回值协变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class AnimalShelter {
Animal getAnimalForAdoption() {
...
}
void putAnimal(Animal animal) {
...
}
}
class CatShelter extends AnimalShelter {
Cat getAnimalForAdoption() {
return new Cat();
}
}
|
泛型方法
1
2
3
4
5
6
7
8
9
10
11
|
public class GenericMethod {
public <K,V> void f(K k,V v) {
System.out.println(k.getClass().getSimpleName());
System.out.println(v.getClass().getSimpleName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.f(new Integer(0),new String("generic"));
}
}
|
定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
java 中泛型标记符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
泛型类 与 泛型接口
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
通配符
-
<T>
:泛型标识符,用于泛型定义(类、接口、方法等)时。
-
<?>
:通配符,用于泛型实例化时。
-
向上转型:子类型通过类型转换成为父类型(隐式的)。
-
向下转型:父类型通过类型转换成为子类型(显式的,有风险需谨慎 ClassCastException
)。
-
<? extends T>
上边界通配符 因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据。
-
<? super T>
下边界通配符 因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。而不能获取数据。
-
<?>
无边界通配符 等同于 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。
PECS 原则
“Producer Extends, Consumer Super”
- “Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用
<? extends T>
。
- “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用
<? super T>
。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
|
1
2
3
4
|
@Override
public boolean removeIf(Predicate<? super E> filter) {
return removeIf(filter, 0, size);
}
|
1
2
3
4
5
6
7
|
// 两者结合起来一起用
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));
}
}
|
类型擦除
简单的说就是,类型参数只存在于编译期,在运行时,Java 的虚拟机 ( JVM ) 并不知道泛型的存在。
1
2
3
4
5
|
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
|
上面的代码输出是 true。这说明在 JVM 看来它们是同一个类。而在 C++、C# 这些支持真泛型的语言中,它们就是不同的类。
之所以取出来自动就是传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。如果参数类型有边界那么就擦除到它的第一个边界
类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。
1
2
3
4
5
6
7
8
9
|
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
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
}
|
通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。
1
2
3
4
5
6
7
8
9
10
|
interface FactoryI<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
// ...
}
|
另一种解决的方法是利用模板设计模式:
1
2
3
4
5
|
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() { element = create(); }
abstract T create();
}
|
泛型与数组
数组与容器差异:
- 数组创建后大小便固定,但效率更高
- 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查
- 数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了
如果要创建一个泛型数组,应该是这样: Generic<Integer> ga = new Generic<Integer>[]
。不过行代码会报错,也就是说不能直接创建泛型数组。
那么如果要使用泛型数组怎么办?
- 一种方案是使用 ArrayList。
- 不能直接创建,但可以定义泛型数组的引用。成功创建泛型数组的唯一方式是创建一个类型擦除的数组,然后转型
1
2
3
4
5
6
7
8
9
10
11
|
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
gia = (Generic<Integer>[])new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
Generic<Integer> g = gia[0];
}
}
|
元素的类型是泛型类
rep()
调用会报 ClassCastException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[])new Object[sz]; // 创建泛型数组
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Method that exposes the underlying representation:
public T[] rep() { return array; } //返回数组 会报错
public static void main(String[] args) {
GenericArray<Integer> gai =
new GenericArray<Integer>(10);
// This causes a ClassCastException:
//! Integer[] ia = gai.rep();
// This is OK:
Object[] oa = gai.rep();
}
}
|
推荐的方案: 使用类型标识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Expose the underlying representation:
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai =
new GenericArrayWithTypeToken<Integer>(
Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
}
|