泛型
- 其实前几章已经零零散散提到过泛型的使用方法了,这一章我们来详细地了解一下泛型是如何使用的。
- 为什么要有泛型?我们知道一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类型。稍微通用一点,我们可以定义某个方法传入类型是一个基类或者接口,那我们就可以传入它的导出类。那如果我现在要求这个方法能传入其他类型的接口该怎么办?要将这个接口与原来的接口再提取一个新接口吗?这样未免也太麻烦了。
- 泛型是Java SE5的重大变化之一。泛型实现了参数化类型(我们知道参数值可以变化,此处指类型也可以像参数值一样变化)的概念。不过Java中的泛型不如其他语言中(如C++)的泛型来的纯粹,所以Java中的泛型的用处可能没你想的那么大,但在多数情况下的确可以使你的代码更加优雅(还有什么比代码优雅更重要的呢?哈哈哈)。
简单泛型
- 有很多原因促成了泛型的出现,而最引人注目的原因,就是为了创建容器类。我们先来看看一个只能持有单个对象的类,这个类可以明确指定其持有的对象类型:
class A {}
public class Test {
private A a ;
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
}
- 不过这个类的重用性就不怎么样了,它无法持有其他类型的任何对象。那该怎么改呢?在Java SE5之前,我们可以让这个类直接持有Object类型的对象:
class A {}
class B {}
public class Test {
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static void main(String args[]) {
Test t = new Test();
t.setData(new A());
System.out.println(t.getData());
t.setData(new B());
System.out.println(t.getData());
}
}
- 恩,勉强可以接受。但是通常而言,我们只会使用容器来存储一种类型的对象。我们并不希望同样一个集合对象,可以同时放A或B。或者说我们希望在创建这个集合时就要确定它要存储的对象类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。看下面的例子:
class A {}
class B {}
public class Test<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String args[]) {
Test<A> t = new Test<A>();
t.setData(new A());
System.out.println(t.getData().getClass().getSimpleName());
//t.setData(new B());//不被允许! 指定泛型能在编译时检查类型是否对应
//如果我们需要一个存放B的容器,我们就得重新创建一个新容器
Test<B> t2 = new Test<B>();
t2.setData(new B());
System.out.println(t2.getData().getClass().getSimpleName());
//下面四种写法在较新的JDK中都是一个意思
Test t3 = new Test();
Test t4 = new Test<>();
Test t5 = new Test<Object>();
Test<Object> t6 = new Test<Object>();
}
}
- 这就是Java泛型的核心概念,告诉编译器想使用什么类型,然后编译器帮你处理一切细节。 接下来是几个应用泛型的例子,用来熟悉一下泛型的使用方式。
一个元组类库
- 元组(tuple)是指将一组对象打包存储于一个单一对象的一个容器。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象(也可以成为数据传送对象或信使)。通常元组可以具有任意长度,同时其对象可以是任意类型。下面是一个二维元组的样例:
public class Test<T> {
public static void main(String args[]) {
TwoTuple<String, Integer> tt = new TwoTuple<String, Integer>("hello", 1);
TwoTuple<Integer, String> tt2 = new TwoTuple<Integer, String>(1, "hello");
System.out.println(tt.a);
System.out.println(tt2.a);
}
}
/*class A {
void test() {};
}*/
/* The type parameter A is hiding the type A 注意这段警告
* 此处的A B是参数化类名,不是明确的class A B
* 如果你定义了A类,这里的A还是指参数名,和A类没有半毛钱关系
* 如果你非要A和A类有关系,可以写成<A extends test.A, B>
*/
class TwoTuple<A, B> {
/*A a;
* void test() {a.test();} //编译错误 这个可以验证此A非彼A
*/
public final A a;//final保证不会被修改
public final B b;
public TwoTuple(A a, B b) {
this.a = a;
this.b = b;
}
}
- 如果需要多维的元组,我们再重新定义或者继承这个二维元组进行扩展即可。
一个堆栈类
- 前面我们提到java.util.Stack(已弃用)和LinkedList都可以作为堆栈的实现,下面我们将自己定义一个堆栈类,来熟悉一下泛型的使用。
public class Test {
public static void main(String args[]) {
LinkedStack<String> s = new LinkedStack<String>();
System.out.println(s.pop());
s.push("123");
s.push("456");
s.push("789");
System.out.println(s.pop());
System.out.println(s.pop());
System.out.println(s.pop());
System.out.println(s.pop());
}
}
class LinkedStack<T> {
//注意嵌套类要重新定义泛型, 一是减少耦合度, 也许以后会提取出这个内部类
//二是因为该类定义为static, 不能直接使用T
//如果要使用T 可以定义成非嵌套类,同样也可以正常使用,
//因为我们不需要在内部类中访问外围类的非静态属性或方法,所以没这个必要。
private static class Node<V> {
V item;
Node<V> next;
Node() {}
Node(V item, Node<V> next) {
this.item = item;
this.next = next;
}
boolean end() {
return item == null && next == null;
}
}
private Node<T> top = new Node<T>();//头结点,内容为空
public void push(T item) {
top = new Node<T>(item, top);
}
public T pop() {
T result = top.item;
if (!top.end()) {
top = top.next;
}
return result;
}
}
RandomList
- 做为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其上的select()方法时,它就可以随机地选取一个元素。如果我们希望以此构建一个可以应用于各种类型的对象的工具,就得使用泛型。考虑到查询速度,我们应该使用数组作为底层实现,所以我们这里用ArrayList作为一个代理容器。
import java.util.ArrayList;
import java.util.Random;
public class Test {
public static void main(String args[]) {
RandomList<String> rl = new RandomList<String>();
rl.addArray("hello world haha hehe memeda".split(" "));
for (int i = 0; i < 10; i++) {
System.out.println(rl.select());
}
}
}
class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random random = new Random(47);
public void add(T item) {
storage.add(item);
}
public void addArray(T[] items) {
for (T item : items) {
add(item);
}
}
public T select() {
return storage.get(random.nextInt(storage.size()));
}
}
泛型接口
- 接口也可以设置泛型,和类设置泛型没有什么区别,来看下面一个生成器(generator)的一个接口例子。
import java.util.Iterator;
import java.util.Random;
interface Generator<T> {
T next();
}
class Coffee {
private static int count = 1;
private final int id = count++;//这样每次创建一个实例都会获得唯一id
public String toString() {
return this.getClass().getSimpleName() + " " + id;
}
}
class Latte extends Coffee {}
class Mocha extends Coffee {}
class Cappuccino extends Coffee {}
class Americano extends Coffee {}
class Breve extends Coffee {}
class CoffeeGenerator implements
Generator<Coffee>, Iterable<Coffee> {
private int size;
private Class<?>[] types = new Class<?>[] {
Latte.class, Mocha.class, Cappuccino.class,
Americano.class, Breve.class };
private static Random random = new Random(47);
public CoffeeGenerator() {}
public CoffeeGenerator(int size) {
this.size = size;
}
public Iterator<Coffee> iterator() {
//返回一个匿名类
return new Iterator<Coffee>() {
int size = CoffeeGenerator.this.size;
public boolean hasNext() {
return size-- > 0;
}
public Coffee next() {
return CoffeeGenerator.this.next();
}
};
}
public Coffee next() {
try {
return (Coffee) types[random.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Test {
public static void main(String args[]) {
CoffeeGenerator gen = new CoffeeGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee c : new CoffeeGenerator(5)) {
System.out.println(c);
}
}
}
- 代码本身不是很难懂,只是一个应用的例子而已,这里就不做解析了。
泛型方法
- 到目前为止,我们看到的泛型,都是应用到整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
- 如果你能尽量使用泛型方法完成必要的功能,就尽量不要使用泛型类。不过对于一个static方法而言,无法访问泛型类的类型参数。所以如果static方法需要使用泛型能力,就必须使其成为泛型方法。来看一下泛型方法是如何定义的。
import java.util.ArrayList;
import java.util.List;
public class Test {
public static <T> List<T> asList(T... ts) {
List<T> l = new ArrayList<T>(ts.length);
for (T t : ts) {
l.add(t);
}
return l;
}
public static <T> List<T> asList2(Object... ts) {
List<T> l = new ArrayList<T>(ts.length);
for (Object t : ts) {
try {
//类型擦除,这里可以不管
List.class.getMethod("add", Object.class).invoke(l, t);
} catch (Exception e) {
e.printStackTrace();
}
}
return l;
}
public static void main(String args[]) {
List<String> strl = Test.asList(new String[]{});
//相当于 List<String> strl = Test.<String>asList(new String[]{});
//List<Object> strl2 = Test.asList(new String[]{});//报错
List<Object> strl3 = Test.<Object>asList(new String[]{});//显示指定泛型
List<? extends Object> strl4 = Test.asList(new String[]{});//也可以用通配符
//对比strl2
List<Object> strl5 = Test.asList2(new String[]{});//相当于指定为Object
}
}
- 和创建对象时指定泛型一样,调用方法时也可以显式指定泛型,也可以不写。当不主动指定泛型时,如果参数类型存在泛型类型,则会指定其为参数类型。如果参数类型中没有泛型类型,则会被指定为Object。书上还有其他例子,我觉得没必要都放在这,先简单了解一下泛型方法即可。以后的例子我会尽量增加泛型方法的使用。
- 另外匿名内部类也可以带上泛型,泛型还可以轻松构建复杂的集合。这里都略过了。
擦除的神秘之处
- 我们先来看看下面的代码:
System.out.println(new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass());//true
- 结果打印true。
ArrayList<String>
和ArrayList<Integer>
很容易被认为是两种不同的类型。如果尝试将一个Integer对象放入ArrayList<String>
将会失败。但是上面的程序却认为它们是相同的类型。实际上ArrayList<String>
和ArrayList<Integer>
在运行时是相同的类型,这两种形式都被擦除成他们的原生类型ArrayList。 - 在学习类型信息的时候,还有一个方法我没有提到,就是
Class.getTypeParameters
,他的功能是返回一个TypeVariable的对象数组,表示有泛型声明所声明的类型参数。我们不妨改一下这个代码,看看调用这个方法后会输出什么。
import java.util.ArrayList;
import java.util.Arrays;
class A <T extends ArrayList> {//这种写法叫设置擦除边界,我觉得还不如直接定义T为ArrayList而抛弃泛型
public void test(T t) {
t.add(1);
}
/*public void test(ArrayList t) {
t.add(1);
}*/
}
public class Test {
public static void main(String args[]) throws Exception {
Class a = new ArrayList<String>().getClass();
Class b = new ArrayList<Integer>().getClass();
System.out.println(a == b);
System.out.println(Arrays.toString(a.getTypeParameters()));
System.out.println(Arrays.toString(b.getTypeParameters()));
System.out.println(Arrays.toString(A.class.getTypeParameters()));
}
}
---------------------
true
[E]
[E]
[T]
- 尼玛,竟然输出了定义的泛型名E,我要这类型有何用!
因此,残酷的现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
- Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象(除非使用擦除边界,你就可以使用边界类型的一些方法(默认边界是Object))。
- 当然存在即合理,也不是说擦除边界就一无是处,来看下面的代码:
class A <T extends ArrayList> {
private T obj;
public void test(T t) {
t.add(1);
}
public void set(T t) {
obj = t;
}
//返回类型为泛型时体现了意义,其结果会得到泛型而不是ArrayList,就不用再自行强转了
public T get() {return obj;}
}
擦除的问题
- 擦除的代价是显著的。泛型不能用于显式的引用运行时类型的操作之中。例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。例如:
class Test<T> {
T var;
}
Test<Integer> f = new Test<Integer>();
- 当你创建实例时,Test类的代码应该知道现在工作于Integer之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object”。
边界处的动作
- 为了更加深刻理解擦除,我们有以下两个例子:
class A {
private Object obj;
public Object get() {
return obj;
}
public void set(Object obj) {
this.obj = obj;
}
public static void Test() {
A a = new A();
a.set("123");
String str = (String) a.get();
}
}
class B <T> {
private T obj;
public T get() {
return obj;
}
public void set(T obj) {
this.obj = obj;
}
public static void Test() {
B<String> b = new B<String>();
b.set("123");
String str = b.get();
}
}
- 下面是通过反编译后获得的字节码:
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void Test();
Code:
0: new #3 // class A
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_0
8: aload_0
9: ldc #5 // String 123
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_0
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_1
22: return
}
class B<T> {
B();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void Test();
Code:
0: new #3 // class B
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_0
8: aload_0
9: ldc #5 // String 123
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_0
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_1
22: return
}
- 然后你会惊奇的发现,这不是一模一样吗?注意
18: checkcast
即使使用泛型后不必显式的书写转换代码,编译器也会自动为我们带上这个操作。这里举这个例子是想说泛型中的所有动作都发生在边界(不是擦除边界的那个边界,是指进出类的时间点)处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。
擦除的补偿
- 正如我们看到的,擦除丢失了在泛型代码中,执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:
class A <T> {
public void getNewA(Object arg) {
if (arg instanceof T) {}//error
T var = new T();//error
T[] array = new T[10];//error
T[] array = (T[]) new Object[10];//warning
}
}
- 偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
import java.lang.reflect.Array;
public class Test {
public static void main(String args[]) throws Exception {
A<Arg> a = new A<Arg>(Arg.class);
}
}
class Arg {}
class A <T> {
Class<?> type;
public A (Class<?> type) {
this.type = type;
}
public void getNewA(Object arg) throws InstantiationException, IllegalAccessException {
//if (arg instanceof T) {}//error
if (type.isInstance(arg)) {}
//T var = new T();//error
T var = (T) type.newInstance();//需要无参构造器,最佳解决方案是使用工厂模式
//T[] array = new T[10];//error
T[] array = (T[]) Array.newInstance(type, 10);//然而由于版本原因,jdk中有很多的代码不是这么写的
}
}
- 我们来看一个例子:
public class Test {
public static void main(String args[]) throws Exception {
A<String> a = new A<String>(1);
a.put(0, "123");
System.out.println(a.get(0));
String [] arr = a.rep();//error 注意必须进行赋值操作,或调用函数,否则不会进行类型转换检查
//这也正是某些集合(例如ArrayList)无法将握有的数组引用直接转换成明确数组类型的原因
}
}
class A <T> {
private T[] array;
public A(int size) {
array = (T[]) new Object[size];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {return array;}
}
- 还记得前面说过的泛型边界吗?对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。在A类的内部,array的类型是Object[]而不是的String[]。所以哪怕该数组下面的所有元素都是String,当我们调用rep()时,这种自动转型也依旧会造成ClassCastException。当然对于数组握有的引用将其转换成T是可以的,因为其本身就是T类型。要注意数组本身是一个特殊的类型,数组的每个元素可以是数组声明的类型的任意的子类型。
- 因为无法正确转型,一般集合都是这么定义数组的:
class A <T> {
private Object[] array;
public A(int size) {
array = new Object[size];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return (T) array[index];
}
public T[] rep() {return (T[]) array;}
}
- 这种写法可能会更清晰一点,这将明确得告诉我们array的真实类型是Object[](所以我们要深刻理解到,在泛型类的内部,T的意义就是其擦除边界而不是什么具体类型)。这也是ArrayList的写法,不过其调用rep()同样会造成CCE异常。
- 其实我们要认识到,创建一个数组,其实相当于调用了数组类的构造方法,这个构造方法一般传入大小作为参数。我们稍微修改一下第一个例子,这样调用rep()就不会出现异常了。
import java.lang.reflect.Array;
class A <T> {
private T[] array;
public A(Class<?> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {return array;}
}