一、泛型
1 概述
1.1 泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
方法 public void set(int a) {} public void set(String b) {} 类ArrayList<E> 传入Integer则为ArrayList<Integer>类 传入String则为ArrayList<String>类
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 主要是为了提高代码可读性和安全性。
// 不会报错,运行时容易产生ClassCastException List arrayList = new ArrayList(); arrayList.add("aaaa"); arrayList.add(100);
1.2 类型擦除
类型擦除指的是,你在给类型参数<T> 赋值时,编译器会将实参类型擦除为Object(这里假设没有限定符,限定符下面会讲到),即泛型只在编译阶段有效。 所以这里我们要明白一个东西:虚拟机中没有泛型类型对象的概念,在它眼里所有对象都是普通对象
List List1 = new ArrayList(); List1.add("aaaa"); List List2 = new ArrayList(); List2.add(100); list1.getclass()==list2.getClass() public class EraseDemo<T> { private T t; } //类型擦除后 public class EraseDemo<T> { private Object t; } public class EraseDemo<T extends Animal> { private T t; } //类型擦除后 public class EraseDemo { private Animal t; }
2. 通配符限定
2.1 常用的 T,E,K,V,?
本质上这些个都是通配符,是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:
-
?表示不确定的 Java 类型(类型通配符),一般用于方法声明,表示泛型方法形参(无需定义即可使用),不用于定义类和泛型方法。
List<?> list = new ArrayList<>(); List<T> list = new ArrayList<>();
//当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。 public static void printList(List<?> list) { for (Object o : list) { //可以使用Object的方法 System.out.println(o); } } public static void main(String[] args) { List<String> l1 = new ArrayList<>(); l1.add("aa"); l1.add("bb"); l1.add("cc"); printList(l1); List<Integer> l2 = new ArrayList<>(); l2.add(11); l2.add(22); l2.add(33); printList(l2); }
-
T (type) 表示具体的一个Java类型(类型参数),通常用于泛型类和泛型方法的定义。
public class A<T> {} //上界为Object public class B<T entends Number> {} //上界为Number
-
K V (key value) 分别代表Java键值中的Key Value
-
E (element) 代表Element
2.2 无边界的通配符?
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
/** * String 是 Object 的子类,但是 List<String> 不是 List<Object> 的子类。 * 例如:如下代码会导致编译错误。 * 如果将 List<Object> 换成 List<?>,则可以编译通过。 */ public static void func(List<?> list) { } public static void main(String[] args) { List<String> list = new ArrayList<String>(); func(list); // 编译错误. }
下面这个例子,List不是List的父类 ,同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
static int countLegs (List<? extends Animal> animals ) { int retVal = 0; for (Animal animal : animals) { retVal += animal.countLegs(); } return retVal; } static int countLegs1 (List<Animal> animals){ int retVal = 0; for ( Animal animal : animals ) { retVal += animal.countLegs(); } return retVal; }
public static void main(String[] args) { List<Animal> Animals = new ArrayList<>(); // 不会报错 countLegs(Animals); // 不会报错 countLegs1(Animals); List<Dog> dogs = new ArrayList<>(); List<Dog> dogs = new ArrayList<>(); // 不会报错 countLegs(dogs); // 编译报错 countLegs1(dogs); }
我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对,只有null是所有引用数据类型都具有的元素。 List<?>也不能使用get方法, 只有Object类型是个例外. 为什么呢? 因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get,但是Object是所有数据类型的父类, 所以只有接受他可以。
public static void addTest(List<?> list) { Object o = new Object(); // list.add(o); // 编译报错 // list.add(1); // 编译报错 // list.add("ABC"); // 编译报错 list.add(null); } // 过使用强制转换,就没有必要使用?,直接用List<T>或者list<Integer> public static void getTest(List<?> list) { // String s = list.get(0); // 编译报错 // Integer i = list.get(1); // 编译报错 Object o = list.get(2); }
2.3 固定上边界通配符
使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。 要声明使用该类通配符,采用<? extends E>的形式, 这里的E就是该泛型的上边界。
public static double sumOfList(List<? extends Number> list) { double s = 0.0; for (Number n : list) { // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double. s += n.doubleValue(); } return s; } public static void main(String[] args) { List<Integer> list1 = Arrays.asList(1, 2, 3, 4); System.out.println(sumOfList(list1)); List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4); System.out.println(sumOfList(list2)); }
有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:
public static void addTest2(List<? extends Number> l) { // l.add(1); // 编译报错 // l.add(1.1); //编译报错 l.add(null); }
为什么? 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:
List<Integer> list = new ArrayList<>(); addTest2(list);
那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能add。 但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.
2.4 固定下边界通配符
使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界。(不能同时指定上下边界)
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } public static void main(String[] args) { List<Object> list1 = new ArrayList<>(); addNumbers(list1); System.out.println(list1); List<Number> list2 = new ArrayList<>(); addNumbers(list2); System.out.println(list2); List<Double> list3 = new ArrayList<>(); // addNumbers(list3); // 编译报错 }
我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的,而传入的list不管是什么,都一定是Integer或其父类泛型的List,这时add一个Integer元素是没有任何疑问的。 但是, 我们不能使用get方法, 请看如下代码:
public static void getTest2(List<? super Integer> list) { // Integer i = list.get(0); //编译报错 Object o = list.get(1); } public static void addNumbers(List<? super Dog> list) { for (int i = 1; i <= 10; i++) { list.add(new PoodleDog());//Dog及其子类不报错 list.add(new Animal());//编译报错 } }
这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型。
总结:
对于extends和super的使用有PECS(即"Producer Extends, Consumer Super"),"in out"等原则, 总的来说就是:
-
in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字,固定上边界的通配符. 你可以将该对象当做一个只读对象;
-
out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
-
当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
-
当你需要一个既能读又能写的对象时, 就不要使用通配符了.