【阅读笔记】Java核心技术卷一 #6.Chapter8
8 泛型程序设计
8.1 为什么要使用泛型程序设计
- 类型参数(type parameters)(
E
、T
、S
...) - 通配符类型(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的更多相关文章
- 【阅读笔记】Java核心技术卷一 #0
这是一篇备忘性质的读书笔记,仅记录个人觉得有用的知识点 本文作为一个目录索引,部分章节跳过 吐槽:此书中文翻译有不少地方不太通顺,这种情况我要把英文版对应的部分也读一遍才能明白(说实话,英文里的从句表 ...
- java核心技术卷一
java核心技术卷一 java基础类型 整型 数据类型 字节数 取值范围 int 4 +_2^4*8-1 short 2 +_2^2*8-1 long 8 +_2^8*8-1 byte 1 -128- ...
- 对《Java核心技术卷一》读者的一些建议
<Java核心技术卷一>是唯一可以和<Java编程思想>媲美的一本 Java 入门书.单从技术的角度来看,前者更好一些.但上升到思想层面嘛,自然后者更好,两者的偏重点不同. 思 ...
- 读《java核心技术卷一》有感
过去一个多月了吧.才囫囵吞枣地把这书过了一遍.话说这书也够长的,一共706页.我从来不是个喜欢记录的人,一直以来看什么书都是看完了就扔一边去,可能有时候有那么一点想记录下来的冲动,但算算时间太紧,很多 ...
- 【阅读笔记】Java核心技术卷一 #5.Chapter7
7 异常.断言和日志 在 Java 中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法. 在这种情况下,将会立刻退出,并不返回任何值,而是抛出(throw)一个封装了错误 ...
- 【阅读笔记】Java核心技术卷一 #4.Chapter6
6 接口.lambda 表达式与内部类 6.1 接口 6.1.1 接口概念 接口绝不能含有实例域:但在接口中可以定义常量,被自动设为 public static final 接口中的所有方法自动地属于 ...
- 【阅读笔记】Java核心技术卷一 #3.Chapter5
5 继承 5.1 类.超类和子类 5.1.1 定义子类 超类(superclass)和子类(subclass), 基类(base class)和派生类(derived class), 父类(paren ...
- 【阅读笔记】Java核心技术卷一 #2.Chapter4
4 对象和类 4.1 面向对象程序设计概述(略) 4.2 使用预定义类 java.time.LocalDate static LocalDate now(); static LocalDate of( ...
- 【阅读笔记】Java核心技术卷一 #1.Chapter3
3 Java的基本程序设计结构 3.1 一个简单的 Java 应用程序(略) 3.2 注释(略) 3.3 数据类型 8种基本类型 byte,short,int,long float,double ch ...
随机推荐
- 一文带你.Net混合锁和lock语句
本文主要讲解.Net基于Monitor.Enter和lock实现互斥锁 Monitor.Enter实现 相比前面的锁来说,混合锁的性能更高,任何引用类型的对象都可以做为锁对象,不需要事先创建指定类型的 ...
- 利用ServletContext,实现Session动态权限变更
1.前言 很多Spring Boot应用使用了Session作为缓存,一般会在用户登录后保存用户的关键信息,如: 用户ID. 用户名. 用户token. 权限角色集合. 等等... 在管理员修改了用户 ...
- C# 位图BitArray 小试牛刀
前面聊了布隆过滤器,回归认识一下位图BitMap,阅读前文的同学应该发现了布隆过滤器本身就是基于位图,是位图的一种改进. 位图 先看一个问题, 假如有1千万个整数,整数范围在1到1亿之间,如何快速确定 ...
- Vue(11)组件化的基本使用
前言 有时候有一组html结构的代码,并且这个上面可能还绑定了事件.然后这段代码可能有多个地方都被使用到了,如果都是拷贝来拷贝去,很多代码都是重复的,包括事件部分的代码都是重复的.那么这时候我们就可以 ...
- python读取txt文件绘制散点图
方法和画折线图类似,差别在于画图函数不一样,用的是scatter() import matplotlib.pyplot as plt #以外部两个txt表分别作为x,y画图n=0m=0with ope ...
- linux设备驱动编写入门
linux设备驱动是什么,我个人的理解是liunx有用户态和内核态,用户空间中是不能直接对设备的外设进行使用而内核态中却可以,这时我们需要在内核空间中将需要的外设驱动起来供用户空间使用.linux的驱 ...
- Place the Robots 需要较强的建图能力
Place the Robots 思路:在任意一个点格子放机器人,那么它所在的行和列被控制了.我们对每一行或每一列连续的空地(草地忽视)称之为块,给每一行和每一列的块标号, 每一行的快与每一列的快相交 ...
- 浅读tomcat架构设计之Pipeline-Valve管道(4)
tomcat Container容器处理请求是使用Pipeline-Valve管道来处理的,后续写的tomcat内存马,和他紧密结合 Pipeline-Valve是责任链模式,责任链模式是指在一个请求 ...
- centos 安装启动配置Jenkins
一.Jenkins的安装 1.前提条件:已经成功安装了OPENJDK,因为jenkins是一款基于Java的持续集成工具. 安装OPENJDK的链接请参见我的另一篇博客: 安装连接:https://w ...
- Java核心基础第2篇-Java基本语法
Java基本语法 本章一起来探讨下Java的基本语法.主要从以下几个方面展开: Java关键字 Java标识符 Java变量 Java数据类型 Java运算符 学完本章内容之后,我们对Java会有更深 ...