8 泛型程序设计

8.1 为什么要使用泛型程序设计

  • 类型参数(type parameters)(ETS...)
  • 通配符类型(wildcard type)(?

注意这两者用法用处并不同。

8.2 定义简单泛型类

public class Pair<T> {
private T first;
private T second; public Pair() { first = null; second = null; }
public Pair(T first, T second) { this.first = first; this.second = 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; }
}

8.3 泛型方法

类型变量放在修饰符的后面,返回类型的前面。

泛型方法可以定义在普通类中,也可以定义在泛型类中。

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。

8.4 类型变量的限定

可以通过对类型变量 T 设置限定(bound):

public static <T extends Comparable> T min(T[] a) {. . .}
  • 关键词是extends,T 和限定类型可以是类,也可以是接口。
  • 一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable;限定类型用“&”分隔,而逗号用来分隔类型变量。
  • 限定中至多有一个类,且必须是限定列表中的第一个。

8.5 泛型代码和虚拟机

虚拟机没有泛型类型对象 —— 所有对象都属于普通类。

8.5.1 类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型。

原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。为了提高效率,应该将标记接口(即没有方法的接口)放在边界列表的末尾。

public class Pair {
private Object first;
private Object second; public Pair() { first = null; second = null; }
public Pair(Object first, Object second) { this.first = first; this.second = 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; }
}

8.5.2 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

8.5.3 翻译泛型方法

如果类型擦除与多态发生了冲突,例如:

public class Datelnterval extends Pair<LocalDate> {
@Override
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
...
}

类型擦除之后,成为:

class DateInterval extends Pair {
public void setSecond(LocalDate second) { . . . }
. . .
}

但是 Pair 类型擦除之后的方法是

public void setSecond(Object second);

覆盖时方法参数类型不一致,这与多态产生了冲突。因此编译器会为 Datelnterval 生成桥方法(bridge method):

public void setSecond(Object second) { setSecond((LocalDate) second); }

如果为 get 方法生成桥方法,于是在 Datelnterval 类中,有两个 getSecond 方法:

LocalDate getSecond() // defined in DateInterval 中定义的
Object getSecond() // 桥方法,覆盖 Pair 中的方法,调用第一个方法

方法签名(不包含返回值)相同的两个方法是不合法的。

但是在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

第五章提到的协变返回类型也使用了桥方法

总之,需要记住有关 Java 泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插人强制类型转换。

8.5.4 调用遗留代码(略)

8.6 约束与局限性

8.6.1 不能用基本类型实例化类型参数

没有Pair<double>,只有Pair<Double>。因为类型擦除之后,Pair 会有 Object 域,而 Object 不能存储 double。

8.6.2 运行时类型查询只适用于原始类型

如:

if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> p = (Pair<String>) a; // Warning--can only test that a is a Pair

其实只能测试 a 是否是一个 Pair。为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。同样,getClass方法总是返回原始类型。因为虚拟机中的对象总有一个特定的非泛型类型。

8.6.3 不能创建参数化类型的数组

如:

Pair<String>[] table = new Pair<String>[10]; // Error
Object[] objarray = table;
objarray[0] = "Hello"; // Error--component type is Pair
objarray[0] = new Pair<Employee>(); // 可以通过数组存储检査

因此不允许创建(new)参数化类型的数组。但是声明是合法的,可以使用类型转换来初始化。

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

当然这样不安全。只有一种安全而有效的方法:ArrayList<Pair<String>>

8.6.4 Varargs 警告

参数个数可变的方法:

public static <T> void addAll(Collection<T> coll, T... ts)
{
for (t : ts) coll.add(t);
}
// 如下调用:
Collection<Pair<String>> table = . . .;
Pair<String> pair1 = . . .;
Pair<String> pair2 = . . .;
addAll(table, pair1, pair2);

为了调用这个方法虚拟机会创建Pair<String>的数组。不过此时只会得到一个警告,而不是错误。消除这个警告可以用注解@SuppressWarnings("unchecked")@SafeVarargs

8.6.5 不能实例化类型变置

不能使用new T(...)T.class这样的表达式。

想要创建实例可以调用者提供一个构造器表达式或提供class(然后使用Class.newlnstance方法)。

// 1.构造器表达式
Pair<String> p = Pair.makePair(String::new);
public static <T> Pair<T> makePair(Supplier<T> constr)
{
return new Pair<>(constr.get(), constr.get());
}
// 2. 提供class
Pair<String> p = Pair.makePair(String.class);
public static <T> Pair<T> makePair(Class<T> cl)
{
try { return new Pair<>(cl.newInstance(), cl.newInstance()); }
catch (Exception ex) { return null; }
}

8.6.6 不能构造泛型数组

不能new T[...]

  • 构造器表达式或者反射,详见P324
  • ArrayList.toArray的例子,详见P324

8.6.7 泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

public class Singleton<T> {
private static T singleInstance; // Error
public static T getSingleInstance() // Error
{
if (singleInstance == null) construct new instance of T
return singleInstance;
}
}

声明Singleton<Random>Singleton<JFileChooser>,然而类型擦除后只剩 Singleton 类,它只有一个 singleInstance 域。

8.6.8 不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展 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
Logger.global.info(...)
}
}

不过,在异常规范中使用类型变量是允许的。

public static <T extends Throwable> void doWork(T t) throws T { // OK
try {
do work
}
catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}

8.6.9 可以消除对受查异常的检查

8.6.10 注意擦除后的冲突

8.7 泛型类型的继承规则

  • 无论 S 与 T 有什么联系,通常,Pair<S>Pair<T>没有什么联系。

  • 永远可以将参数化类型转换为一个原始类型。但是会有风险,如:

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File(". . .")); // only a compile-time warning
Manager one = rawBuddies.getFirst(); // 抛出 ClassCastException 异常
  • 泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。

    例如,ArrayList<T>类实现List<T>接口。这意味着,一个ArrayList<Manager>可以被转换为一个List<Manager>。但是如前面所见,一个ArrayList<Manager>不是一个ArrayList<Employee>List<Employee>

8.8 通配符类型

8.8.1 通配符概念

通配符类型中,允许类型参数变化。例如:

Pair<? extends Employee>

它表示类型参数是 Employee 的子类任何泛型 Pair 类型。

Pair<Employee>Pair<Manager>都是Pair<? extends Employee>的子类型。

使用通配符会通过Pair<? extends Employee>的引用不会破坏Pair<Manager>

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error

因为,Pair<? extends Employee>的方法看起来是这样的:

? extends Employee getFirst()
void setFirst(? extends Employee)

这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道

具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。

使用 getFirst 就不存在这个问题:将 getFirst 的返回值赋给一个 Employee 的引用完全合法。

这就是引入有限定的通配符的关键之处:现在已经有办法区分安全的访问器方法和不安全的更改器方法了。

8.8.2 通配符的超类型限定

通配符限定类型变量限定十分类似,但是它还可以指定一个超类型限定(supertype bound)。

例如,Pair<? super Manager>有方法:

void setFirst(? super Manager)
? super Manager getFirst()

编译器无法知道 setFirst 方法的具体类型,因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。只能传递Manager 类型的对象,或者某个子类型对象。

另外,如果调用 getFirst,不能保证返回对象的类型,只能把它赋给一个 Object。

直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

8.8.3 无限定通配符

如:Pair<?>

? getFirst()
void setFirst(?)

getFirst 的返回值只能赋给一个 Object 。setFirst 方法不能被调用,甚至传入 Object 对象(但是可以调用setFirst(null))。但是原始类型 Pair 可以用任意 Object 对象传入其 setFirst 方法。

  • 用处在于一些简单操作的可读性更强。如检测一个 pair 是否包含一个 null 引用:
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}

8.8.4 通配符捕获

public static void swap(Pair<?> p) {
swapHelper(p);
} public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}

注意:swapHelper 是一个泛型方法,而 swap 不是,它具有固定的Pair<?>类型的参数。

在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道通配符表示哪种类型,但是是一个确定的类型。

8.9 反射和泛型

Class 类是泛型的。例如,String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。

被擦除的类仍然保留了一些关于它们的泛型起源的信息:

public static Comparable min(Comparable[] a)

是由泛型方法擦除的:

public static <T extends Comparable<? super T>> T min(T[] a)

可以使用反射 API 来确定:

  • 这个泛型方法有一个叫做 T 的类型参数。
  • 这个类型参数有一个子类型限定,其自身又是一个泛型类型。// Comparable<? super T>
  • 这个限定类型有一个通配符参数。// ? super T
  • 这个通配符参数有一个超类型限定。
  • 这个泛型方法有一个泛型数组参数。// T[] a

换句话说,你可以重建关于泛型类和方法的所有信息,就如他们的实现者声明的那样。但是你不会知道特定的对象或方法调用是如何处理类型参数的。

为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type。这个接口包含下列子类型:

  • Class类,描述具体类型。
  • Type Variable接口,描述类型变量(如T extends Comparable<? super T>)。
  • WildcardType接口,描述通配符(如?super T)。
  • ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)。
  • GenericArrayType接口,描述泛型数组(如T[])。

说实话,通配符及后面部分看的云里雾里的。。。。。

【阅读笔记】Java核心技术卷一 #6.Chapter8的更多相关文章

  1. 【阅读笔记】Java核心技术卷一 #0

    这是一篇备忘性质的读书笔记,仅记录个人觉得有用的知识点 本文作为一个目录索引,部分章节跳过 吐槽:此书中文翻译有不少地方不太通顺,这种情况我要把英文版对应的部分也读一遍才能明白(说实话,英文里的从句表 ...

  2. java核心技术卷一

    java核心技术卷一 java基础类型 整型 数据类型 字节数 取值范围 int 4 +_2^4*8-1 short 2 +_2^2*8-1 long 8 +_2^8*8-1 byte 1 -128- ...

  3. 对《Java核心技术卷一》读者的一些建议

    <Java核心技术卷一>是唯一可以和<Java编程思想>媲美的一本 Java 入门书.单从技术的角度来看,前者更好一些.但上升到思想层面嘛,自然后者更好,两者的偏重点不同. 思 ...

  4. 读《java核心技术卷一》有感

    过去一个多月了吧.才囫囵吞枣地把这书过了一遍.话说这书也够长的,一共706页.我从来不是个喜欢记录的人,一直以来看什么书都是看完了就扔一边去,可能有时候有那么一点想记录下来的冲动,但算算时间太紧,很多 ...

  5. 【阅读笔记】Java核心技术卷一 #5.Chapter7

    7 异常.断言和日志 在 Java 中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法. 在这种情况下,将会立刻退出,并不返回任何值,而是抛出(throw)一个封装了错误 ...

  6. 【阅读笔记】Java核心技术卷一 #4.Chapter6

    6 接口.lambda 表达式与内部类 6.1 接口 6.1.1 接口概念 接口绝不能含有实例域:但在接口中可以定义常量,被自动设为 public static final 接口中的所有方法自动地属于 ...

  7. 【阅读笔记】Java核心技术卷一 #3.Chapter5

    5 继承 5.1 类.超类和子类 5.1.1 定义子类 超类(superclass)和子类(subclass), 基类(base class)和派生类(derived class), 父类(paren ...

  8. 【阅读笔记】Java核心技术卷一 #2.Chapter4

    4 对象和类 4.1 面向对象程序设计概述(略) 4.2 使用预定义类 java.time.LocalDate static LocalDate now(); static LocalDate of( ...

  9. 【阅读笔记】Java核心技术卷一 #1.Chapter3

    3 Java的基本程序设计结构 3.1 一个简单的 Java 应用程序(略) 3.2 注释(略) 3.3 数据类型 8种基本类型 byte,short,int,long float,double ch ...

随机推荐

  1. Typora 配置码云图床

    目录 在码云创建一个项目作为自己床图 设置私人令牌 下载安装 PigGo Typora中设置图片上传选项 在码云创建一个项目作为自己床图 创建的项目必须为公开项目,创建的过程不细说了. 设置私人令牌 ...

  2. Reactive Spring实战 -- 响应式Kafka交互

    本文分享如何使用KRaft部署Kafka集群,以及Spring中如何实现Kafka响应式交互. KRaft 我们知道,Kafka使用Zookeeper负责为kafka存储broker,Consumer ...

  3. NUC980 运行 RT-Thread 时使用 GPIO

    如何使用 GPIO? NuMaker-RTU-NUC980 板子引出的 IO 有: 分别有一个 I2C1.GPIO.SPI0.UART4,RT-Thread 中 NuMaker-RTU-NUC980 ...

  4. R语言--读取文件(数据输入)

    1 数据的输入 1.1 键盘输入 首先新建一张空表: dat<-data.frame(age=numeric(0),gender=character(0),weight=numeric(0)) ...

  5. 《机器学习Python实现_10_10_集成学习_xgboost_原理介绍及回归树的简单实现》

    一.简介 xgboost在集成学习中占有重要的一席之位,通常在各大竞赛中作为杀器使用,同时它在工业落地上也很方便,目前针对大数据领域也有各种分布式实现版本,比如xgboost4j-spark,xgbo ...

  6. 36、mysql数据库(dml)

    36.1.表记录的增删改: 1.增加表记录: insert[into]tab_name (field1,filed2,.......) values (value1,value2,.......); ...

  7. 精尽Spring Boot源码分析 - Condition 接口的扩展

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  8. 深入理解Java多线程——线程池

    目录 为什么需要线程池 定义 ThreadPoolExecutor 工作队列workQueue 不同的线程池 Executor 线程池的工作原理 线程池生命周期 线程池增长策略 线程池大小的设置 线程 ...

  9. GKCTF X DASCTF 2021_babycat复现学习

    17解的一道题,涉及到了java反序列化的知识,学习了. 看了下积分榜,如果做出来可能能进前20了哈哈哈,加油吧,这次就搞了两个misc签到,菜的扣脚. 打开后是个登录框,sign up提示不让注册, ...

  10. I-Identical Day[题解]

    原题目地址(牛客) Identical Day 题目大意 给定一个长度为 \(n\) 的 \(01\) 串,对于每段长度为 \(l\) 的连续的 \(1\) ,其权值为 \(\frac{l\times ...