什么是泛型?
Java SE 5 开始引入了泛型的概念,泛型即参数化类型,利用泛型我们可以编写出更通用的代码(先不指定类型,使用时再指定类型)。泛型出现的最大的目的之一就是用来指定容器要持有的对象的类型,而且这种指定是由编译器来保证其正确性的。来看个例子:
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) {
// 可以不指定类型,类型参数 T 就是 Object,因此会被初始化为 null
Holder holder = new Holder();
System.out.println(holder.get());
Holder<String> strHolder = new Holder<>("Aaron");
System.out.println(strHolder.get());
strHolder.set(1); // Error
}
}
以上代码中定义了一个 Holder
类,使用类型参数 T
作为持有的对象的类型。T
可以表示任何对象,所以Holder
类也就具有了持有任何对象的能力。当我们使用它的时候,可以使用 <>
来确定 Holder
所持有的对象的类型,这样我们就可以保证持有对象的类型的正确性。
泛型方法
如果普通方法中定义了泛型参数,那么这就是一个泛型方法。泛型方法和与该类是否是泛型类无关,但是如果静态方法想要使用泛型参数,那么它就必须定义为泛型方法,因为静态方法无法访问泛型类中的泛型参数。
public class DemoGenericMethod<E> {
private E e;
public DemoGenericMethod() {
e = (E) new Object();
}
public DemoGenericMethod(E e) {
this.e = e;
}
/**
* 根据泛型类的类型变量返回相应类型的 List
*/
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
使用。
// ArrayList<String> 被擦除为 ArrayList
Class c1 = new ArrayList<String>().getClass();
// ArrayList<Integer> 被擦除为 ArrayList
Class c2 = new ArrayList<Integer>().getClass();
// 这两个 Class 对象都被擦除为 ArrayList了,所以是相等的
System.out.println("c1 == c2? " + (c1 == c2)); // true
擦除意味着无法使用 instanceof
,new
或者转型等需要在运行时才能知道确切类型信息的操作。
边界
边界允许我们在参数类型上设置限制条件,这样就能部分抵消擦除带来的负面影响。来看代码:
interface HasColor {
Color getColor();
}
/**
* 用 extend 关键字指定类型参数的边界
*/
class Colored<T extends HasColor> {
T element;
Colored(T element) {
this.element = element;
}
T getElement() {
return element;
}
Color color() {
// 因为设置了边界,所以调用是安全的
return element.getColor();
}
}
可以看到,我们用 extend
关键字指定泛型边界。当参数类型继承多个边界时,定义的规则与类的继承相同,类在前,接口在后,类与多个接口的连接符用 &
,比如:
class Solid<T extends Dimension & HasColor & Weight> {...}
通配符
通配符允许我们更加自由地使用泛型类。
- 首先是通配符结合
extends
关键字,用于确定泛型类的上边界。
// 表示"具有任何从 Number 类继承的类型的 List"
List<? extends Number> numbers = new ArrayList<Integer>();
但是此时往 numbers 中添加任何元素都是不被允许的,因为只声明了上边界,编译器是无法确定捕获(capture)的类型到底是什么,所以无论添加任何对象都是类型不安全的。
- 通配符 +
super
关键字,即超类型通配符,用于确定泛型类的下边界。
// 表示边界范围由 "Number 类的任何基类" 来确定
List<? super Number> numbers = new ArrayList<>();
使用超类型通配符后,由于下边界确定,所以当我们向 list 中添加 Number 类或者其子类的时候,才可能保证是类型安全的。同样地,如果添加的是 Number 类的超类,也是不被允许的,因为编译器无法确定捕获的超类型到底是哪个超类。
可能文字比较难以理解,来看几个例子,更为直观:
public static void main(String[] args) {
// 通配符必须是单一边界的,无法使用继承
//List<? extends Fruit & Something > wildcard = new ArrayList<>();
// Incompatible types,泛型不支持协变返回类型
//List<Number> numberList = new ArrayList<Integer>();
System.out.println("----------确定泛型的上边界----------");
// 但是利用通配符和 extends 可以做到这一点
// 这样的 List 可以看作是“具有任何从 Number 类继承的 List”,即为通配符确定了上界
List<? extends Number> numbers = new ArrayList<Integer>();
/*
* 但是这样的List是非常有局限性的,无法添加任何有意义的元素
* 因为List的参数类型为 ? extends Fruit,也就是任何继承自 Fruit 的对象
* 编译器无法确定 List 所持有的类型,这样就无法保证类型安全性,所以不允许添加任何有意义的对象
* */
//numbers.add(1);
//numbers.add(new Object()); // 甚至连 Object 都无法添加
// 可以添加 null 进去,因为 null 可以表示任何对象,这也说明了此时添加某个对象是不安全的
numbers.add(null);
// 可以从中取出元素,因为至少可以确定这是一个 Number 类的对象
Number number = numbers.get(0);
System.out.println(number);
System.out.println("----------泛型的转型----------");
Holder<Number> numberHolder = new Holder<>(1);
// Incompatible types,无法向上转型
//Holder<Integer> intHolder = numberHolder;
// 利用通配符设定边界后可以完成向上转型
Holder<? extends Number> nHolder = numberHolder;
// 无法完成调用set,原因同上,捕获的 Fruit 类无法应用到 Apple
//nHolder.set(new Apple());
// 获取 Fruit,但是实际类型为 Apple
Number num = nHolder.get();
Integer iNum = (Integer) nHolder.get();
System.out.println("num: " + num + ", iNum: " + iNum);
System.out.println("num.equals(iNum) " + num.equals(iNum)); // true
System.out.println("----------确定泛型的下边界----------");
// 用 super 关键字指定下界后才可以添加内容
List<? super Number> boundedNums = new ArrayList<>();
boundedNums.add(1);
writeTo(boundedNums);
System.out.println(boundedNums);
System.out.println("----------无界通配符----------");
List<?> list = new ArrayList<String>();
// 捕获的参数类型 capture<?> 无法应用到String
//list.add("1");
}
无界通配符
上面的例子中有关于无界通配符的使用,第一次看到会觉得似乎难以理解,其实它表示的意思是”我可以持有任何类型“,它是更为泛化的参数化类型,但也因此无法像有界的泛型参数那样做更多的事。它最主要的用途就是捕获转换,即捕获未指定的通配符类型,然后将之转换为确切的某种类型。
public class DemoCaptureConversion {
static <T> void f1(Holder<T> holder) {
System.out.println(holder.get().getClass().getSimpleName());
}
// 调用该方法时,发生了类型参数的捕获
static void f2(Holder<?> holder) {
f1(holder);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder(1);
f1(raw);
f2(raw);
Holder<?> wildcarded = new Holder<>(1.2f);
f1(wildcarded);
f2(wildcarded);
}
}
以上代码中,f1() 中的参数是确切的已知的,而 f2() 中使用了无界通配符,参数是未知的。在调用 f2() 的时候,首先会捕获参数类型,然后转换为确切的类型以供 f1() 调用。
问题
- 基本类型无法作为类型参数,必须使用其包装类。
- 不能同时实现同一个泛型接口的两种变体,原因是接口的参数类型会被擦除,也就相当于同一个接口被实现了两次。
interface Pay<T> {}
class Emp implements Pay<Emp> {}
// cannot be inherited with different type arguments
class Hour extends Emp implements Pay<Hour>{}
- 对泛型转型有时会产生”unchecked cast“的警告,因为编译期无法确定转型是否安全。
- 无法使用泛型参数作为区分两个方法,也就是无法用类型参数作为重载的依据。
- 基类劫持接口,最常见的例子就是实现
Comparable
接口。
// Pet 类把自己作为参数类型传到 Comparable 接口中
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 中,除了以上问题外,还详细讲解了自限定的类型、动态类型安全、泛型在异常中的使用、混型、潜在类型机制的缺失及补偿、将函数对象用作策略等,想要深入了解泛型的,不妨仔细阅读下这部分内容,如果有新的感受记得留言交流哦~
参考资料:
- Think in Java 第4版
网友评论