@author:posper
@version:v 1.0
@date:2021/7/9-2021/7/10
ch 8 泛型程序设计
本文档为第二遍看 《Java 卷1》ch 8 整理,第一遍看的时候画的思维导图。
薄弱点:
- 8.6 泛型的限制与局限性
ch 8 泛型程序设计
8.1 为什么使用泛型?
- 背景:在泛型类推出之前都是使用 Objec[] 数组
- 即,维护一个 Objec[] 数组,可以向数组中添加任意类型的元素。
使用 Object[] 的缺点(2个)
- (1)获取一个值时必须进行强制类型转换
- (2)调用一个方法前必须使用 instanceof 判断对象类型
泛型概述
-
泛型在 JDK 1.5 引入
-
使用泛型类型时,编译器会进行检查,防止插入错误类型的对象
- 泛型只发生在编译阶段,与虚拟机无关
-
泛型的本质是参数化数据类型
泛型的好处
-
(1)类型安全
- 调用方法时更安全
-
(2)减少了强制类型转换的次数
- 获取数据值更方便
8.2 泛型类
泛型类概述
-
泛型类(generic class)就是有一个或多个类型变量(也叫,泛型标识)的类
-
类型变量(泛型标识):用于指定泛型类中方法的返回类型以及字段和局部变量的类型
-
常用的类型变量
- E:标识集合的元素类型
- K 和 V 分别表示表的键和值
- T 表示 “任意类型”
泛型类定义语法
-
泛型类的定义语法:class 类名<类型变量1,类型变量2, …>
public class Student<K, V> { K key; V value; ... }
泛型类的注意事项:(3 条)
- 1)泛型类 如果没有指定具体的数据类型,此时,操作类型是 0bject;
- 2)泛型的类型参数只能是类 类型,不能是基本数据类型;
- 3)泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型。
从泛型类派生子类
-
从泛型类派生子类 (2 种情况)
- 1、如果子类也是泛型类,子类和父类的泛型类型要保持一致
class ChildGeneric<T> extends Generic<T>
- 2、如果子类不是泛型类,父类要明确泛型的数据类型
Generic<E> { // 父类 ... } class ChildGeneric extends Generic<String> { ... } // 明确父类泛型类型
泛型接口
- 泛型接口的使用 (同上 从泛型类派生子类)
- 1、如果泛型接口的实现类也是泛型类,则实现类和接口的泛型类型要一致;
- 2、如果泛型接口的实现类不是泛型类,则泛型接口要明确数据类型。
8.3 泛型方法
-
泛型方法是方法首部带有类型变量<T, E…>的方法
- 注意:泛型类中使用了泛型的成员方法不是泛型方法
public static T setKey(T t) { // 不是泛型方法 ... }
-
泛型方法定义语法
- 语法:修饰符 <类型变量1, 类型变量2, …> 方法返回类型 方法名(参数列表 …)
public static <T> T setValue(T t) { // 泛型方法 ... }
-
泛型方法可以定义在泛型类中,也可以定义在普通类中
- 泛型类,是在实例化类的时候指定泛型的具体类型;
- 泛型方法,是在调用方法的时候指明泛型的具体类型
-
泛型方法的调用 (2 种方法)
-
(1)方法1:可以把具体类型包围在尖括号 <> 中,放在方法名的前面
- 通用,可以避免方法二义性冲突;但较为复杂。
<String>setValue("java"); // <具体类型>泛型方法
-
(2)方法2:大多情况下,可以省略 <具体类型>,由编译器推断出具体的方法
- 常用,大多情况都可以推断出方法类型;使用方便
setValue("java"); // 通常用法,省略 <具体类型>
-
8.4 类型变量的限定
-
引入类型限定的原因:有时,类和方法需要对类型变量(泛型标识)加以约束
- eg:只有实现了 Comparable 接口的类才具有 compareTo 方法,所以需要对类型变量 T 设置一个限定
public static <T extends Comparable> T min(T[] a) { T smallest = a[0]; for (int i = 1; i < a.length(); i++) { if (smallest.compareTo(a[i] > 0)) { // 只有实现了 Comparable 接口的类才具有 compareTo 方法 smallest = a[i]; } } return smallest; }
-
语法:
<T extends BoundingType>
-
一个变量可以有多个限定类型,限定类型用 ‘&’ 分隔
T extends Comparable & Serializable // T 类型可以是 Comparable 和 Serializable 实现类
-
为什么使用 extends 而不是 implements ?
- 表示 T 是限定类型(BoundingType)的子类型
- T 和限定类型可以是类,也可以是接口
- 选择 extends 是因为它更接近子类型的概念
8.5 泛型类型代码和虚拟机
类型擦除
-
泛型只在编译阶段有效,虚拟机运行的时候没有泛型类型对象(因为进行了类型擦除)
-
定义泛型类型后,会自动提供一个相应的 原始类型
- 即,原始类型就是将类型参数擦除后,替换为对应的具体类型(如下 2 种情况)
-
类型擦除的 2 种情况:
- (1)如果类型变量有限定类型,则原始类型用第一个限定来替换类型变量
// (1)类型变量有限定类型 public class Interval <T extends Comparable & Serializable〉implements Serializable { private T lower; private T upper; public Interval (T first , T second) { if (first .compareTo(second) <= 0) { lower = first ; upper = second; } else { lower = second; upper = first; } } } // 使用第一限定类型 Comparable 来替换类型变量 T(泛型标识) public class Interval implements Serializable { // 类型擦除后,得到原始类型 private Comparable lower; private Coiparable upper; public Interval (Coiparable first, Coiparable second) { ... } } // Note:如果泛型类改为如下形式 public class Interval <T extends Serializable & Comparable〉implements Serializable { ... } // 此时,进行类型擦除时,使用第一个限定类型 Serializable 来替换类型变量 T public class Interval implements Serializable { // 原始类型 private Serializable lower; private Serializable upper; ... }
- (2)如果类型变量没有限定类型,则将类型变量替换为 Object
// (2)没有限定类型的类型变量 public class Pair<T> { private T first; private T second; public T getFirst() { return first; } public T getSecond() { return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(T newValue) { second = newValue; } } // 类型擦除后,将所有类型变量 T 都替换为 Object public class Pair<Object> { // 原始类型 private Object first; private Object second; public Object getFirst() { return first; } public Object getSecond() { return second; } public void setFirst(Object newValue) { first = newValue; } public void setSecond(Object newValue) { second = newValue; } }
类型擦除泛型表达式
-
(1)调用泛型方法时,如果擦除了返回类型(如果没有限定类型,擦除后返回类型为 Object),编译器会自动插入强制类型转换;
Pair<Employee> buddies = ... // Pair 类同上 Employee buddy = buddies.getFirst(); // 调用泛型方法。同上,类型擦除后 getFirst 返回类型为 Object // 其实,编译器将以上调用泛型方法转换为两条虚拟机指令: // 1)调用原始方法 getFirst(); // 2)将返回的 Object 类型强转为 Employee 类型。 Employee buddy = (Employee) buddies.getFirst(); // 调用后编译器自动插入一个强转
-
(2)当访问一个泛型字段时,编译器也会插入强制类型转换(尽管将字段设置为 public 不是合理的,但是在 Java 中的合法的)
// 访问泛型字段 Employee buddy = buddies.first; // 通常字段都是 private 的(封装性),但是 public 也是合法的(尽量不用) // 编译器会在字节码中加入强制类型转换,如下: Employee buddy = (Employee) buddies.first; // 类型擦除后,first 字段为 Object 类型,访问时会强转
类型擦除泛型方法
-
Java 泛型会自动合成一个桥方法,来解决类型擦除与多态发生冲突的问题
- 桥方法是编译器在泛型父类的子类中自动生成的
- eg:Parent类中有个 set(T val) 方法,然后 Child extends Parent,且 Child 类中有个 set(String val)方法
- Parent 中 T 没有类型限定,所以Parent 类型擦除后会将其中 T 全替换为 Object,即 Parent.set(Object val)
- 但是,Child.set(String val),此时方法参数类型不同,理论上是不能进行方法覆盖的(没覆盖,则无法多态)。但是Java泛型会在 Child 类中自动合成一个桥方法 set(Object val) { setVal(String val) }; 这样就又可以多态了…
public class Parent<T> { // 泛型父类 ... public void test(T t) { // 类型擦除后,这里参数类型 T 变成 Object System.out.println("调用父类 test 方法"); } } // 具体子类 public class Child extends Parent<String> { // 泛型类继承时,子类不是泛型类,需要具体明确父类泛型类的泛型类型 ... public void test(String s) { // 这里不是重写,而是相当于在子类中建立了一个特有方法。 // 因为父类 test 方法进行类型擦除后,参数类型为 Object; // 而子类 test方法参数类型为 String,无法重写。 System.out.println("调用子类 test 方法"); } // 桥方法(类中没有显示给出,编译器自动合成的) @Override public void test(Object obj) { // 桥方法(但其实只在泛型方法中有,这里只是为了看清多态) test((String) obj); // 如果删除子类中的 test(String s) 方法,这里会死递归;但此时,不会死递归 System.out.println("不会死递归吧...."); } } public static void main(String[] args) { Parent parent = new Child(); // 多态,上转型对象 /** * 若不存在桥方法时(即父子类都不是泛型类),调用如下: */ parent.test(obj); // OK.调用父类 test parent.test("abc"); // OK.仍然调用父类 test,因为此时子类的 test(String s) 是子类特有方法 /** * 泛型类,存在桥方法: */ parent.test(obj); // error。抛出异常ClassCastException,这里只能接受String类型,因为桥方法里面有个String强转 parent.test("abc"); // OK。这里调用子类方法,利用桥方法实现了多态!!! }
- 桥方法是编译器在泛型父类的子类中自动生成的
注意:只要子类重写方法失败,则上转型对象调用父类方法(准确地说,调用子类从父类继承来的方法)。
Java 泛型转换的注意事项(4点)
-
虚拟机中没有泛型, 只有普通的类和方法
- 因为发生了类型擦除
- 泛型只发生在编译阶段
-
所有的类型参数都会替换为它们的限定类型
- 有具体类型则替换为具体类型;
- 否则,替换为第一个限定类型;
- 否则,替换为 Object。
-
桥方法被编译器合成来保持多态
-
为保持类型安全性,必要时插入强制类型转换
- 见 转换泛型表达式
- 泛型方法的返回类型,强转;
- 泛型字段,强转
- 见 转换泛型表达式
8.6 泛型的限制与局限性
大多数限制都是由类型擦除引起的
1、不能用基本类型实例化类型参数
- 没有Pair, 只有Pair
2、运行时类型查询只适用于原始类型
- 原因:
- if (a instanceof Pair) // Error
- Pair 和 Pair 调用 getClass 方法返回的都是 Pair 类型
3、不能创建参数化类型的数组
-
原因:类型擦除后,table 数组类型可能变成 Object[],此时看似可存放其他类型,但是存入其他类型就报错
Pair<String>[] table = new Pair<String>[10] ; // Error
-
只是不允许创建这些数组, 而声明类型为Pair[] 的变量仍是合法的。
- 不过不能用new Pair[10] 初始化这个变量
Pair<String>[] table; // ok
4、Varargs 警告
- 变参方法中,如果变参类型是泛型类型时,
5、不能实例化类型变置
public Pair<T> {
...
// 这种 Pair 构造器是非法的
public Pair() {
first = new T(); // ERROR
second = new T(); // ERROR
}
}
- 原因:类型擦除后 T 变成 Object,但是肯定不希望调用 new Object()
- 解决办法:
- 不能在类似 new T(…),new T[…] 或 T.class 这样的表达式中使用类型变量
6、不能构造泛型数组
- 原因:
7、泛型类的静态上下文中类型变量无效
- 原因:
- 不能在静态字段或静态方法中引用类型变量
8、不能抛出或捕获泛型类的实例
-
甚至泛型类扩展Throwable 都是不合法的
- public class Problem extends Exception { /* . . . */ } // Error can’t extend Throwable
-
catch 子句中不能使用类型变量
- try { … } catch (T e) // Error can’t catch type variable
9、可以消除对受查异常的检查
10、注意擦除后的冲突
8.7 泛型类型的继承规则
- 无论 S 与 T 有什么联系, Pair
与 Pair 之间没有任何联系(没有继承关系) - 使用这种规定的原因:保证类型安全
- 具体没细看,参考 《Java 核心卷1》第 11 版本 p346
8.8 通配符类型
什么是通配符?
- 通配符一般是使用 “?” 代替具体的类型实参
- 注意:通配符是类型实参,而不是类型形参
通配符上限(带有子类型限定的通配符)
-
语法:
类/接口<? extends 类型实参>
-
要求泛型类型,只能是实参类型,或者实参类型的子类型
-
允许读取一个泛型对象
- 能调用 getter,不能用 setter
- 因为 setter(? extends 类型实参),无法确定具体的参数类型,无法执行;
- 而 ? extends 类型实参 getter(),完全 ok,因为返回类型就算是 类型实参 也是合法的。
- 能调用 getter,不能用 setter
通配符下限(通配符的超类型限定)
无限定通配符
-
语法:
类/接口<?> { ... }
-
Pair<?> 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。
通配符捕获
-
通配符不是类型变量
- 因此,不能在编写代码中使用 “ ?” 作为一种类型
? t = p.getFirst(); // Error
-
带有通配符的方法不能使用 “?" 作为变量类型
- 解决方案为:构造一个辅助的泛型方法,然后让带有通配符 “?” 的方法调用辅助泛型方法
// 带有通配符的方法,不能使用 ? 作为类型变量 public static void swap(Pair<?> p) { // ? t = p.getFirst(); // Error swapHelper(p); // 调用辅助方法 } // 辅助方法 public static <T> void swapHelper (Pair<T> p) { T t = p.getFirst(); p.setFirst(p.getSecond()); p.setSecond(t); }
反射和泛型
- 第一遍暂时没看…
- 2021/7/10 第二遍仍然没看…