目录
8.6.8 不允许静态属性是参数化类型,并且类中的静态方法,如果要使用泛型能力,必须将该方法声明为泛型方法
8.1 为什么要使用泛型程序设计
泛型是JavaSE1.5的新特效,泛型的本质是参数化类型,就是说所操作的数据类型被指定为一个参数,这种参数可以用在类、接口和方法中创建,分别称为泛型类、泛型接口、泛型方法。引用泛型的好处是安全简单。
泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比使用Object时强制类型转换的机制具有更好的可读性和安全性。
8.1.1 类型参数的好处
试想在没有泛型设计之前,要想使ArrayList对任意对象都通用,需要使用继承实现,即只维护一个Object引用的数组:
public class ArrayList { // before generic classes
private Object[] elementData; // jdk8中依然是定义一个Object数组,强制类型转换交由编译器去完成
public Object get(int i) { . . .}
public void add(Object o) { . . . }
}
这种写法会存在两个问题:
-
取值时必须进行强制类型转换
-
可以添加任意类型的对象
没有进行类型检查,在运行期间就可能报错。
泛型提供了解决方案:类型参数。
ArrayList<String> files = new ArrayList<String>();
这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。
编译器会依据声明的类型参数,进行类型检查,使得程序具有更好的可读性和安全性。
-
使用泛型能写出更加灵活通用的代码
-
泛型将代码安全性检查提前到编译期
-
泛型能够省去类型强制转换(编译器自动插入强制类型转换)
8.2 泛型类
一个泛型类(generic class) 就是具有一个或多个类型变量的类。
/**
* 定义一个简单的泛型类
* @param <T>
*/
public class Pair<T> {
private T first;
private T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "Pair{" +
"first=" + first +
", second=" + second +
'}';
}
}
8.3 泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
/**
* 泛型方法
* 获取中间元素
* @param a
* @param <T> 放在修饰符和返回值类型中间
* @return
*/
public static <T> T getMiddleElement(T... a) {
if (a == null || a.length <= 0) {
return null;
}
return a[a.length / 2];
}
类型变量在方法修饰符的后面,返回值的前面。
8.4 类型变量的限定
/**
* 获取数组最值
* @param a
* @return
*/
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length <= 0) {
return new Pair(null, null);
}
// 不改变入参
T[] temp = a.clone();
List<T> aList = Arrays.asList(temp);
aList = aList.stream().filter(obj -> obj != null).collect(Collectors.toList());
if (org.springframework.util.CollectionUtils.isEmpty(aList)) {
return new Pair<>(null, null);
}
T min = aList.get(0);
T max = aList.get(0);
for (T str : aList) {
if (str.compareTo(min) < 0) {
min = str;
}
if (str.compareTo(max) > 0) {
max = str;
}
}
return new Pair<>(min, max);
}
T extends Comparable & Serializable
在 Java 的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。 如果用一个类作为限定,它必须是限定列表中的第一个。
8.5 泛型代码和虚拟机
8.5.1 类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除 ( erased ) 类型变量, 并替换为第一个限定类型 (无限定的变量用 Object)。
举个例子:
class Interval<T extends Serializable & Comparable>
如果这样做,原始类型用Serializable替换T, 而编译器在必要时要将 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging) 接口 (即没有方法的接口)放在边界列表的末尾。
-
将泛型中所有参数化类型替换为泛型边界,如果参数化类型是无界的,则替换为 Object 类型。字节码中没有任何泛型的相关信息。
-
为了类型安全,在必要时插入类型转换代码。
-
生成桥接方法来保持泛型类型的多态性。 ——> 桥接方法什么鬼?
8.5.2 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器自动插入强制类型转换。——> 类型被擦除了,在必要的时候当然要强制类型转换
Pair<Employee> buddies = . .
Employee buddy = buddies.getFirst(); // 类型擦除之后,返回值是Object,编译器在字节码中自动插人 Employee 的强制类型转换。
8.5.3 翻译泛型方法
一个日期区间是一对 LocalDate 对象,并且需要覆盖(Override)这个方法来确保第二个值永远不小于第一个值。
class DateInterval extends Pair<LocalDate> {
public setSecond(LocalDate second) {
if (second.compareTo(getFirst) >= 0) {
super.setSecond(second);
}
}
...
}
这个类擦除后变成:
class DateInterval extends Pair { // after erasure
public void setSecond(LocalDate second) { . . . }
...
}
注意:实际存在另一个从 Pair 继承的 setSecond 方法,不满足重写规则了,虚拟机该怎么办?
public void setSecond(Object second) {...}
再考虑下面这个语句序列:
DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
这里,希望对 setSecond 的调用具有多态性,并调用最合适的那个方法。由于 pair 引用 Datelnterval 对象,所以应该调用 Datelnterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在 Datelnterval 类中生成一个桥方法(bridge method): ——> 编译器辛苦了。。生成字节码
public void setSecond(Object second) {
setSecond((Date) second);
}
桥方法可能会变得十分奇怪。假设 Datelnterval 方法也覆盖了 getSecond 方法:
public LocalDate getSecond() { return (Date) super.getSecond().clone(); }
在 Datelnterval 类中,有两个 getSecond 方,能否满足重写规则?
LocalDate getSecond() // defined in Datelnterval
Object getSecond() // overrides the method defined in Pair to call the first method
在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。—> 虚拟机也辛苦了。。满足重写的规则:返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
总之,需要记住有关 Java 泛型转换的事实:
-
虚拟机中没有泛型,只有普通的类和方法。
-
所有的类型参数都用它们的限定类型替换。
-
桥方法被合成来保持多态。
-
为保持类型安全性,必要时插人强制类型转换。
8.6 约束与局限性
8.6.1 不能用基本类型实例化类型参数
8.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
if (a instanceof Pair) // 实际上仅仅测试 a 是否是任意类型的一个 Pair
Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair
为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
同样的道理,getClass 方法总是返回原始类型。例如:
Pair<String> stringPair = . .
Pair<Employee> employeePair = . .
if (stringPair.getClass() == employeePair.getClass()) // they are equal
8.6.3 不能创建确切类型的泛型数组(通配符可以)
Pair<String>[] table = new Pair<String>[10]; // Error
Generic<?>[] generics = new Generic<?>[2]; // OK
8.6.4 不能实例化类型变量
不能使用像 new T(...) ,newT[...] 或 T.class 这样的表达式中的类型变量。
public Pair() { first = new T(); second = new T(); } // Error
8.6.5 不能构造泛型数组
public static <T extends Comparable> T[] minmax(T[] a) { T[] a = new T[2]; …} //Error
就像不能实例化一个泛型实例一样, 也不能实例化数组。 不过原因有所不同, 毕竟数组会填充 null 值,构造时看上去是安全的。不过, 数组本身也有类型, 用来监控存储在虚拟机中的数组。这个类型会被擦除。
8.6.6 不能重载参数类型为相同原始类型的方法
public class Example {
// 不能有两个重载方法,当他们的方法签名在类型擦除后是一样的。
public void print(List<String> list) {}
public void print(List<Integer> list) {}
}
8.6.7 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。 实际上,甚至泛型类扩展 Throwable 都是不合法的。
public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable
catch 子句中不能使用类型变量。 例如, 以下方法将不能编译:
public static <T extends Throwable〉 void doWork(Class<T> t) {
try {
do work
} catch (T e) {...} // Error can't catch type variable
}
8.6.8 不允许静态属性是参数化类型,并且类中的静态方法,如果要使用泛型能力,必须将该方法声明为泛型方法
8.7 泛型类型的继承规则
// Pair<Manager> 是 Pair<Employee> 的一个子类吗? 不是!
Pair<Manager> a = ...;
Pair<Employee> result = a; // Error
必须注意泛型与 Java 数组之间的重要区别。 可以将一个 Manager[]数组賦给一个 类型为 Employee[] 的变量:
Manager[] managerBuddies = { ceo, cfo };
Employee[] employeeBuddies = managerBuddies; // OK
然而数组带有特别的保护。如果试图将一个低级别的雇员存储到 employeeBuddies[0] ,虚拟机将会抛出 ArrayStoreException 异常。
最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如,ArrayList<T> 类实现 List<T> 接口。这意味着,一个 ArrayList<Manager> 可以被转换为一个 List< Manager>。但是,如前面所见,一个 ArrayList< Manager> 不是一个 ArrayList <Employee> 或 List<Employee>。
8.8 通配符类型
从需求看通配符的使用:
问题1:前面我们说,Pair<Manager> 和 Pair<Employee>没有什么关系,所以,在使用 Pair<Employee>作为形参的方法中,不能使用Pair<Manager>的实例传入。如果现在有需求要将参数类型是Employee及其子类的Pair对象作为方法参数,该如何实现呢?
// 参数类型
Pair<?> s 或者 Pair<? extends Object> s 或者 Pair<? extends Employee> s
问题2:定义一个方法,接收一个任意集合,并打印出集合中的所有元素,如下所示:
public static void printCollection(Collection<?> c) {
c.size();
// c.add(1); // 不能调用与类型相关的方法
for (Object o : c) {
System.out.println(o);
}
}
8.8.1 泛型的上下边界
-
在Java泛型定义时:
用<T>等大写字母标识泛型类型,用于表示未知类型。
用<T extends ClassA & InterfaceB …>等标识有界泛型,用于表示有边界的类型。
如果我们把泛型类的定义改一下:
public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//这一行代码也会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");
再来一个泛型方法的例子:
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
-
在Java泛型实例化时:
用<?>标识通配符,用于表示实例化时的类型。
用<? extends 父类型>标识上边界通配符,用于表示实例化时可以确定父类型的类型。
用<? super 子类型>标识下边界通配符,用于表示实例化时可以确定子类型的类型。
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
为泛型添加上边界,即传入的类型实参必须是指定类型或者该类型的子类型:
// 普通方法
public void showKeyValue1(Generic<? extends Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
8.8.2 PECS原则
“producer extends - consumer super” 这啥意思? (多态)
先举一个应用泛型上下边界的例子:
public static void getOutFruits(List<? extends Fruit> basket){
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
}
public static void main(String[] args) {
List<Fruit> fruitBasket = new ArrayList<>();
fruitBasket.add(new Fruit());
getOutFruits(fruitBasket);
List<Apple> appleBasket = new ArrayList<>();
appleBasket.add(new Apple());
getOutFruits(appleBasket); //编译正确
}
问题出现在下面的例子中:
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? extends Fruit> basket = apples; //按上一个例子,这个是可行的
// 一定是水果:多态
for (Fruit fruit : basket)
{
System.out.println(fruit);
}
//basket.add(new Apple()); //编译错误,苹果不是香蕉
//basket.add(new Fruit()); //编译错误,水果不是苹果
篮子里面放的是水果或者是具体的水果,因为这个不确定性,编译器不允许往这个篮子里添加任何东西。编译器可以确定的是,篮子里面是水果(多态),所以允许读操作。
无法添加新元素的原因,是对于List<? extends Number>类型来说,可能是 List<Number>、 List<Integer>或List<Double>等类型,无法确定新元素的类型与集合里的类型一致,所以编译器会提示报错。所以可以将List<? extends Number>类型的列表看作非严格意义上的只读列表。
所以,简单的说,当只想从集合中获取元素,请把这个集合看成生产者,请使用<? extends T>,这就是Producer extends原则,PECS原则中的PE部分。
上一个例子里,我们不能往篮子里加水果。现在换一个角度,我们要实现如何往篮子里加水果,而且是不同的水果。这将用到<? super T>通配符泛型。
首先我们扩展一下水果的继承关系,增加苹果的子类型RedApple:
public class Fruit {...}
public class Apple extends Fruit {...}
public class RedApple extends Apple {...}
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? super Apple> basket = apples; // 这里使用了super
basket.add(new Apple());
basket.add(new RedApple()); // 可以添加Apple及其子类,要添加只能添加子类,实现多态
//basket.add(new Fruit()); //编译错误
Object object = basket.get(0);//正确
//Fruit fruit = basket.get(0);//编译错误,需要强制类型转换
//Apple apple = basket.get(0);//编译错误,列表中存的可能是超类对象,向下转型可能会报错
//RedApple redApple = basket.get(0);//编译错误
篮子里面放的是苹果或者是其超类,因为编译不确定性里面放的是什么,所以不允许读操作。编译器可以确定的是,篮子里面是苹果(多态),所以允许把苹果或者红苹果放进篮子里。
无法读取列表的原因,是对于List<? super Number>类型来说,可能是List<Number>也可能是List<Object>类型,读取列表元素时不能确定元素类型。所以可以将List<? super Number>类型的列表看作只写列表。
因此,在上面的例子中的,我们将数据放进集合List<? super Apple> basket,所以这个篮子是实际上消费元素,例如Apple。简单的说,当你仅仅想增加元素到集合,把这个集合看成消费者,请使用<? super T>。这就是Consumer super原则,PECS原则中的CS部分。
参考:https://blog.51cto.com/flyingcat2013/1616068
总结PECS原则:
-
如果你只需要从集合中获得类型T(当然包括Object),使用<? extends T>通配符
-
如果你只需要将类型T(当然包括T的子类)放到集合中, 使用<? super T>通配符
-
如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple>
-
如果只读类型只用到 Object 的方法,即List<? extends Object>,可以用List<?>无界通配符
为什么要PECS原则:
List<Fruit>和List<Apple>之间没有任何继承关系。API的参数想要同时兼容两者,则只能使用PECS原则。这样做提升了API的灵活性。在java集合API中,大量使用了PECS原则,例如java.util.Collections中的集合复制的方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}