什么是泛型?
Java SE 5 开始引入了泛型的概念,泛型即参数化类型,利用泛型我们可以编写出更通用的代码(先不指定类型,使用时再指定类型)。泛型出现的最大的目的之一就是用来指定容器要持有的对象的类型,而且这种指定是由编译器来保证其正确性的。来看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Holder<T> { private T value;
public Holder() { }
public Holder(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
public static void main(String[] args) { Holder holder = new Holder(); System.out.println(holder.get());
Holder<String> strHolder = new Holder<>("Aaron"); System.out.println(strHolder.get());
strHolder.set(1); } }
|
以上代码中定义了一个 Holder
类,使用类型参数 T
作为持有的对象的类型。T
可以表示任何对象,所以Holder
类也就具有了持有任何对象的能力。当我们使用它的时候,可以使用 <>
来确定 Holder
所持有的对象的类型,这样我们就可以保证持有对象的类型的正确性。
泛型方法
如果普通方法中定义了泛型参数,那么这就是一个泛型方法。泛型方法和与该类是否是泛型类无关,但是如果静态方法想要使用泛型参数,那么它就必须定义为泛型方法,因为静态方法无法访问泛型类中的泛型参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class DemoGenericMethod<E> {
private E e;
public DemoGenericMethod() { e = (E) new Object(); }
public DemoGenericMethod(E e) { this.e = e; }
public List<E> getAsList(E e) { System.out.println(e.getClass().getName()); return new ArrayList<>(); }
public <T> T printClassName(T t) { System.out.println(t.getClass().getName()); return t; }
public <T> void printSelfAndThis(T t) { System.out.println("this = " + e.getClass().getName() + ", that = " + t.getClass().getName()); }
public static <T> void getName(T t) { System.out.println(t.getClass().getName()); }
public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<>(); Collections.addAll(result, args); return result; } }
|
擦除
在使用泛型时,你是无法通过代码获得任何有关泛型参数类型的信息的,这其实是因为擦除的存在。在泛型类或泛型方法中,关于泛型参数的任何具体的类型信息都被擦除了(wiped),所以我们只能把类型参数当作一个 Object
使用。
1 2 3 4 5 6
| Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println("c1 == c2? " + (c1 == c2));
|
擦除意味着无法使用 instanceof
,new
或者转型等需要在运行时才能知道确切类型信息的操作。
边界
边界允许我们在参数类型上设置限制条件,这样就能部分抵消擦除带来的负面影响。来看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| interface HasColor { Color getColor(); }
class Colored<T extends HasColor> { T element;
Colored(T element) { this.element = element; }
T getElement() { return element; }
Color color() { return element.getColor(); } }
|
可以看到,我们用 extend
关键字指定泛型边界。当参数类型继承多个边界时,定义的规则与类的继承相同,类在前,接口在后,类与多个接口的连接符用 &
,比如:
1
| class Solid<T extends Dimension & HasColor & Weight> {...}
|
通配符
通配符可以保证类型安全,允许我们更加自由地使用泛型类,使得泛型支持协变和逆变。
- 首先是通配符 +
extends
关键字,用于确定泛型类的上边界。
1 2
| List<? extends Number> numbers = new ArrayList<Integer>();
|
但是此时往 numbers 中添加任何元素都是不被允许的,因为只声明了上边界,编译器是无法确定捕获 (capture) 的类型到底是什么,所以无论添加任何对象都是类型不安全的。
- 通配符 +
super
关键字,即超类型通配符,用于确定泛型类的下边界。
1 2
| List<? super Number> numbers = new ArrayList<>();
|
使用超类型通配符后,由于下边界确定,所以当我们向 list 中添加 Number 类及其子类的时候,才可能保证是类型安全的。不过,如果添加的是 Number 类的超类,也是不被允许的,因为编译器无法确定捕获的超类型到底是哪个超类。
可能文字比较难以理解,来看几个例子,更为直观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| public static void main(String[] args) {
System.out.println("----------确定泛型的上边界----------"); List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(null);
Number number = numbers.get(0); System.out.println(number);
List<Apple> appleList = new ArrayList<>(); appleList.add(new Apple("Apple")); showList(appleList);
System.out.println("----------确定泛型的下边界----------"); List<? super Number> boundedNums = new ArrayList<>(); boundedNums.add(1); boundedNums.add(1.0f); boundedNums.add(1.33d); System.out.println(boundedNums);
Object num1 = boundedNums.get(0);
Integer number1 = (Integer) boundedNums.get(0);
System.out.println("----------无界通配符----------"); List<?> list = new ArrayList<String>(); }
static void showList(Collection<? extends Fruit> fruits) { for (Fruit fruit : fruits) { fruit.sayName(); } }
|
总结一下,上界通配符使得泛型类只能被读取而无法修改,因此这种泛型类型也被称为生产者;而下界通配符使得泛型类只能被修改而无法被读取,因此这种泛型类型就是消费者。
无界通配符
上面的例子中有关于无界通配符的使用,第一次看到会觉得似乎难以理解,其实它表示的意思是”我可以持有任何类型“,它是更为泛化的参数化类型,但也因此无法像有界的泛型参数那样做更多的事。它最主要的用途就是捕获转换,即捕获未指定的通配符类型,然后将之转换为确切的某种类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class DemoCaptureConversion {
static <T> void captureWithType(Holder<T> holder) { System.out.println(holder.get().getClass().getSimpleName()); }
static void captureWithWildcard(Holder<?> holder) { captureWithType(holder); }
@SuppressWarnings("unchecked") public static void main(String[] args) { Holder raw = new Holder(1); captureWithType(raw); captureWithWildcard(raw); Holder<?> wildcarded = new Holder<>(1.2f); captureWithType(wildcarded); captureWithWildcard(wildcarded); } }
|
以上代码中,第一个方法中的参数是确切的已知的,而第二个方法中使用了无界通配符,参数是未知的。在调用第二个方法的时候,可以捕获到类型参数并进行调用。
泛型存在的问题
- 基本类型无法作为类型参数,必须使用其包装类。
- 不能同时实现同一个泛型接口的两种变体,原因是接口的参数类型会被擦除,也就相当于同一个接口被实现了两次。
1 2 3 4 5 6
| interface Pay<T> {}
class Employ implements Pay<Emp> {}
class Hour extends Employ implements Pay<Hour>{}
|
- 对泛型转型有时会产生”unchecked cast“的警告,因为编译期无法确定转型是否安全。
- 无法使用泛型参数作为区分两个方法,也就是无法用类型参数作为重载的依据。
- 基类劫持接口,最常见的例子就是实现
Comparable
接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Pet implements Comparable<Pet> {
private String name; private int age;
public Pet(String name, int age) { this.name = name; this.age = age; }
@Override public int compareTo(@NotNull Pet pet) { return Integer.compare(this.age, pet.age); } }
|
结语
在 Think in Java 中,除了以上问题外,还详细讲解了自限定的类型、动态类型安全、泛型在异常中的使用、混型、潜在类型机制的缺失及补偿、将函数对象用作策略等,想要深入了解泛型的,不妨仔细阅读下这部分内容,如果有新的感受记得留言交流哦~
参考资料: