Java核心技术之泛型
2017-11-22 14:17
295 查看
泛型类(generic class)是带有一个或者多个类型形参(type parameter)的类。
泛型方法是带有类型形参的方法。
可以要求类型形参必须是一个或多个类型的子类型。
泛型类不是协变的(invariant):当S是T的子类型时,
通过使用通配类型形参(wildcards)
泛型类和泛型方法在编译时,其类型形参将被擦除(erase)。
由于擦除(erasure)机制的存在,泛型的使用有很多限制(restriction)
即便泛型类和方法在虚拟机中运行时已被擦除过了,你仍然可以知道它们是如何声明的。
例如,Manager是Employee的子类型,你只需要传递一个
现在假设你要处理一个ArrayList,问题就出现了:
Java语言施加这种限制是有道理的,如果可以将
Java里面使用了wildcards来指定这种允许变化的方法形参和返回值。
如:
仔细观察方法调用
一般的,当你需要指定一个泛型的函数式接口作为方法的形参时,你应该使用super wildcard。
一些程序员喜欢用”PECS”(producer extends,consumer super)来帮助记忆何时使用
类型形参
这样的话太过限制。假设Employee类实现了Comparable,然后Manager类继承Employee。这样Manager类实现了Comparable,而没有实现Comparable,那么Manager就不是Comparable的子类,但是它是Comparable
当然,上面的方法完全可以使用一个泛型方法来替代:
但是通配类型的方式更加容易理解。
这样的情况可以使用一个辅助方法来捕获这个通配类型,如下:
因此在编译泛型代码的时候,编译器会插入强制类型转换或者桥方法。
上面的getKey方法在经过擦除后返回是Object,因而编译器会生成类似如下的代码:
再考虑下面的代码片段:
调用ArrayList的add方法时,由于多态机制的存在,最终调用的方法是WordList类的add方法。然而多态能够正常工作,依赖于编译器合成的bridge method。因为ArrayList在经过擦除后,其add方法接受的参数是一个Object,因此WordList类的add方法并没有真正Override,所以多态机制是不能工作的。除非编译器在WordList类中生成一个桥方法如下:
当方法的返回类型变化时,桥方法依然有效,如下:
此时WordList类中有两个get方法,其中第二个是编译器合成的:
1. 泛型类不能使用基本类型的实参进行初始化
2. 在运行时,所有的类型都是原始类型
3. 不能实例化类型变量
4. 不能构造一个泛型类型的数组
5. 不能在static上下文中使用类型变量(因为静态域一个类中只有一份)
6. 不能定义擦除后会造成冲突的方法
7. 不能抛出泛型类对象的异常,也不能使用类型变量来捕获异常
1. Class类,用于描述具体的类型
2. TypeVariable接口,描述类型变量(如
3. WildcardType接口,描述通配类型(如
4. ParameterizedType接口,描述泛型类或者接口类型(如
5. GenericArrayType接口,描述泛型数组(如
sort方法中的
泛型方法是带有类型形参的方法。
可以要求类型形参必须是一个或多个类型的子类型。
泛型类不是协变的(invariant):当S是T的子类型时,
G<S>和
G<T>没有任何关系。
通过使用通配类型形参(wildcards)
G<? extends T>或者
G<? super T>,使得一个方法可以接受使用
T的子类或者超类实例化(instantiation)的泛型类型。
泛型类和泛型方法在编译时,其类型形参将被擦除(erase)。
由于擦除(erasure)机制的存在,泛型的使用有很多限制(restriction)
Class<T>是一个泛型类,这相当有用,因为
newInstance方法可以返回
T。
即便泛型类和方法在虚拟机中运行时已被擦除过了,你仍然可以知道它们是如何声明的。
泛型类
如ArrayList<T>是一个泛型类(generic class),
T被称为类型形参(type parameter)。泛型类如下定义:
public class Entry<K, V> { private K key; private V value; public Entry(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
泛型方法
定义如下:public class Arrays { public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } }
类型限定(bound)
如下,类型形参T必须要实现
Comparable和
Serializable接口:
class Interval<T extends Comparable & Serializable> implements Serializable { }
类型变化(variance)和通配(wildcards)
假设需要实现一个方法,用于处理Employee的子对象数组。只需要如下声明方法即可:public static void process(Employee[] staff) { … }
例如,Manager是Employee的子类型,你只需要传递一个
Manager[]给该方法即可。这种行为称之为协变(covariance),也就是说数组与其元素一同变化。
现在假设你要处理一个ArrayList,问题就出现了:
ArrayList<Manager>并不是
ArrayList<Employee>的子类型。
Java语言施加这种限制是有道理的,如果可以将
ArrayList<Manager>对象赋值给
ArrayList<Employee>类型的变量,那么会造成如下的问题:
ArrayList<Manager> bosses = new ArrayList<>(); ArrayList<Employee> empls = bosses; // Not legal, but suppose it is… empls.add(new Employee(…)); // A nonmanager in bosses!
Java里面使用了wildcards来指定这种允许变化的方法形参和返回值。
子类通配
如下:public static void printNames(ArrayList<? extends Employee> staff) { for (int i = 0; i < staff.size(); i++) { Employee e = staff.get(i); System.out.println(e.getName()); } }
父类通配
? super Employee这种wildcards通常被用于函数型对象。
如:
public static void printAll(Employee[] staff, Predicate<? super Employee> filter) { for (Employee e : staff) if (filter.test(e)) System.out.println(e.getName()); }
仔细观察方法调用
filter.test(e)。既然test方法可以处理Employee的父类型,那么将Employee类型的对象传递给它是安全的。这种情形是很普遍的。函数型对象天然就与其形参类型逆变(contravariant)。当一个函数能够处理employee对象时,那么提供一个能处理任意类型对象的函数当然是没问题的。
一般的,当你需要指定一个泛型的函数式接口作为方法的形参时,你应该使用super wildcard。
一些程序员喜欢用”PECS”(producer extends,consumer super)来帮助记忆何时使用
extends,何时使用
super。例如,一个从中读取数据的ArrayList是producer,而Predicate用于判断你提供的数据,那么就是consumer。
使用类型变量的通配
考虑Collections.sort方法的定义:
public static <T extends Comparable<? super T>> void sort(List<T> list)
类型形参
T指定了Comparable接口的compareTo方法的实参类型。那么为什么不直接定义成下面这样:
public static <T extends Comparable<T>> void sort(List<T> list)
这样的话太过限制。假设Employee类实现了Comparable,然后Manager类继承Employee。这样Manager类实现了Comparable,而没有实现Comparable,那么Manager就不是Comparable的子类,但是它是Comparable
未作限制的通配
如下:public static boolean hasNulls(ArrayList<?> elements) { for (Object e : elements) { if (e == null) return true; } return false; }
当然,上面的方法完全可以使用一个泛型方法来替代:
public static <T> boolean hasNulls(ArrayList<T> elements)
但是通配类型的方式更加容易理解。
通配捕获
通配类型(?)可以用作类型实参,但是不能用于声明变量,如下:public static void swap(ArrayList<?> elements, int i, int j) { ? temp = elements.get(i); // Won’t work elements.set(i, elements.get(j)); elements.set(j, temp); }
这样的情况可以使用一个辅助方法来捕获这个通配类型,如下:
public static void swap(ArrayList<?> elements, int i, int j) { swapHelper(elements, i, j); } private static <T> void swapHelper(ArrayList<T> elements, int i, int j) { T temp = elements.get(i); elements.set(i, elements.get(j)); elements.set(j, temp); }
JVM中的泛型
泛型对于虚拟机是不可见的,仅仅作用于编译阶段。编译后,类型变量被替换。没有限定的类型变量替换为Object,有限定的情况下被替换为第一个限定类型。因此在编译泛型代码的时候,编译器会插入强制类型转换或者桥方法。
插入类型转换
Entry<String, Integer> entry = …; String key = entry.getKey();
上面的getKey方法在经过擦除后返回是Object,因而编译器会生成类似如下的代码:
String key = (String) entry.getKey();
桥方法(bridge method)
当编译器在擦除方法的形参和返回类型时,有时需要合成bridge method。如下:public class WordList extends ArrayList<String> { public void add(String e) { return isBadWord(e) ? false : super.add(e); } … }
再考虑下面的代码片段:
WordList words = …; ArrayList<String> strings = words; // OK—conversion to superclass strings.add("C++");
调用ArrayList的add方法时,由于多态机制的存在,最终调用的方法是WordList类的add方法。然而多态能够正常工作,依赖于编译器合成的bridge method。因为ArrayList在经过擦除后,其add方法接受的参数是一个Object,因此WordList类的add方法并没有真正Override,所以多态机制是不能工作的。除非编译器在WordList类中生成一个桥方法如下:
public void add(Object e) { add((String) e); }
当方法的返回类型变化时,桥方法依然有效,如下:
public class WordList extends ArrayList<String> { public String get(int i) { return super.get(i).toLowerCase(); } … }
此时WordList类中有两个get方法,其中第二个是编译器合成的:
String get(int) // Defined in WordList Object get(int) // Overrides the method defined in ArrayList
泛型的一些限制
由于类型擦除机制的存在,在使用泛型类和方法时存在一些限制。1. 泛型类不能使用基本类型的实参进行初始化
2. 在运行时,所有的类型都是原始类型
3. 不能实例化类型变量
4. 不能构造一个泛型类型的数组
5. 不能在static上下文中使用类型变量(因为静态域一个类中只有一份)
6. 不能定义擦除后会造成冲突的方法
7. 不能抛出泛型类对象的异常,也不能使用类型变量来捕获异常
反射和泛型
Class类
虚拟机中的泛型信息
对于Collections类的方法static <T extends Comparable<? super T>> void sort(List<T> list)来说,可以这样通过反射得知整个方法签名。
Method m = Collections.class.getMethod("sort", List.class); TypeVariable<Method>[] vars = m.getTypeParameters(); String name = vars[0].getName(); // "T"
java.lang.reflect包中的Type接口用于表示泛型类型声明。该接口有如下子类型:
1. Class类,用于描述具体的类型
2. TypeVariable接口,描述类型变量(如
T extends Comparable<? super T>)
3. WildcardType接口,描述通配类型(如
? super T)
4. ParameterizedType接口,描述泛型类或者接口类型(如
Comparable<? super T>)
5. GenericArrayType接口,描述泛型数组(如
T[])
sort方法中的
T类型变量有一个bound,可以这样来处理:
Type[] bounds = vars[0].getBounds(); if (bounds[0] instanceof ParameterizedType) { // Comparable<? super T> ParameterizedType p = (ParameterizedType) bounds[0]; Type[] typeArguments = p.getActualTypeArguments(); if (typeArguments[0] instanceof WildcardType) { // ? super T WildcardType t = (WildCardType) typeArguments[0]; Type[] upper = t.getUpperBounds(); // ? extends … & … Type[] lower = t.getLowerBounds(); // ? super … & … if (lower.length > 0) { String description = lower[0].getTypeName(); // "T" … } } }
相关文章推荐
- 【Java核心技术——泛型】
- 【java核心技术卷一】泛型程序设计
- java核心技术之泛型
- 关于JAVA核心技术的泛型一章
- JAVA基础【8.1】《Java核心技术1》泛型程序设计-泛型
- java 泛型 java核心技术 读书笔记
- Java核心技术(五) —— 泛型程序设计(2)
- JAVA核心技术卷一,泛型例子
- JAVA 泛型总结(结合JAVA核心技术和Effective Java两书)
- [学习笔记] Java核心技术 卷一:基础知识 泛型程序设计(五)
- Java核心技术-01-谈谈泛型
- JAVA基础【5.4】《Java核心技术1》继承-泛型数组列表
- [JAVA]《Java 核心技术》(三)泛型、集合
- [JAVA]《Java 核心技术》(三)泛型、集合
- Java核心技术(五) —— 泛型程序设计(1)
- Java核心技术点之泛型
- Java核心技术点之泛型
- java核心技术----访问权限
- 小博老师解析Java核心技术 ——JDBC普通增删改操作
- Java开发核心技术面试心得分析