这篇文章的目的在于介绍Java泛型,使大家对Java泛型的各个方面有一个最终的,清晰的,准确的理解,同时也为下一篇《重新理解Java反射》打下基础。
简介
泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。
泛型基础
泛型类
我们首先定义一个简单的Box类:
public class Box { private String object; public void set(String object) { this.object = object; } public String get() { return object; } }
这是最常见的做法,这样做的一个坏处是Box里面现在只能装入String类型的元素,今后如果我们需要装入Integer等其他类型的元素,还必须要另外重写一个Box,代码得不到复用,使用泛型可以很好的解决这个问题。
public class Box{ // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
这样我们的Box类便可以得到复用,我们可以将T替换成任何我们想要的类型:
BoxintegerBox = new Box (); Box doubleBox = new Box (); Box stringBox = new Box ();
泛型方法
看完了泛型类,接下来我们来了解一下泛型方法。声明一个泛型方法很简单,只要在返回类型前面加上一个类似
public class Util { public staticboolean compare(Pair p1, Pair p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } public class Pair { 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; } }
我们可以像下面这样去调用泛型方法:
Pairp1 = new Pair<>(1, "apple"); Pair p2 = new Pair<>(2, "pear"); boolean same = Util. compare(p1, p2);
或者在Java1.7/1.8利用type inference,让Java自动推导出相应的类型参数:
Pairp1 = new Pair<>(1, "apple"); Pair p2 = new Pair<>(2, "pear"); boolean same = Util.compare(p1, p2);
边界符
现在我们要实现这样一个功能,查找一个泛型数组中大于某个特定元素的个数,我们可以这样实现:
public staticint 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等原始类型,其他的类并不一定能使用操作符>,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符。
public interface Comparable{ public int compareTo(T o); }
做一个类似于下面这样的声明,这样就等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了compareTo方法。
public static> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) > 0) ++count; return count; }
通配符
在了解通配符之前,我们首先必须要澄清一个概念,还是借用我们上面定义的Box类,假设我们添加一个这样的方法:
public void boxTest(Boxn) { /* ... */ }
那么现在Box
首先我们先定义几个简单的类,下面我们将用到它:
class Fruit {} class Apple extends Fruit {} class Orange extends Fruit {}
下面这个例子中,我们创建了一个泛型类Reader,然后在f1()中当我们尝试Fruit f =
fruitReader.readExact(apples);编译器会报错,因为List
public class GenericReading { static Listapples = Arrays.asList(new Apple()); static List fruit = Arrays.asList(new Fruit()); static class Reader { T readExact(List list) { return list.get(0); } } static void f1() { Reader fruitReader = new Reader (); // Errors: List cannot be applied to List . // Fruit f = fruitReader.readExact(apples); } public static void main(String[] args) { f1(); } }
但是按照我们通常的思维习惯,Apple和Fruit之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:
static class CovariantReader{ T readCovariant(List extends T> list) { return list.get(0); } } static void f2() { CovariantReader fruitReader = new CovariantReader (); Fruit f = fruitReader.readCovariant(fruit); Fruit a = fruitReader.readCovariant(apples); } public static void main(String[] args) { f2(); }
这样就相当与告诉编译器, fruitReader的readCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。
PECS原则
上面我们看到了类似 extends T>的用法,利用它我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?我们来尝试一下:
public class GenericsAndCovariance { public static void main(String[] args) { // Wildcards allow covariance: 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()) flist.add(null); // Legal but uninteresting // We Know that it returns at least Fruit: Fruit f = flist.get(0); } }
答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List extends Fruit> flist它自身可以有多种含义:
List extends Fruit> flist = new ArrayList(); List extends Fruit> flist = new ArrayList (); List extends Fruit> flist = new ArrayList ();
所以对于实现了 extends T>的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。
如果我们要add元素应该怎么做呢?可以使用 super T>:
public class GenericWriting { static Listapples = new ArrayList (); static List fruit = new ArrayList (); static void writeExact(List list, T item) { list.add(item); } static void f1() { writeExact(apples, new Apple()); writeExact(fruit, new Apple()); } static void writeWithWildcard(List super T> list, T item) { list.add(item) } static void f2() { writeWithWildcard(apples, new Apple()); writeWithWildcard(fruit, new Apple()); } public static void main(String[] args) { f1(); f2(); } }
这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List super Apple> list,它可以有下面几种含义:
List super Apple> list = new ArrayList(); List super Apple> list = new ArrayList (); List super Apple> list = new ArrayList
当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:
如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections { public staticvoid copy(List super T> dest, List extends T> src) { for (int i=0; i
类型擦除
Java泛型中最令人苦恼的地方或许就是类型擦除了,特别是对于有C++经验的程序员。类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型。
说了这么多,那么泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:
public class Node{ private T data; private Node next; public Node(T data, Node 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; } // ... }
这意味着不管我们声明Node
public class Node> { private T data; private Node next; public Node(T data, Node next) { this.data = data; this.next = next; } public T getData() { return data; } // ... }
这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:
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; } // ... }
上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。
问题一
在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:
List[] arrayOfLists = new List [2]; // compile-time error
为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。
我们先来看一下下面这个例子:
Object[] strings = new String[2]; strings[0] = "hi"; // OK strings[1] = 100; // An ArrayStoreException is thrown.
对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:
Object[] stringLists = new List[]; // compiler error, but pretend it's allowed stringLists[0] = new ArrayList (); // OK // An ArrayStoreException should be thrown, but the runtime can't detect it. stringLists[1] = new ArrayList ();
假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new
ArrayList
如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:
public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList().getClass(); Class c2 = new ArrayList ().getClass(); System.out.println(c1 == c2); // true } }
问题二
继续复用我们上面的Node的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。
public class Node{ 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 { 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. // Integer x = mn.data;
如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)方法,所以只能调用父类Node的setData(Object data)方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException到底是怎么抛出的?
实际上Java编译器对上面代码自动还做了一个处理:
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的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Node
问题三
正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:
public staticvoid append(List list) { E elem = new E(); // compile-time error list.add(elem); }
但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:
public staticvoid append(List list, Class cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); }
我们可以像下面这样调用:
Listls = new ArrayList<>(); append(ls, String.class);
实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。
问题四
我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayList
public staticvoid rtti(List list) { if (list instanceof ArrayList ) { // compile-time error // ... } } => { ArrayList , ArrayList , LinkedList , ... }
和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:
public static void rtti(List> list) { if (list instanceof ArrayList>) { // OK; instanceof requires a reifiable type // ... } }
忍者必须死34399账号登录版 最新版v1.0.138v2.0.72
下载勇者秘境oppo版 安卓版v1.0.5
下载忍者必须死3一加版 最新版v1.0.138v2.0.72
下载绝世仙王官方正版 最新安卓版v1.0.49
下载Goat Simulator 3手机版 安卓版v1.0.8.2
Goat Simulator 3手机版是一个非常有趣的模拟游
Goat Simulator 3国际服 安卓版v1.0.8.2
Goat Simulator 3国际版是一个非常有趣的山羊模
烟花燃放模拟器中文版 2025最新版v1.0
烟花燃放模拟器是款仿真的烟花绽放模拟器类型单机小游戏,全方位
我的世界动漫世界 手机版v友y整合
我的世界动漫世界模组整合包是一款加入了动漫元素的素材整合包,
我的世界贝爷生存整合包 最新版v隔壁老王
我的世界MITE贝爷生存整合包是一款根据原版MC制作的魔改整