@ Testpublic void test() { ArrayList list = new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("ccc"); for (int i = 0; i < list.size(); i++) { System.out.println((String)list.get(i)); }}
当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。
基本语法如下:
public <类型参数> 返回类型 方法名(类型参数 变量名) { ...}
(1)只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
举例如下:
public class Test {// 该方法只是使用了泛型类定义的类型参数,不是泛型方法public void testMethod(U u){System.out.println(u);}// 真正声明了下面的方法是一个泛型方法public T testMethod1(T t){return t;}}
(2)泛型方法中可以同时声明多个类型参数。
举例如下:
public class TestMethod {public T testMethod(T t, S s) {return null;}}
(3)泛型方法中也可以使用泛型类中定义的泛型参数。
举例如下:
public class TestMethod {public U testMethod(T t, U u) {return u;}}
public class Test {public void testMethod(T t) {System.out.println(t);}public T testMethod1(T t) {return t;}}
上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。 而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。 虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
注意事项:
1. < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。 2. 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。 3. 与泛型类的类型参数定义一样,此处泛型方法中的 T 可以写为`任意标识`,常见的如 T、E、K、V 等形式的参数常用于表示泛型。
public class Test {// 这是一个简单的泛型方法 public static T add(T x, T y) { return y; } public static void main(String[] args) { // 一、不显式地指定类型参数 //(1)传入的两个实参都是 Integer,所以泛型方法中的 == int i = Test.add(1, 2); //(2)传入的两个实参一个是 Integer,另一个是 Float, // 所以取共同父类的最小级, == Number f = Test.add(1, 1.2);// 传入的两个实参一个是 Integer,另一个是 String,// 所以取共同父类的最小级, == Object o = Test.add(1, "asd"); // 二、显式地指定类型参数 //(1)指定了 = ,所以传入的实参只能为 Integer 对象 int a = Test.add(1, 2);//(2)指定了 = ,所以不能传入 Float 对象 int b = Test.add(1, 2.2);// 编译错误 //(3)指定 = ,所以可以传入 Number 对象 // Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象 Number c = Test.add(1, 2.2); } }
在该泛型类中定义了一个属性 num,该属性的数据类型是泛型类声明的类型参数 T ,这个 T 具体是什么类型,我们也不知道,它只与外部传入的数据类型有关。将这个泛型类反编译。
代码如下:
public class Caculate {public Caculate() {}// 默认构造器,不用管private Object num;// T 被替换为 Object 类型}
可以发现编译器擦除了 Caculate 类后面的泛型标识 < T >,并且将 num 的数据类型替换为 Object 类型,而替换了 T 的数据类型我们称之为原始数据类型。
那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释)。
再看一个例子,假设定义一个泛型类如下:
public class Caculate {private T num;}
将其反编译:
public class Caculate {public Caculate() {}// 默认构造器,不用管private Number num;}
可以发现,使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。
extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。 也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型,进行类型擦除并替换。(这一部分涉及到了泛型通配符,在下面还会具体介绍)
public class GenericType { public static void main(String[] args) { List list = new ArrayList(); } }
上面的代码很好得体现了 Java 的多态特性。
在 Java 标准库中的集合 ArrayList< T > 类实现了 List< T >接口,其源码大致如下:
public class ArrayList implements List {...}
那现在我们思考一个问题,在 ArrayList< T > 泛型集合中,当传入 < T > 中的数据类型相同时,是否还能将一个 ArrayList< T > 对象赋值给其父类的引用 List< T >。
代码如下:
public class GenericType { public static void main(String[] args) { List list = new ArrayList(); } }
上面的代码没有问题, 即 ArrayList< T > 对象可以向上转型为 List< T >,但两者传入 < T > 中的数据类型必须相同。
继续思考一个问题,已知 Integer 类是 Number 类的子类,那如果 ArrayList<> 泛型集合中,在 <> 之间使用向上转型,也就是将 ArrayList< Integer > 对象赋值给 List< Number > 的引用,是否被允许呢?
举例如下:
public class GenericType { public static void main(String[] args) { List list01 = new ArrayList();// 编译错误 ArrayList list02 = new ArrayList();// 编译错误 } }
上面代码会报错,我们发现并不能把 ArrayList< Integer > 对象赋值给 List< Number >的引用,甚至不能把 ArrayList< Integer > 对象赋值给 ArrayList< Number >的引用。这也说明了在一般泛型中,不能向上转型。
这是为什么?如果我们假设 ArrayList< Integer >可以向上转型为 ArrayList< Number >。
观察下面代码:
public class GenericType { public static void main(String[] args) { // 创建一个 ArrayList 集合ArrayList integerList = new ArrayList<>();// 添加一个 Integer 对象integerList.add(new Integer(123));// “向上转型”为 ArrayListArrayList numberList = integerList;// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错numberList.add(new Float(12.34));// 从 ArrayList 集合中获取索引为 1 的元素(即添加的 Float 对象):Integer n = integerList.get(1); // ClassCastException,运行出错 } }
当我们把一个 ArrayList< Integer > 向上转型为 ArrayList< Number > 类型后,这个 ArrayList< Number > 集合就可以接收 Float 对象了,因为 Float 类是 Number 类的子类。
正因如此,编译器为了避免发生这种错误,根本就不允许把 ArrayList< Integer >对象向上转型为 ArrayList< Number >;换而言之, ArrayList< Integer > 和 ArrayList< Number > 两者之间没有继承关系。
2. 泛型通配符的引入
我们上面讲到了泛型的继承关系,ArrayList< Integer > 不是 ArrayList< Number > 的子类。
(1)先看一个问题:假设我们定义了一个 Pair< T >类,如下:
public class Pair { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } public void setFirst(T first) { this.first = first; } public void setLast(T last) { this.last = last; }}
(2)然后,我们针对 Pair< Number >类型写了一个静态方法,它接收的参数类型是 Pair< Number >。
代码如下:
public class PairHelper { static int addPair(Pair p) { Number first = p.getFirst(); Number last = p.getLast(); return first.intValue() + last.intValue(); }}
(3)在测试类中创建一个 Pair< Number > 对象,并调用 addPair() 方法。
代码如下:
public class Main {public static void main(String[] args) { Pair pair = new Pair<>(1, 2); int sum = PairHelper.addPair(pair); } }
public class GenericType { public static void main(String[] args) { ArrayList list01 = new ArrayList();// 编译错误ArrayList extends Number> list02 = new ArrayList();// 编译正确 } }
public class GenericType { public static void main(String[] args) { ArrayList extends Number> list = new ArrayList<>();list.add(new Integer(1));// 编译错误list.add(new Float(1.0));// 编译错误 } }
// 改写前public class PairHelper { static int addPair(Pair p) { Number first = p.getFirst(); Number last = p.getLast(); return first.intValue() + last.intValue(); }}// 改写后public class PairHelper { static int addPair(Pair extends Number> p) { Number first = p.getFirst(); Number last = p.getLast(); return first.intValue() + last.intValue(); }}
下界通配符 super T>:T 代表了类型参数的下界, super T>表示类型参数的范围是 T 和 T 的超类,直至 Object。需要注意的是: super T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
(1)ArrayList super Integer> 在逻辑上表示为 Integer 类以及 Integer 类的所有父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、 ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。
举个例子:
public class GenericType { public static void main(String[] args) { ArrayList list01 = new ArrayList();// 编译错误ArrayList super Integer> list02 = new ArrayList();// 编译正确 } }
逻辑上可以将 ArrayList super Integer> 看做是 ArrayList< Number > 的父类,因此,在使用了下界通配符 super Integer> 后,便可以将 ArrayList< Number > 对象向上转型了。
(2)ArrayList super Integer> 只能表示指定类型参数范围中的某一个集合,但我们不能指定 ArrayList super Integer> 的数据类型。(这里有点难理解)
看一个例子:
public class GenericType { public static void main(String[] args) { ArrayList super Number> list = new ArrayList<>();list.add(new Integer(1));// 编译正确list.add(new Float(1.0));// 编译正确// Object 是 Number 的父类 list.add(new Object());// 编译错误 } }
这里奇怪的地方出现了,为什么和ArrayList extends Number> 集合不同, ArrayList super Number> 集合中可以添加 Number 类及其子类的对象呢?
其原因是, ArrayList super Number> 的下界是 ArrayList< Number > 。因此,我们可以确定 Number 类及其子类的对象自然可以加入 ArrayList super Number> 集合中; 而 Number 类的父类对象就不能加入 ArrayList super Number> 集合中了,因为不能确定 ArrayList super Number> 集合的数据类型。
5.2 super T> 的用法
(1)下界通配符 super T> 的正确用法:
public class Test {public static void main(String[] args) {// 创建一个 ArrayList super Number> 集合ArrayList list = new ArrayList(); // 往集合中添加 Number 类及其子类对象list.add(new Integer(1));list.add(new Float(1.1));// 调用 fillNumList() 方法,传入 ArrayList 集合fillNumList(list);System.out.println(list);}public static void fillNumList(ArrayList super Number> list) {list.add(new Integer(0));list.add(new Float(1.0));}}
输出如下:
与带有上界通配符的集合ArrayList extends T>的用法不同,带有下界通配符的集合ArrayList super Number> 中可以添加 Number 类及其子类的对象;ArrayList super Number>的下界就是ArrayList集合,因此,其中必然可以添加 Number 类及其子类的对象;但不能添加 Number 类的父类对象(不包括 Number 类)。
(2)下界通配符 super T> 的错误用法:
public class Test { public static void main(String[] args) { // 创建一个 ArrayList 集合 ArrayList list = new ArrayList<>(); list.add(new Integer(1)); // 调用 fillNumList() 方法,传入 ArrayList 集合 fillNumList(list);// 编译错误 } public static void fillNumList(ArrayList super Number> list) { list.add(new Integer(0));// 编译正确 list.add(new Float(1.0));// 编译正确// 遍历传入集合中的元素,并赋值给 Number 对象;会编译错误 for (Number number : list) { System.out.print(number.intValue() + " "); System.out.println(); } // 遍历传入集合中的元素,并赋值给 Object 对象;可以正确编译 // 但只能调用 Object 类的方法,不建议这样使用 for (Object obj : list) { System.out.println(obj);使用 } }}
注意,ArrayList super Number> 代表了 ArrayList< Number >、 ArrayList< Object > 中的某一个集合,而 ArrayList< Integer > 并不属于 ArrayList super Number> 限定的范围,因此,不能往 fillNumList() 方法中传入 ArrayList< Integer > 集合。
并且,不能将传入集合的元素赋值给 Number 对象,因为传入的可能是 ArrayList< Object > 集合,向下转型可能会产生ClassCastException 异常。
(1)copy() 方法的作用是把一个 List 中的每个元素依次添加到另一个 List 中。它的第一个形参是 List super T>,表示目标 List,第二个形参是 List extends T>,表示源 List。
代码如下:
public class Collections { // 把 src 的每个元素复制到 dest 中: public static void copy(List super T> dest, List extends T> src) { for (int i = 0; i < src.size(); i++) { // 获取 src 集合中的元素,并赋值给变量 t,其数据类型为 T T t = src.get(i); // 将变量 t 添加进 dest 集合中 dest.add(t);// 添加元素进入 dest 集合中 } }}
我们可以简单地用 for 循环实现复制。在 for 循环中,我们可以看到,对于 extends T> 集合 src,我们可以安全地获取类型参数 T的引用(即变量 t),而对于 super T> 的集合 dest,我们可以安全地传入类型参数 T的引用。
(2)copy() 方法的定义完美地展示了通配符 extends 和 super 的意图:
copy() 方法内部不会读取 dest,因为不能调用 dest.get() 方法来获取 T 的引用(如果调用则编译器会直接报错)。
这是由编译器检查来实现的。如果在方法代码中意外修改了 src 集合,或者意外读取了 dest ,就会导致一个编译错误。
代码如下:
public class Collections { // 把 src 的每个元素复制到 dest 中: public static void copy(List super T> dest, List extends T> src) { ... // 获取 super T> 集合的元素只能赋值给 Object 对象 T t = dest.get(0); // 编译错误 // 不能向 extends T> 集合中添加任何类型的对象,除了 null src.add(t); // 编译错误 }}
根据上面介绍的,获取 super T> 集合 dest 的元素后只能赋值给 Object 对象,而不能赋值给其下界类型 T;我们不能向 extends T> 集合 src 中添加任何类型的对象,除了 null。
(3)copy() 方法的另一个好处是可以安全地把一个 List< Integer >添加到 List< Number >,但是无法反过来添加。
代码如下:
// 将 List 复制到 ListList numList = ...;List intList = ...;Collections.copy(numList, intList);// 编译正确// 不能将 List 复制到 ListCollections.copy(intList, numList);// 编译错误
这个很好理解,List< Number > 集合中可能有 Integer、Float 等对象,所以肯定不能复制到List< Integer > 集合中;而 List< Integer > 集合中只有 Integer 对象,因此肯定可以复制到 List< Number > 集合中。
8. PECS 原则
我们何时使用 extends,何时使用 super 通配符呢?为了便于记忆,我们可以用 PECS 原则:Producer Extends Consumer Super。
即:如果需要返回 T,则它是生产者(Producer),要使用 extends 通配符;如果需要写入 T,则它是消费者(Consumer),要使用 super 通配符。
还是以 Collections 的 copy() 方法为例:
public class Collections { public static void copy(List super T> dest, List extends T> src) { for (int i = 0; i < src.size(); i++) { T t = src.get(i); // src 是 producer dest.add(t); // dest 是 consumer } }}
需要返回 T 的 src 是生产者,因此声明为List extends T>,需要写入 T 的 dest 是消费者,因此声明为List super T>。