Java核心技术(一)
前言
《Java 核心技术》第 10 版增加了 Java 8 的相关内容,最新版为第 11 版。文章目录与书籍目录一致,仅以此作为书籍第 10 版学习的笔记,非零基础。
文章目录
- Java核心技术(一)
- 前言
- 第 1 章 Java 程序设计概述
- 1.1 Java 程序设计平台
- 1.2 Java “白皮书” 的关键术语
- 1.3 Java applet 与 Internet
- 1.4 Java 发展简史
- 1.5 关于 Java 的常见误解
- 第 2 章 Java 程序设计环境
- 2.1 安装 Java 开发工具包
- 2.2 使用命令行工具
- 2.3 使用集成开发环境
- 2.4 运行图形化应用程序
- 2.5 构建并运行 applet
- 第 3 章 Java 的基本程序设计结构
- 3.1 一个简单的 Java 应用程序
- 3.2 注释
- 3.3 数据类型
- 3.4 变量
- 3.5 运算符
- 3.6 字符串
- 3.7 输入输出
- 3.8 控制流程
- 3.9 大数值
- 3.10 数组
- 第 4 章 对象与类
- 4.1 面向对象程序设计概述
- 4.2 使用预定义类
- 4.3 用户自定义类
- 4.4 静态域与静态方法
- 4.5 方法参数
- 4.6 对象构造
- 4.7 包
- 4.8 类路径
- 4.9 文档注释
- 4.10 类设计技巧
- 第 5 章 继承
- 5.1 类、超类和子类
- 5.2 Object:所有类的超类
- 5.3 泛型数组列表
- 5.4 对象包装器与自动装箱
- 5.5 参数数量可变的方法
- 5.6 枚举类
- 5.7 反射
- 5.8 继承的设计技巧
- 第 6 章 接口、lambda 表达式与内部类
- 6.1 接口
- 6.2 接口示例
- 6.3 lambda 表达式
- 6.4 内部类
- 6.5 代理
- 第 7 章 异常、断言和日志
- 7.1 处理错误
- 7.2 捕获异常
- 7.3 使用异常机制的技巧
- 7.4 使用断言
- 7.5 记录日志
- 7.6 调试技巧
- 第 8 章 泛型程序设计
- 8.1 为什么要使用泛型程序设计
- 8.2 定义简单泛型类
- 8.3 泛型方法
- 8.4 类型变量的限定
- 8.5 泛型代码和虚拟机
- 8.6 约束与局限性
- 8.7 泛型类型的继承规则
- 8.8 通配符类型
- 8.9 反射和泛型
- 第 9 章 集合
- 9.1 Java 集合框架
- 9.2 具体的集合
- 9.3 映射
- 9.4 视图与包装器
- 9.5 算法
- 9.6 遗留的集合
- 第 10 章 图形程序设计
- 10.1 Swing 概述
- 10.2 创建框架
- 10.3 框架定位
- 10.4 在组件中显示信息
- 10.5 处理 2D 图形
- 10.6 使用颜色
- 10.7 文本使用特殊字体
- 10.8 显示图像
- 第 11 章 事件处理
- 11.1 事件处理基础
- 11.2 动作
- 11.3 鼠标事件
- 11.4 AWT 事件继承层次
- 第 12 章 Swing用户界面组件
- 12.1 Swing和模型-视图-控制器设计模式
- 12.2 布局管理概述
- 12.3 文本输入
- 12.4 选择组件
- 12.5 菜单
- 12.6 负责的布局管理
- 12.7 对话框
- 第 13 章 部署 Java 应用程序
- 13.1 JAR 文件
- 13.2 应用首选项的存储
- 13.3 服务加载器
- 13.4 applet
- 13.5 Java Web Start
- 第 14 章 并发
- 14.1 什么是线程
- 14.2 中断线程
- 14.3 线程状态
- 14.4 线程属性
- 14.5 同步
- 14.6 阻塞队列
- 14.7 线程安全的集合
- 14.8 Callable 与 Future
- 14.9 执行器
- 14.10 同步器
- 14.11 线程与 Swing
- 完
第 1 章 Java 程序设计概述
1.1 Java 程序设计平台
1.2 Java “白皮书” 的关键术语
1)简单性 2)面向对象 3)分布式 4)健壮性 5)安全性 6)体系结构中立 7)可移植性 8)解释性 9)高性能 10)多线程 11)动态性
1.3 Java applet 与 Internet
1.4 Java 发展简史
1.5 关于 Java 的常见误解
第 2 章 Java 程序设计环境
2.1 安装 Java 开发工具包
2.2 使用命令行工具
2.3 使用集成开发环境
Eclipse、NetBeans、IntelliJ IDEA
2.4 运行图形化应用程序
2.5 构建并运行 applet
第 3 章 Java 的基本程序设计结构
3.1 一个简单的 Java 应用程序
public class FirstSample {
public static void main(String[] args) {
System.out.println("I will not use 'Hello, World!'");
}
}
- Java区分大小写
- public 为访问修饰符
- class:类——加载程序逻辑的容器,程序逻辑定义了应用程序的行为
- 类命名规则及规范
C + + 注释 : 作为一名 C++ 程序员 , 一定知道类的概念 。 Java 的类与 C ++ 的类很相似 ,但还是有些差异会使人感到困惑 。例如 , Java 中的所有函数都属于某个类的方法 ( 标准
术语将其称为方法,而不是成员函数 )。 因此 , Java中的 main 方法必须有一个外壳类 。读者有可能对 C + + 中的静态成员函数 ( static member functions ) 十分熟悉 。 这些成员函数定义在类的内部 , 并且不对对象进行操作 。 Java 中的 main 方法必须是静态的 。 最后 ,与 C / C + + — 样,关键字 void 表示这个方法没有返回值, 所不同的是 main方法没有为操作系统返回 “ 退出代码 ” 。 如果 main 方法正常退出 , 那么 Java 应用程序的退出代码为 0 ,表示成功地运行了程序 。 如果希望在终止程序时返回其他的代码 , 那就需要调用System.exit
方法 。
3.2 注释
- //
- /* 注释内容 */
- /** 生成文档 */
警告 : 在 Java 中 , /* / 注释不能嵌套 „也就是说, 不能简单地把代码用 / 和 */ 括起来作为注释,因为这段代码本身可能也包含一个 */ 。
3.3 数据类型
- 整型
类型 | 存储需求 | 取值范围 |
int | 4字节 | -2147483648 ~ 2147483647(超20亿) |
short | 4字节 | -32768 ~ 32767 |
long | 8字节 | -9223372036854775808 ~ 9223372036854775807 |
byte | 1字节 | -128 ~ 127 |
C + + 注? : 在 C 和 C + + 中 , int 和 long 等类型的大小与目标平台相关 。 在 8086 这样的16 位处理器上整型数值占 2 字节 ; 不过, 在32 位处理器 ( 比如 Pentium 或 SPARC ) 上,整型数值则为 4 字节 。 类似地, 在 32 位处理器上 long 值为4 字节 , 在 64 位处理器上则为 8 字节 。 由于存在这些差别 , 这对编写跨平台程序带来了很大难度 。 在 Java 中, 所有的数值类型所占据的字节数量与平台无关。注意 , Java 没有任何无符号 ( unsigned ) 形式的 int、 long 、 short或 byte 类型 。
- 浮点类型
类型 | 存储需求 | 取值范围 |
float | 4字节 | 大约 ±3.40282347E + 38F(有效位数为6 ~ 7位) |
double | 8字节 | 大约 ±1.79769313486231570E + 308(有效位数为15位) |
警告 : 浮点数值不适用于无法接受舍入误差的金融计算中 。 例如 , 命令
System.out.println(2.0 - 1.1)
将打印出 0.8999999999999999 , 而不是人们想象的 0.9。 这种舍入误差的主要原因是浮点数值采用二进制系统表示 , 而在二进制系统中无法精确地表示分数 1 / 10。 这就好像十进制无法精确地表示分数 1 / 3—样。 如果在数值计算中不允许有任何舍入误差 ,就应该使用BigDecimal
类 , 本章稍后将介绍这个类 。
- char类型
转义序列\u
警告 : Unicode 转义序列会在解析代码之前得到处理 。 例如, " \ u 0022 + \ u 0022 ”并不是一个由引号 ( U + 0022 ) 包围加号构成的字符串 。 实际上 , \ u 0022 会在解析之前转换为 ", 这会 得 到 也 就 是 一 个 空 串 。
更隐秘地 , 一定要当心注释中的 \ u。 注释// \u 00A0 is a newline
会产生一个语法错误 , 因为读程序时 \u 00A0 会替换为一个换行符。类似地 , 下面这个注释// Look inside c:\users
也会产生一个语法错误 , 因为 \ u 后面并未跟着 4 个十六进制数 。
- Unicode和char类型
强烈建议不要在程序中使用 char 类型 , 除非确实需要处理 UTF - 16 代码单元。 最好将字符串作为抽象数据类型处理 ( 有关这方面的内容将在 3.6 节讨论 ) 。 - boolean类型
C++ 注释: 在 C++ 中 , 数值甚至指针可以代替 boolean 值 。 值 0 相当于布尔值 false ,非0 值相当于布尔值 true , 在 Java 中则不是这样 , , 因此, Java 程序员不会遇到下述麻烦 :
if (x = 0) // oops... meant x == 0
在 C + + 中这个测试可以编译运行,其结果总是 false : 而在 Java 中 , 这个测试将不能通过编译, 其原因是整数表达式 x = 0 不能转换为布尔值 。
3.4 变量
变量名不仅局限于 ’ A ’ ~ ’ Z ’ 、 ’ a ’ ~ ’ z ’ 、 ’ _ ’ 、 ’ $ ',如希腊用户可以使用 ’ π ‘。
提示 : 如果想要知道哪些 Unicode 字符属于 Java 中的 “ 字母 ”,可以使用
Character
类的isJavaldentifierStart
和isJavaldentifierPart
方法来检查 。
尽管 $ 是一个合法的 Java 字符 , 但不要在你自己的代码中使用这个字符 。 它只用在 Java 编译器或其他工具生成的名字中。
final 修饰的变量为常量,const是Java保留的关键字。
3.5 运算符
可移植性是 Java 语言的设计目标之一 , 无论在哪个虚拟机上运行 , 同一运算应该得到样的结果 3 对于浮点数的算术运算 , 实现这样的可移植性是相当困难的 。 double类型使用 64 位存储一个数值,而有些处理器使用 80 位浮点寄存器这些寄存器增加了中间过程的计算精度 . 例如,以下运算 :
double w = x * y / z ;
很多 Intel处理器计算 x * y, 并且将结果存储在80 位的寄存器中 , 再除以 z 并将结果截断为 64 位 。 这样可以得到一个更加精确的计算结果 , 并且还能够避免产生指数溢出 。 但是,这个结果可能与始终在 64 位机器上计算的结果不一样 。 因此, Java虚拟机的最初规范规定所有的中间计算都必须进行截断这种行为遭到了数值计算团体的反对 。截断计算不仅可能导致溢出 , 而且由于截断操作需要消耗时间 , 所以在计算速度上实际上要比精确计算慢 。 为此, Java程序设计语言承认了最优性能与理想结果之间存在的冲突, 并给予了改进 。 在默认情况下 ,虚拟机设计者允许对中间计算结果采用扩展的精度。但是,对于使用 strictfp关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。 例如 , 可以把 main 方法标记为public static strictfp void main(String [] args)
于是, 在main 方法中的所有指令都将使用严格的浮点计算。 如果将一个类标记为strictfp ,这个类中的所有方法都要使用严格的浮点计算。实际的计算方式将取决于 Intel 处理器的行为 。在默认情况下, 中间结果允许使用扩展的指数, 但不允许使用扩展的尾数 ( Intel 芯片在截断尾数时并不损失性能 ) 。 因此 , 这两种方式的区别仅仅在于采用默认的方式不会产生溢出 , 而采用严格的计算有可能产生溢出 。
StrictMath比Math类性能较差,但可以确保在所有平台上得到相同的结果。
数值类型之间的合法转换:
只有极少数情况才需要将布尔类型转换为数值类型,使用条件表达式b ? 1 : 0
。
3.6 字符串
每个用双引号括起来的字符串都是String类的一个实例,String类对象为不可变字符串。因为Java 的设计者认为共享带来的高效率远远胜过于提取 、 拼接字符串所带来的低效率。
使用equals或者equalsIgnoreCase检测字符串是否相等,禁用 == 运算符。
//initialize greeting to a string
String greeting = "Hello";
if (greeting == "Hello") {
// true
}
if (greeting.substring(0, 3) == "Hel") {
// false
}
字符串可以是空串(“”)或Nul。关于String类的API,强烈建议阅读源码。
JDK 5.0引入了StringBuilder类,前身是StringBuffer,区别自行搜索学习。
3.7 输入输出
Scanner类和Console类(Java SE 6引入)。
3.8 控制流程
警告 : 在循环中 , 检测两个浮点数是否相等需要格外小心 。 下面的 for 循环
for (double x = 0; x != 10; x += 0.1)...
可能永远不会结束。 由于舍入的误差 ,最终可能得不到精确值。例如, 在上面的循环中,因为 0.1 无法精确地用二进制表示,所以, x将从 9.999 999 999 999 98 跳到10.099 999999 999 98。
Java提供了一种带标签的break语句,用于跳出多重嵌套的循环语句。
Scanner in = new Scanner(System.in);
int n;
read_data:
// this loop statement is tagged with the label
while (...) {
...
// this inner loop is not labeled
for (...) {
System.out.print("Enter a number >= 0: ");
n = in.nextInt();
if (n < 0) {
// should never happen-can't go on
break read_data;
// break out of read_data loop
...
}
}
}
// this statement is executed immediately after the labeled break
if (n < 0) {
// check for bad situation
// deal with bad situation
} else {
// carry out normal processing
}
如果输入有误,通过执行带标签的break跳转到带标签的语句块末尾。事实上,可以将标签应用到任何语句中,甚至可以应用到 if 语句或者块语句中。
3.9 大数值
3.10 数组
Arrays类提供了sort方法,使用了优化的快排算法,其他排序算法自行搜索学习。
第 4 章 对象与类
4.1 面向对象程序设计概述
面向过程与面向对象的程序设计对比:
由类构造(construct)对象的过程称为创建类的实例(instance)。
对象的三个主要特性:
- 对象的行为(behavior)
- 对象的状态(state)
- 对象的标识(identity)
类之间的关系:
- 依赖(“uses-a”)
- 聚合(“has-a”)
- 继承(“is-a”)
4.2 使用预定义类
4.3 用户自定义类
Java对象都是在堆中构造的,构造器总是伴随着new操作符一起使用。
final 修饰符大都应用于基本 ( primitive ) 类型域, 或不可变 ( immutable )类的域 ( 如果类中的每个方法都不会改变其对象 , 这种类就是不可变的类。 例如 , String 类就是一个不可变的类 ) 。
4.4 静态域与静态方法
注释: 如果查看一下 System 类 , 就会发现有一个 setOut 方法 , 它可以将
System.out
设置为不同的流。 读者可能会感到奇怪 , 为什么这个方法可以修改 final 变量的值。 原因在于 ,setOut 方法是一个本地方法,而不是用 Java 语言实现的 。 本地方法可以绕过 Java 语言的存取控制机制 。 这是一种特殊的方法,在自己编写程序时 , 不应该这样处理 。
4.5 方法参数
两种类型:基本数据类型、对象引用。
4.6 对象构造
警告:请记住, 仅当类没有提供任何构造器的时候 ,系统才会提供一个默认的构造器如果在编写类的时候 , 给出了一个构造器 , 哪怕是很简单的 , 要想让这个类的用户能够采用下列方式构造实例 :
new ClassName()
就必须提供一个默认的构造器 ( 即不带参数的构造器 )。
初始值不一定是常量值。 在下面的例子中 , 可以调用方法对域进行初始化 。 仔细看一下Employee 类, 其中每个雇员有一个id 域。 可以使用下列方式进行初始化 :
class Employee {
private static int nextId;
private int id = assignId();
...
private static int assignId() {
return nextId++;
}
}
除了在构造器中设置值、在声明中赋值之外,Java还有第三种机制:初始化块(initialization block)。在一个类的声明中可以包含多个代码块。只要构造类的对象,这些块就会被执行。首先运行初始化块,然后才运行构造器的主体部分。例如:
class Employee {
private static int nextId;
private int id;
private String name;
private double salary;
// object initialization block
{
id = nextId++;
}
public Employee(String n, double s) {
name = n;
salary = s;
}
public Employee() {
name = "";
salary = 0;
}
...
}
下面是调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(0、false 或 null)。
- 按照在类声明中出现的次序,一次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
注释: 让人惊讶的是 , 在 JDK 6 之前 , 都可以用 Java 编写一个没有 main 方法的 “Hello ,World ” 程序 。
public class Hello { static { System.out.println("Hello, World"); } }
当用 java Hello 调用这个类时 , 就会加载这个类, 静态初始化块将会打印 “Hello ,World "。在此之后 , 会显示一个消息指出 main 未定义 。 从 Java SE 7 以后 , java 程序首先会检查是否有一个 main 方法 。
如果某个资源需要在使用完毕后立刻被关闭 , 那么就需要由人工来管理。 对象用完时 ,可以应用一个 close 方法来完成相应的清理操作。 7.2节会介绍如何确保这个方法自动得到调用。
4.7 包
从编译器的角度来看,嵌套的包之间没有任何关系。
静态导入
import 语句不仅可以导人类 , 还增加了导人静态方法和静态域的功能 。例如, 如果在源文件的顶部 , 添加一条指令 :import static java.lang.System.*;
就可以使用 System 类的静态方法和静态域, 而不必加类名前缀 :out.println("Goodbye, World!") ; // i.e., System.out
exit(0); // i.e., System.exit
另外, 还可以导入特定的方法或域 :import static java.lang.System.out;
实际上, 是否有更多的程序员采用 System . out或 System . exit 的简写形式, 似乎是一件值得怀疑的事情。 这种编写形式不利于代码的清晰度 。 不过 ,sqrt(pow(x, 2) + pow(y, 2))
看起来比Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
清晰得多。
java.awt包中部分源代码:
public class Window extends Container {
String warningString;
...
}
这里的warningString变量并不是private!这意味着java.awt包中所有类的方法都可以访问该变量,并将它设置为任意值。事实上,只有Window类的方法访问它,因此应该将它设置为私有变量。在Java程序设计语言的早期版本中,只需要将下列这条语句放在类文件的开头,就可以很容易地将其他类混入java.awt包中:
package java.awt;
使用这一手段可以对警告框内容 (warningString) 进行设置:
从 1.2 版开始, JDK 的实现者修改了类加载器 ,明确地禁止加载用户自定义的 、 包名以 “ java . ” 开始的类 ! 当然, 用户自定义的类无法从这种保护中受益 。 如果讲一个包密封起来,就不能再向这个包添加类了。第9章中将介绍制作包含密封包的JAR文件的方法。
4.8 类路径
4.9 文档注释
4.10 类设计技巧
- 一定要保证数据私有。这是最重要的:绝对不要破坏封装性。
- 一定要对数据初始化。Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。
- 不要再类中使用过多的基本类型。
- 不是所有的域都需要独立的域访问器和域更改器。
- 将职责过多的类进行分解。
- 类名和方法名要能够体现他们的职责。
- 优先使用不可变的类。
第 5 章 继承
5.1 类、超类和子类
在Java中,所有的继承(extends)都是公有继承,没有C++中的私有继承和保护继承。关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类成为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
注释 : 有些人认为 super 与 this 引用是类似的概念 , 实际上 , 这样比较并不太恰当 。 这是因为 super 不是一个对象的引用 , 不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
注释 : 回忆一下 , 关键字 this 有两个用途 : 一是引用隐式参数 , 二是调用该类其他的构造器。同样, super关键字也有两个用途 : 一是调用超类的方法, 二是调用超类的构造器 。在调用构造器的时候 , 这两个关键字的使用方式很相似 。 调用构造器的语句只能作为另一个构造器的第一条语句出现 。 构造参数既可以传递给本类 ( this ) 的其他构造器 , 也可以传递给超类 ( super ) 的构造器 。
Java不支持多继承。有关Java中多继承功能的实现方式,请参看下一章6.1节有关接口的讨论。
有一个用来判断是否应该设计为继承关系的简单规则,这就是 “is - a” 规则,它表明子类的每个对象也是超类的对象。
警告 : 在 Java 中, 子类数组的引用可以转换成超类数组的引用 , 而不需要采用强制类型转换。 例如 ,下面是一个经理数组
Manager[] managers = new Manager[10];
将它转换成 Employee[ ] 数组完全是合法的 :Employee[] staff = managers; // OK
这样做肯定不会有问题 , 请思考一下其中的缘由。 毕竟 ,如果 manager[i] 是一个 Manager, 也一定是一个 Employee。 然而 ,实际上, 将会发生一些令人惊讶的事情 。 要切 managers 和 staff 引用的是同一个数组。 现在看一下这条语句 :staff[0] = new Employee("Harry Hacker", ...);
编译器竟然接纳了这个赋值操作。 但在这里 , *staff[0]*与 manager[0] 引用的是同一个
对象 , 似乎我们把一个普通雇员擅自归入经理行列中了 。 这是一种很忌伟发生的情形,当调用managers[0].setBonus(1000)
的时候,将会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容。为了确保不发生这类错误 , 所有数组都要牢记创建它们的元素类型 , 并负责监督仅将类型兼容的引用存储到数组中 。 例如 , 使用 new managers[10] 创建的数组是一个经理数组。如果试图存储一个 Employee 类型的引用就会引发 ArrayStoreException 异常。
理解方法的调用:
- 编译器查看对象的声明类型和方法名。
- 接下来,编译器将查看调用方法时提供的参数类型。
- 如果是 private 方法、static 方法、final 方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String) ,就直接调用它;否则,将在 D 类的超类中寻找 f(String) ,以此类推。
关于强制类型转换:
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用 instanceof 进行检查。
5.2 Object:所有类的超类
Java语言规范要求 equals 方法具有下面的特性:
- 自反性:对于任何非空引用 x,x.equals(x) 返回 true。
- 对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true。
- 传递性:对于任何引用 x、y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
- 一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
- 对于任意非空引用 x,x.equals(null) 应该返回 false。
散列码(hash code)是由对象导出的一个整数值。散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同。由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
5.3 泛型数组列表
java.util.ArrayList<E>
5.4 对象包装器与自动装箱
有时,需要将 int 这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int。通常,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void 和 Boolean(前 6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是 final,因此不能定义它们的子类。
警告:由于每个值分别包装在对象中,所以 ArrayList<lnteger> 的效率远远低于 int[ ] 数组。 因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
有些人认为包装器类可以用来实现修改数值参数的方法,然而这是错误的。在第 4 章中曾经讲到,由于 Java 方法都是值传递,所以不可能编写一个下面这样的能够增加整型参数值的 Java 方法。
public static void triple (int x) {
// modifies local variable
x = 3 * x;
}
将 int 替换成 Integer 又会怎样呢 ?
public static void triple (Integer x) {
// won't work
...
}
问题是 Integer 对象是不可变的:包含在包装器中的内容不会改变:不能使用这些包装器类创建修改数值参数的方法。如果想编写一个修改数值参数值的方法,就需要使用在 org.omg.CORBA 包中定义的持有者(holder)类型,包括 IntHolder、 BooleanHolder 等。每个持有者类型都包含一个公有(!)域值,通过它可以访问存储在其中的值。
public static void triple (IntHolder x) {
x.value = 3 * x.value ;
}
5.5 参数数量可变的方法
public static void main(String... args)
5.6 枚举类
在比较两个枚举类型的值时 , 永远不需要调用 equals , 而直接使用 “==” 就可以了。所有的枚举类型都是Enum类的子类。其中 toString() 方法返回枚举常量名,其逆方法是静态方法 valueOf()。
5.7 反射
反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动
态操纵 Java 代码的程序。能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:
- 在运行时分析类的能力。
- 在运行时查看对象,例如,编写一个 toString 方法供所有类使用。
- 实现通用的数组操作代码。
- 利用 Method 对象,这个对象很像 C++ 中函数的指针。
反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。如果仅对设计应用程序感兴趣,而对构造工具不感兴趣,可以跳过本章的剩余部分,稍后再返回来学习。
Object.getClass() 方法、Class.getName() 方法、Class.forName(String className)方法
Class 类实际上是一个泛型类。例如,Employee.class 的类型是 Class<Employee>。
将 forName 与 newInstance 配合起来使用,可以根据存储在字符串中的类名创建一个对象。
String s = "java.util.Random";
Object m = Class.forName(s).new Instance();
newInstance 方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。使用 try - catch 捕获异常,详情见第7章。
下面简要地介绍一下反射机制最重要的内容 —— 检查类的结构 。
在 java.lang.reflect 包中有三个类 Field、Method 和 Constructor,分别用于描述类的域、方法和构造器 。 这三个类都有一个叫做 getName 的方法 , 用来返回项目的名称。Held 类有一个 getType 方法,用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。 这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。 另外 , 还可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifiers 返回的整型数值。例如,可以使用 Modifier 类中的 isPublic、isPrivate 或 isFinal 判断方法或构造器是否是 public、private 或 final。我们需要做的全部工作就是调用Modifier 类的相应方法,并对返回的整型数值进行分析,另外,还可以利用 Modifier.toString 方法将修饰符打印出来。
Class 类中的 getFields、getMethods 和 getConstructors 方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
下面程序显示如何打印一个类的全部信息:
package reflection;
import java.util.*;
import java.lang.reflect.*;
/**
* This program uses reflection to print all features of a cl ass .
* @version 1.1 2004-02-21
* @author Cay Horstmann
*/
public class ReflectionTest {
public static void main(String[] args) {
// read class name from command line args or user input
String name;
if (args.length > 0) {
name = args[0];
} else {
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (e.g. java.util.Date):");
name = in.next();
}
try {
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) {
System.out.print(" extends " + supercl.getName());
}
System.out.print(" {\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
/**
* Prints all constructors of a class
* @param cl a class
*/
private static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
System.out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
private static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Print all fields of a class
* @param cl a class
*/
private static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f : fields) {
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.println(type.getName() + " " + name + ";");
}
}
}
输入:java.lang.Double
输出:
public final class java.lang.Double extends java.lang.Number {
java.lang.Double(double);
java.lang.Double(java.lang.String);
public boolean equals(java.lang.Object);
public static java.lang.String toString(double);
public java.lang.String toString();
public int hashCode();
public static int hashCode(double);
public static double min(double, double);
public static double max(double, double);
public static native long doubleToRawLongBits(double);
public static long doubleToLongBits(double);
public static native double longBitsToDouble(long);
public volatile int compareTo(java.lang.Object);
public int compareTo(java.lang.Double);
public byte byteValue();
public short shortValue();
public int intValue();
public long longValue();
public float floatValue();
public double doubleValue();
public static java.lang.Double valueOf(java.lang.String);
public static java.lang.Double valueOf(double);
public static java.lang.String toHexString(double);
public static int compare(double, double);
public static boolean isNaN(double);
public boolean isNaN();
public static boolean isFinite(double);
public static boolean isInfinite(double);
public boolean isInfinite();
public static double sum(double, double);
public static double parseDouble(java.lang.String);
public static final double POSITIVE_INFINITY;
public static final double NEGATIVE_INFINITY;
public static final double NaN;
public static final double MAX_VALUE;
public static final double MIN_NORMAL;
public static final double MIN_VALUE;
public static final int MAX_EXPONENT;
public static final int MIN_EXPONENT;
public static final int SIZE;
public static final int BYTES;
public static final java.lang.Class TYPE;
private final double value;
private static final long serialVersionUID;
}
查看对象域的关键方法是 Field 类中的 get 方法。 如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象),obj 是某个包含 f 域的类的对象, f.get(obj) 将返回一个对象, 其值为 obj 域的当前值 。示例:
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
// the class object representing Employee
Class cl = harry.getClass();
// the name field of the Employee class
Field f = cl.getDeclaredField("name");
// the value of the name field of the harry object, i.e., the String object "Harry Hacker"
Object v = f.get(harry);
事实上,这段代码存在一个问题。由于 name 是一个私有域,所以 get 方法将会抛出一个 IllegalAccessException ,只有利用 get 方法才能得到可访问的域值。除非拥有访问权限,否则 Java 安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。
反射机制的默认行为受限于 Java 的访问控制。然而,如果一个 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method 或 Constructor 对象的 setAccessible 方法。 例如:f.setAccessible(true); // now OK to call f.get(harry);
可以使用 toString 方法查看任意对象的内部信息,还可以使用通用的 toString 方法实现自己类中的 toString 方法,如下所示:
public String toString() {
return new ObjectAnalyzer().toString(this);
}
这是一种公认的提供 toString 方法的手段,在编写程序时会发现,它是非常有用的。
使用反射编写泛型数组代码:
java.lang.reflect 包中的 Array 类允许动态地创建数组。例如,将这个特性应用到 Array 类中的 copyOf 方法实现中,应该记得这个方法可以用于扩展已经填满的数组 。
Employee[] a = new Employee[100];// array is fulla = Arrays.copyOf(a, 2 * a.length);
如何编写这样一个通用的方法呢?正好能够将 Employee[ ] 数组转换为 Object[ ] 数组,这让人感觉很有希望。下面进行第一次尝试。
public static Object[] badCopyOf(Object[] a, int newLength) {// not usefulObject[] newArray = new Object[newlength];System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
return newArray ;
}
然而,在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组(Object[ ])类型,这是由于使用下面这行代码创建的数组 :
new Object[newLength]
一个对象数组不能转换成雇员数组 (Employee[ ])。如果这样做,则在运行时 Java 将会产生 ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型,即创建数组时 new 表达式中使用的元素类型。**将一个 Employee[ ] 临时地转换成 Object[ ] 数组 , 然后再把它转换回来是可以的, 但一从开始就是 Object[ ] 的数组却永远不能转换成 Employee[ ] 数组。**为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。为此,需要 java.lang.reflect 包中 Array 类的一些方法。其中最关键的是 Array 类中的静态方法 newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。Object newArray = Array.newlnstance(componentType, newLength);
为了能够实际地运行,需要获得新数组的长度和元素类型。
可以通过调用 Array.getLength(a) 获得数组的长度,也可以通过 Array 类的静态 getLength 方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作 :
- 首先获得 a 数组的类对象。
- 确认它是一个数组
- 使用 Class 类(只能定义表示数组的类对象)的 getComponentType 方法确定数组对应的类型。
为什么 getLength 是 Array 的方法,而 getComponentType 是 Class 的方法呢?我们也不清楚。反射方法的分类有时确实显得有点古怪。下面是这段代码:
public static Object goodCopyOf(Object a, int newLength) { Class cl = a.getClass(); ; if (!cl.isArray()) { return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newlnstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray ;
}
请注意,这个 CopyOf 方法可以用来扩展任意类型的数组,而不仅是对象数组。
int[] a = {1, 2, 3, 4, 5};a = (int[]) goodCopyOf(a, 10);
为了能够实现上述操作,应该将 goodCopyOf 的参数声明为 Object 类型,而不要声明为对象型数组 (Object[ ])。整型数组类型 int[ ] 可以被转换成 Object,但不能转换成对象数组。
建议 Java 开发者不要使用 Method 对象中的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。
5.8 继承的设计技巧
在本章最后,给出一些对设计继承关系很有帮助的建议。
- 将公共操作和域放在超类
- 不要使用受保护的域
protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。 - 使用继承实现“is-a”关系
- 除非所有的继承方法都有意义,否则不要使用继承
- 在覆盖方法是,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多的使用反射
反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。
第 6 章 接口、lambda 表达式与内部类
从本章起,开始介绍常用的高级技术。
6.1 接口
接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。接口中的方法自动地被设置为 public,接口中的域自动地被设置为 public static final。
有些接口只定义了常量,而没有定义方法。例如,在标准库中有一个 SwingConstants 就是这样一个接口,其中只包含 NORTH、SOUTH 和 HORIZONTAL 等常量。任何实现 SwingConstants 接口的类都自动地继承了这些常量,并可以在方法中直接地引用 NORTH 等而不必采 SwingConstants.NORTH 这样的繁琐书写形式。然而,这样应用接口似乎有点偏离了接口概念的初衷,最好不要这样使用它。
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。
可以为接口方法提供一个默认实现,必须用 default 修饰符标记这样一个方法。
public interface Comparable<T> {
default int compareTo(T other) {
return 0;
}
// By default, all elements are the same
}
默认方法可以不用实现。
假如默认方法冲突,如在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,规则如下:
- 超类优先。
- 接口冲突:必须覆盖这个方法来解决冲突。
因此,一定不要让一个默认方法重新定义 Object 类中的某个方法。
6.2 接口示例
6.3 lambda 表达式
lambda 表达式即带参数变量的表达式。
- 一般形式:参数,箭头(->)以及一个表达式。
- 函数式接口:同一般形式。
- 方法引用:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
- 构造器引用
使用 lambda 表达式的重点是延迟执行(deferred execution)。关于 lambda 表达式,实践出真知。
6.4 内部类
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 当想要定义一个回调函数且不想编写大量代码时,使用*匿名(anonymous)*内部类比较便捷。
内部类对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。由于内部类拥有访问特权,所以与常规类比较起来功能更加强大。但是存在一定的安全风险。
总而言之, 如果内部类访问了私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但做这些事情需要高超的技巧和极大的决心。程序员不可能无意之中就获得对类的访问权限,而必须刻意地构建或修改类文件才有可能达到这个目的。
局部内部类不能用 public 或 private 访问说明符进行生命。它的作用域被限定在生命这个局部类的块中。局部类可以对外部世界完全地隐藏起来。
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须事实上为 final —— 它们一旦赋值就绝不会改变。(在 Java 8 之前,必须显式的声明 final。)
下面的技巧成为“双括号初始化”(double brace initialization),这里利用了内部类的语法。假设你想构造一个数组列表,并将它传递到一个方法:
ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);
如果不再需要这个数组列表,最好让它作为一个匿名列表。如下:
invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});
注意这里的双括号。外层括号建立了 ArrayList 的一个匿名子类。内层括号则是一个对象构造块。
在内部类不需要访问外围类对象的时候,应该使用静态内部类。有些程序员用嵌套类(nested class)表示静态内部类。与常规内部类不同,静态内部类可以有静态域和方法。声明在接口中的内部类自动成为 static 和 public 类。
6.5 代理
想要创建一个代理对象,需要使用 Proxy 类的 newProxyInstance 方法。这个方法有三个参数:
- 一个类加载器(class loader)。作为 Java 安全模型的一部分,对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。有关类加载器的详细内容将在卷 II 第 9 章中讨论。目前,用 null 表示使用默认的类加载器。
- 一个 Class 对象数组,每个元素都是需要实现的接口。
- 一个调用处理器。
代理类是在程序运行过程中创建的。一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有代理类都扩展于 Proxy 类。一个代理类只有一个实例域——调用处理器,它定义在 Proxy 的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
所有的代理类都覆盖了 Object 类中的方法 toString、equals、 和 hashCode。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的 invoke。Object 类中的其他方法(如 clone 和 getClass)没有被重新定义。
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;
/**
* This program demonstrates the use of proxies
*
* @author Cay Horstmann
* @version 1.00 2000-04-13
*/
public class ProxyTest {
/**
* An invocation handler that prints out the method name and parameters,
* then invokes the original method
*/
static class TraceHandler implements InvocationHandler {
private Object target;
/**
* Constructs a TraceHandler
*/
TraceHandler(Object t) {
target = t;
}
@Override
public Object invoke(Object proxy, Method m, Object[] args) {
// print implicit argument
System.out.print(target);
// print method name
System.out.print("." + m.getName() + "(");
// print explicit argument
if (args != null) {
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if (i < args.length - 1) {
System.out.print(", ");
}
}
}
System.out.println(")");
Object object = null;
try {
// invoke actual method
object = m.invoke(target, args);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return object;
}
}
public static void main(String[] args) {
Object[] elements = new Object[1000];
// fill elements with proxies for the integers 1 ... 1000
for (int i = 0; i < elements.length; i++) {
Integer value = i + 1;
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null, new Class[]{Comparable.class}, handler);
elements[i] = proxy;
}
// construct a random integer
Integer key = new Random().nextInt(elements.length) + 1;
// search for the key
int result = Arrays.binarySearch(elements, key);
//print match if found
if (result >= 0) {
System.out.println(elements[result]);
}
}
}
运行一次的输出:
500.compareTo(662)
750.compareTo(662)
625.compareTo(662)
687.compareTo(662)
656.compareTo(662)
671.compareTo(662)
663.compareTo(662)
659.compareTo(662)
661.compareTo(662)
662.compareTo(662)
662.toString()
662
注意,即使不属于 Comparable 接口,toString 方法也被代理。
第 7 章 异常、断言和日志
7.1 处理错误
- 用户输入错误
- 设备错误
- 物理限值
- 代码错误
在 Java 程序设计语言中,异常对象都是派生于 Throwable 类的一个实例。下图是 Java 异常层次结构的一个简化示意图。
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。这种情况很少出现。
在设计 Java 程序时,需要关注 Exception 层次的结构。这个层次结构又分解为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
派生于 RuntimeException 的异常包含下面几种情况:
- 错误的类型转换。
- 数组访问越界。
- 访问 null 指针。
不是派生于 RuntimeException 的异常包括: - 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在。
“如果出现 RuntimeException 异常,那么就一定是你的问题”是一条相当有道理的规则。
在遇到下面 4 种情况时应该抛出异常:
- 调用一个抛出受查异常的方法,例如,FileInputStream 构造器。
- 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常。
- 程序出现错误,例如,a[-1] = 0 会抛出一个 ArrayIndexOutOfBoundsException 这样的非受查异常。
- Java 虚拟机和运行时库出现的内部错误。
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
此外,异常类可自己创建,定义一个派生于 Exception 的类,或者派生于 Exception 子类的类。
7.2 捕获异常
使用 try-catch(-finally)、try-finally 语句块。try 语句可以只有 finally 子句,而没有 catch 子句。
警告:
当 finally 子句包含 return 语句时,将会出现一种意想不到的结果。假设利用 return 语句从 try 语句块中退出。在方法返回前,finally 子句的内容将被执行。如果 finally 子句中也有一个 return 语句,这个返回值将会覆盖原始的返回值。请看一个复杂的例子:public static int f(int n) { try { int r = n * n; return r; } finally { if (n = 2) { return 0; } } }
如果调用 f(2) 那么 try 语句块的计算结果为 r = 4,并执行 return 语句。然而,在方法真正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0,这个返回值覆盖了原始的返回值 4。
7.3 使用异常机制的技巧
- 异常处理不能代替简单的测试
- 不要过分的细化异常
- 利用异常层次结构
- 不要压制异常
- 在检测错误时,“苛刻”要比放任更好
- 不要羞于传递异常
7.4 使用断言
用于开发、测试阶段。
7.5 记录日志
7.6 调试技巧
- 可以用下面的方法打印或记录任意变量的值:
System.out.println("x=" + x);
或Logger.getGlobal().info("x=" + x);
- 在每一个类中放置一个单独的 main 方法,这样就可以对每一个类进行单元测试。在运行 applet 应用程序的时候,这些 main 方法不会被调用,而在运行应用程序的时候,Java 虚拟机只调用启动类的 main 方法。
- JUnit
- 日志代理
- 利用 printStackTrace() 打印堆栈轨迹
- 一般来说,堆栈轨迹显示在 System.err 上。也可以利用 printStackTrace(PrintWriter s) 方法将它发送到一个文件中。也可以采用下面的方式,将它捕获到一个字符串中:
StringWriter out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();
- 通常,将一个程序中的错误信息保存在一个文件中是非常有用的。然而,错误信息被发送到 System.err 中,而不是 System.out 中。因此,不能够通过运行下面的语句获取它们:
java MyProgram > errors.txt
而是采用下面的方式捕获错误流:java MyProgram 2> errors.txt
要想在同一个文件中同时捕获 System.err 和 System.out,需要使用下面这条命令:java MyProgram 1> errors.txt 2>&1
这条命令将工作在 bash 和 Windows shell 中。 - 让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法。
- 要想观察类的加载过程,可以用 -verbose 标志启动 Java 虚拟机。有时候,这种方法有助于诊断由于类路径引发的问题。
- -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査。下面列出了可以使用的选项:
选项 | 解释 |
-Xlint 或 -Xlint:all | 执行所有的检查 |
-Xlint:deprecation | 与 -deprecation 一样,检查废弃的方法 |
-Xlint:fallthrough | 检查 switch 语句中是否缺少 break 语句 |
-Xlint:finally | 警告 finally 子句不能正常的执行 |
-Xlint:none | 不执行任何检查 |
-Xlint:path | 检查类路径和源代码路径上的所有目录是否存在 |
-Xlint:serial | 警告没有 serialVersionUID 的串行化类 |
-Xlint:unchecked | 对通用类型与原始类型之间的危险转换给予警告 |
- jconsole:Java 虚拟机增加了对 Java 应用程序进行监控(monitoring)和管理(management)的支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。
- 可以使用 jmap 实用工具获得一个堆的转储,其中显示了堆中的每个对象。
- 如果使用 -Xprof 标志运行 Java 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给 System.out。输出结果中还会显示哪些方法是由即时编译器编译的。
第 8 章 泛型程序设计
8.1 为什么要使用泛型程序设计
8.2 定义简单泛型类
泛型类可以看做普通类的工厂。
8.3 泛型方法
8.4 类型变量的限定
可为泛型的类型变量 T 设置限定,如:public static<T extends Comparable> T min(T[] a) ...
一个类型变量或通配符可以有多个限定,如:T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
8.5 泛型代码和虚拟机
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
8.6 约束与局限性
- 不能用基本类型实例化类型参数
- 运行时类型查询只适用于原始类型
- 不能创建参数化类型的数组
- Varargs 警告
- 不能实例化类型变量
- 不能构造泛型数组
- 泛型类的静态上下文中类型变量无效
- 不能抛出或捕获泛型类的实例
- 可以消除对受查异常的检查
- 注意擦除后的冲突
8.7 泛型类型的继承规则
无论 S 和 T 有什么联系,通常,Pair<S> 和 Pair<T> 没有什么联系。
8.8 通配符类型
在之前的例子中,由于 Comparable 也是个泛型类,因此完整写法为:public static<T extends Comparable<T>> T min(T[] a) ...
现在可以写成:public static<T extends Comparable<? super T>> T min(T[] a) ...
保证传递一个 T 类型的对象给 compareTo 方法都是安全的。
注意:通配符不是类型变量,不能再编写代码中使用“?”作为一种类型。
8.9 反射和泛型
反射允许在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为他们会被擦除。
方法public static Comparable min(Comparable[] a)
是一个泛型方法的擦除public static<T extends Comparable<? super T>> T min(T[] a)
可以使用反射 API 来确定:
- 这个泛型方法有一个叫做 T 的类型参数。
- 这个类型参数有一个子类型限定,其自身又是一个泛型类型。
- 这个限定类型有一个通配符参数。
- 这个通配符参数有一个超类型限定。
- 这个泛型方法有一个泛型数组参数。
为了表达泛型类型声明,使用 java.lang.reflect
包中提供的接口 Type。这个借口包含下列子类型:
- Class 类,描述具体类型。
- TypeVariable 接口,描述类型变量(如 T extends Comparable<? super T>)。
- WildcardType 接口,描述通配符(如 ? super T)。
- ParameterizedType 接口,描述泛型类或接口类型(如 Comparable<? super T>)。
- GenericArrayType 接口,描述泛型数组(如 T[])。
第 9 章 集合
本章将跳过数据结构理论部分,仅介绍如何使用标准库中的集合类。
9.1 Java 集合框架
Java 集合类库将*接口(interface)与实现(implementation)*分离。
如队列,通常有两种实现方式:一种是使用循环数组;另一种是使用链表。
在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法:
public interface Collection<E> {
boolean add(E element);
Iterator<E> iterator();
}
Iterator 接口包含 4 个方法:
public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
编译器简单的将“for each”循环翻译为带有迭代器的循环。
在 Java SE 8 中,甚至可以不用写循环。可以调用 forEachRemaining 方法并提供一个 lambda 表达式(它会处理一个元素)。元素被访问的顺序取决于集合类型。
Java 集合框架为不同类型的集合定义了大量接口,如图:
9.2 具体的集合
Java 库中的具体集合:
级和类型 | 描述 |
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端对列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键 / 值关联的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键 / 值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用 == 而不是用 equals 比较键值的映射表 |
集合框架中的类:
在 Java 程序设计语言中,所有链表实际上都是双向链接的(doublely linked)——即每个结点还存放着指向前驱结点的引用。
链表是有个有序集合(ordered collection),每个对象位置十分重要。链表的插入与删除可以用“光标”类比。但是,add 方法只依赖于迭代器的位置,而 remove 方法依赖于迭代器的状态。
有一种数据结构可以快速地查找所需要的对象,这就是散列表(hash table)。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。
在 Java 中,散列表用链表数组实现。
TreeSet 类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。
正如 TreeSet 类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree)),每次将一个元素添加到树中时,都被放置在正确的排序位置上。
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。其使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)或删除(remove)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
9.3 映射
Java 类库为映射提供了两个通用实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。
散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于健。与键关联的值不能进行散列或比较。
与集一样,散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择散列。
映射视图:Set<K> keySet()
,Collection<V> values()
,Set<Map.Entry<K, V>> entrySet()
。
如今访问所有映射条目最高效的方法是使用 forEach:
counts.forEach((k, v) -> {
// do something with k, v
})
弱散列映射(WeakHashMap)使用*弱引用(weak references)*保存键。WeakReference 对象将引用保存到散列键中,GC 会用一种特殊的方式进行处理。如果某个对象只能有 WeakReference 引用,GC 仍然会回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap 将周期性的检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来,于是,WeakHashMap 将删除对应的条目。
链接散列集(LinkedHashSet)与映射(LinkedHashMap)类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中:
链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。每次调用 get 或 put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键列码对应的桶中)。
访问顺序对于实现高速缓存的“最近最少使用”原则十分重要。例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉。这是近期最少使用的几个元素。
甚至可以让这一过程自动化。即构造一个 LinkedHashMap 的子类,然后覆盖下面这个方法:
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
每当方法返回 true 时,就添加一个新条目,从而导致删除 eldest 条目。例如,下面的高速缓存可以存放 100 各元素:
Map<K, V> cache = new LinkedHashMap<>(128, 0.75F, true) {
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 100;
}
} ();
类 IdentityHashMap 有特殊的作用。在这个类中,键的散列值不是用 hashCode 函数计算的,而是用 System.identityHashCode 方法计算的。这是 Object.hashCode 方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap 类使用 ==,而不是 equals。
也就是说,不同的键对象,即使内容相同,也被视为是不同的对象。
9.4 视图与包装器
keySet 方法返回一个实现 Set 接口的类对象,这个类的方法对原映射进行操作。这种集合称为视图。
Collections 类包含很多使用方法,这些方法的参数和返回值都是集合。不要将它与 Collection 接口混淆起来。
Collections 还有几个方法,用于产生集合的不可修改视图(unmodifiable views)。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改,就抛出一个异常,同时将这个集合保持未修改的状态。
可以使用下面 8 种方法获得不可修改视图:
- Collections.unmodifiableCollection
- Collections.unmodifiableList
- Collections.unmodifiableSet
- Collections.unmodifiableSortedSet
- Collections.unmodifiableNavigableSet
- Collections.unmodifiableMap
- Collections.unmodifiableSortedMap
- Collections.unmodifiableNavigableMap
每个方法都定义于一个接口。
如果有多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。例如,Collections 类的静态 synchronizedMap 方法可以将任何一个映射表转换成具有同步访问方法的 Map:Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());
现在,就可以由多线程访问 map 对象了。像 get 和 put 这类方法都是同步操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成。第 14 章将会详细地讨论数据结构的同步访问。
“受查”视图用来对泛型类型发生问题时提供调试支持。如同第 8 章中所述,实际上将错误类型的元素混入泛型集合中的问题极有可能发生。例如:
ArrayList<String> strings = new ArrayList<>();
// warning only, not an error, for compatibility with legacy code
ArrayList rawList = strings;
// now strings contains a Date object!
rawList.add(new Date());
受查视图可以探测到这类问题:List<String> safeStrings = Collections.checkedList(strings, String.class);
视图的 add 方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个 ClassCastException。
警告:受查视图受限于虚拟机可以运行的运行时检查。例如,对于 ArrayList<Pair<String>>,由于虚拟机有一个单独的“原始” Pair 类,所以,无法阻止插入 Pair<Date>。
9.5 算法
使用 Collections 类中的 sort 方法可以排序,shuffle 方法可以随机地混排。
- 如果列表支持 set 方法,则是可修改的。
- 如果列表支持 add 和 remove 方法,则是可改变大小的。
对于已排序的集合,可以使用 binarySearch 方法进行二分查找。返回负值则表示没有匹配的元素。只有采用随机访问,二分查找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分查找就完全失去了优势。因此,如果为 binarySearch 算法提供一个链表,它将自动地变为线性查找。
此外,在 Collections 类中包含了几个简单且很有用的算法以及批操作,集合与数组互相转换等。
9.6 遗留的集合
以下遗留的集合类已经集成到集合框架中:
- Hashtable 类。与 HashMap 类拥有同样的接口。如果对同步性或遗留代码的兼容性没有任何要求,就应该使用 HashMap。如果需要并发访问,则要使用 ConcurrentHashMap,参见第 14 章。
- 枚举。遗留集合使用 Enumeration 接口对元素序列进行遍历。Enumeration 接口有两个方法,即 hasMoreElements 和 nextElement。这两个方法与 Iterator 接口的 hasNext 方法和 next 方法十分类似。
- 属性映射。property map 是一个类型非常特殊的映射结构。实现属性映射的 Java 平台类称为 Properties。属性映射通常用于程序的特殊配置选项,参见第 13 章。
- 键与值都是字符串。
- 表可以保存到一个文件中,也可以从文件中加载。
- 使用一个默认的辅助表。
- 栈。Stack 类扩展为 Vector 类,理论上说,Vector 类并不太令人满意,它可以让栈使用不属于栈操作的 insert 和 remove 方法。
- 位集。BitSet 类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。由于位集将位包装在字节里,所以,使用位集要比使用 Boolean 对象的 ArrayList 更加高效。
第 10 章 图形程序设计
10.1 Swing 概述
10.2 创建框架
10.3 框架定位
10.4 在组件中显示信息
10.5 处理 2D 图形
10.6 使用颜色
10.7 文本使用特殊字体
10.8 显示图像
第 11 章 事件处理
11.1 事件处理基础
11.2 动作
11.3 鼠标事件
11.4 AWT 事件继承层次
第 12 章 Swing用户界面组件
12.1 Swing和模型-视图-控制器设计模式
12.2 布局管理概述
12.3 文本输入
12.4 选择组件
12.5 菜单
12.6 负责的布局管理
12.7 对话框
第 13 章 部署 Java 应用程序
13.1 JAR 文件
Java 归档文件既可以包含类文件,也可以包含诸如图像和声音这些其他类型的文件。此外,JAR 文件是压缩的,使用了 ZIP 压缩格式。
13.2 应用首选项的存储
可以使用 store 方法将属性映射列表保存到一个文件中,如 program.properties。
习惯上,会把程序属性存储在用户主目录的一个子目录中。目录通常以一个点号开头(在 UNIX 系统中),这个约定就说明这是一个对用户隐藏的系统目录。
13.3 服务加载器
13.4 applet
13.5 Java Web Start
第 14 章 并发
14.1 什么是线程
14.2 中断线程
在 Java 早期版本中,还有一个 stop 方法,其他线程可以调用它终止线程。但是,这个方法已经被弃用了。 14.5 中将会讨论它被弃用的缘由。
没有可以强制线程终止的方法。然而,interrupt 方法可以用来请求终止线程。
当对一个线程调用 interrupt 方法时,现成的中断状态将被置位。这是每一个线程都具有的 boolean 标志。
但是,如果线程被阻塞,就无法检测中断状态。这是产生 InterruptException 异常的地方。当在一个被阻塞的线程(调用 sleep 或 wait)上调用 interrupt 方法时,阻塞调用将会被 InterruptException 异常中断。
没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后,继续执行,而不理会中断。但是,更普遍的情况是,线程将简单地将中断作为一个终止的请求。
如果在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法),isInterrupted 检测既没有必要也没有用处。如果在中断状态被置位时调用 sleep 方法,它不会休眠。相反,它将清除这一状态(!)并抛出 InterruptedException。因此,如果你的循环调用 sleep,不会检测中断状态。相反,要捕获 InterruptedException 异常。
有两个非常类似的方法,interrupt 和 isInterrupted。interrupt 方法是一个静态方法,它检测当前的线程是否被中断。而且,调用 interrupt 方法会清除该线程的中断状态。另一方面,isInterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。
在很多发布的代码中会发现 InterruptedException 异常被抑制在很低的层次上,像这样:
void mySubTask() {
...
try { sleep(delay); }
// Don't ignore!
catch (InterruptedException e) {}
...
}
不要这样做!如果不认为在 catch 子句中做这一处理有什么好处的话,仍然有两种合理的选择:
- 在 catch 子句中调用
Thread.currentThread().interrupt()
来设置中断状态。于是,调用者可以对其进行检测。
void mySubTask() {
...
try { sleep(delay); }
catch (InterruptedException e) {
Thread.currentThread().interrupt()
}
...
}
- 或者,更好的选择是,用 throws InterruptedException 标记你的方法,不采用 try 语句块捕获异常。于是,调用者(或者,最终的 run 方法)可以捕获这一异常。
void mySubTask() throws InterruptedException {
...
sleep(delay);
...
}
14.3 线程状态
要确定一个线程的状态,可调用 getState 方法。线程可以有如下 6 种状态:
- New(新创建)
当用 new 操作付创建一个新线程时,如 new Thread®,该线程还没有开始运行。这意味它处于新创建状态。 - Runnable(可运行)
一旦调用 start 方法,线程处于可运行状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。(Java 的规范说明没有将它作为一个单独状态。一个正在运行的线程仍然处于可运行状态。)记住,在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行。 - Blocked(被阻塞)
当一个线程试图获取一个内部的对象锁(不是 java.util.concurrent 库中的锁),而该锁被其他线程持有,则该线程进入被阻塞状态(在 14.5 节讨论内部对象锁和 java.util.concurrent 锁)。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。 - Waiting(等待)
当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时,就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。 - Timed waiting(计时等待)
有几个方法有一个超时参数。调用它们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接受到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.join、Lock.tryLock 以及 Condition.await 的计时版。 - Terminated(被终止)
线程因如下两个运行之一而被终止:
- 因为 run 方法正常退出而自然死亡。
- 因为一个没有捕获的异常终止了 run 方法而意外死亡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZC71WvQn-1609914481342)(E:\documents\学习笔记\线程状态.png)]
14.4 线程属性
下面将讨论线程的各种属性,其中包括:线程优先级、守护线程(daemon thread)、线程组以及处理未捕获异常的处理器。
- 线程优先级。在 Java 程序设计语言中,每一个线程都有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。
警告:如果确实要使用优先级,应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死。
- 守护线程。可以通过调用
t.setDaemon(true);
将线程转换为守护线程。守护线程的唯一用途是为其他线程服务。计时线程就是一个例子,它定时地发送“定时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了。守护线程应该永远不去访问固有资源,如文件、数据库,因为它们会在任何时候甚至在一个操作的中间发生中断。 - 未捕获异常处理器。线程的 run 方法不能抛出任何受查异常,但是,非受查异常会导致线程终止。在种情况下,线程就死亡了。不需要 catch 子句来处理可以传播的异常,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换处理器可以使用日志 API 发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。
注释:线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所有建议不要在自己的程序中使用线程组。
14.5 同步
为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。
有两种机制防止代码块受并发访问的干扰。Java 语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。java.util.concurrent 框架为这些基础机制提供独立的类。
用 ReentrantLock 保护代码块的基本结构如下:
// a ReentrantLock object
myLock.lock();
try {
// critical section
} finally {
// make sure the lock is unlocked even if an exception is thrown
myLock.unlock();
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock 语句。
警告:把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个*持有计数(hold count)*来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
警告:要留心临界区中的代码,不要因为异常的抛出而跳出临界区。如果在临界区代码结束之前抛出了异常,finally 子句将释放锁,但会使对象可能处于一种受损状态。
ReentrantLock(boolean fair) 可以构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所有,默认情况下,锁没有被强制为公平的。
警告:听起来公平锁更合理一些,但是使用公平锁比使用常规锁要慢很多。只有当你确实了解自己要做什么并且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。即使使用公平锁,也无法确保线程调度器是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,那么就没有机会公平地处理这个锁了。
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。在这一节里,我们介绍 Java 库中条件对象的实现。(由于历史的原因,条件对象经常被称为条件变量(conditional variable)。)
现在来细化银行的模拟程序。我们避免选择没有足够资金的账户作为转出账户。注意不能使用下面这样的代码:
if (bank.getBalance(from) >= amount) {
bank.transfer(from, to, amount);
}
当前线程完全有可能在成功地完成测试,且在调用 transfer 方法之前被中断:
if (bank.getBalance(from) >= amount) {
// thread might be deactivated at this point
bank.transfer(from, to, amount);
}
在线程再次运行前,账户余额可能已经低于提款金额。通过使用锁来保护检查与转账动作来做到这一点:
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
// wait
...
}
// transfer funds
...
} finally {
bankLock.unlock();
}
}
一个锁对象可以有一个或多个相关的条件对象。你可以用 newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如,在此设置一个条件对象来表达来表达“余额充足”条件。
class Bank {
private Condition sufficientFunds;
...
public Bank() {
...
sufficientFunds = bankLock.newCondition();
}
}
如果 transfer 方法发现余额不足,它调用 sufficientFunds.await();
。当前线程现在被阻塞了,并放弃了锁。当另一个线程转账时,它应该调用 sufficientFunds.signalAll()
。这一调用重新激活因为这一条件而等待的所有线程。
注释:通常,对 await 的调用应该在如下形式的循环体中:
while(!(/* ok to proceed */)) {condition.await();}
如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的**死锁(deadlock)**现象。
注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
另一个方法 signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用 signal,那么系统就死锁了。
警告:当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用 await、signalAll 或 signal 方法。
之前介绍了如何使用 Lock 和 Condition 对象。在进一步深入之前,总结一下有关锁和条件的关键之处:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理视图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到 Java 语言内部的机制。从 1.0 版本开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。内部对象锁只有一个相关条件。
注释:wait、notifyAll 以及 notify 方法时 Object 类的 final 方法。Confition 方法必须被命名为 await、signalAll 和 signal 以便它们不会与那些方法发生冲突。
将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关的类对象的内部所。例如,如果 Bank 类有一个静态同步的方法,那么当该方法被调用时,Bank.class 对象的锁被锁住。因为,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
内部锁和条件存在一些局限。包括:
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件,可能是不够的。
在代码中应该使用哪一种?Lock 和 Condition 对象还是同步方法?下面是一些建议:
- 最好既不使用 Lock/Condition 也不使用 synchronized 关键字。在许多情况下你可以使用 java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。
- 如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。
- 如果特别需要 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition。
还有另一种机制可以获得锁,通过进入一个同步阻塞。当进程进入如下形式的阻塞:
// this is syntax for a synchronized block
synchronized (obj) {
// critical section
}
于是它获得 obj 的锁。
有时会发现“特殊的”锁,例如:
public class Bank {
private double[] accounts;
private Object lock = new Object();
...
public void transfer(int from, int to, int amount) {
// an ad-hoc lock
synchronized(lock) {
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(...);
}
}
在此,lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。
锁和条件是线程同步的强大工具,但是,严格来讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器(monito)。用 Java 的术语来讲,监视器具有如下特性:
- 监视器是只包含私有域的类。
- 每个监视器类的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj.method(),那么 obj 对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域都是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
- 该锁可以有任意多个相关条件。
volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
警告:Volatile 变量不能提供原子性。
还有一种情况可以安全的访问一个共享域,即这个域声明为 final 时。
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatile。java.util.concurrent.atomic 包中有很多类试用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。如果希望完成更复杂的更新,就必须使用 compareAndSet 方法。
线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎的申请锁。tryLock 方法试图申请一个锁,在成功获得锁后返回 true,否则,立即返回 false,而且线程可以立即离开去做其他事情。
if (myLock.tryLock()) {
// now the thread owns the lock
try {...}
finally {myLock.unlock();}
} else {
// do something else
}
可以调用 tryLock 时,使用超时参数,像这样:if (myLock.tryLock(100, TimeUnit.MILLISECONDS)) {...}
TimeUnit 是一个枚举类型,可以取的值包括 SECONDS、MILLISECONDS、MICROSECONDS 和 NANOSECONDS。
lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock 方法就无法终止。
然而,如果调用带有用超市参数的 tryLock,那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。也可以调用 lockInterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。等待也可以提供超时。
java.util.concurrent.locks 包定义了两个锁类,ReentrantLock 类和 ReentrantReadWriteLock 类。后者适用于对共享资源写操作较少的的场景。
14.6 阻塞队列
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,**阻塞队列(blocking queue)**导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。队列会自动地平衡负载。下表给出了阻塞队列的方法。
方法 | 正常动作 | 特殊情况下的动作 |
add | 添加一个元素 | 如果队列满,则抛出 IllegalStateException 异常 |
element | 返回队列的头元素 | 如果队列空,抛出 NoSuchElementException 异常 |
offer | 添加一个元素并返回 true | 如果队列满,返回 false |
peek | 返回队列的头元素 | 如果队列空,则返回 null |
poll | 移出并返回队列的头元素 | 如果队列空,则返回 null |
put | 添加一个元素 | 如果队列满,则阻塞 |
remove | 移出并返回头元素 | 如果队列空,则抛出 NoSuchElementException 异常 |
take | 移出并返回头元素 | 如果队列空,则阻塞 |
注释:poll 和 peek 方法返回空来指示失败。因此,向这些队列中插入 null 值是非法的。
还有带有超时的 offer 方法和 poll 方法的变体。
java.util.concurrent 包提供了阻塞队列的几个变种。默认情况下,LinkedBlockingQueue 的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。PriorityBlockingQueue 是一个带优先级的队列,而不是先进先出队列。元素会按照他们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的,取元素的操作会阻塞。
此外,DelayQueue 实现 Delayed 接口,Java SE 7 新增 TransferQueue 接口,允许生产者线程等待,知道消费者准备就绪可以接收一个元素。LinkedTransferQueue 类实现了这个接口。
14.7 线程安全的集合
如果多线程要并发地修改一个数据结构,例如散列表,那么很容易会破坏这个数据结构。可以通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易些。
java.util.concurrent 包提供了映射、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet 和 ConcurrentLinkedQueue。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同不分来使竞争极小化。确定这样的集合当前的大小通常需要遍历。
注释:有些应用使用庞大的并发散列映射,这些映射太过庞大,以至于无法用 size 方法得到它的大小,因为这个方法只能返回 int。对于一个包含超过 20 亿条目的银蛇该如何处理?Java SE 8 引入了一个 mappingCount 方法可以把大小作为 long 返回。
在 Java SE 8 中,并发散列映射将桶组织为树,而不是列表,键类型实现了 Comparable,从而可以保证性能为 O(log(n))。
Java SE 8 为并发散列映射提供提供了批操作,即使有其他线程在处理映射,这些操作也能安全地执行。批操作会遍历映射,处理遍历过程中找到的元素。无须冻结当前映射的快照。除非你恰好知道批操作运行时映射不会修改,否则就要把结果看作是映射状态的一个近似。
有 3 种不同的操作:
- 搜索(search)为每个键或值提供一个函数,直到函数生成一个非 null 的结果。然后搜索终止,返回这个函数的结果。
- 归约(reduce)组合所有键或值,这里要使用所提供的一个累加函数。
- forEach 为所有键或值提供一个函数。
每个操作都有 4 个版本:
- operationKeys:处理键。
- operationValues:处理值。
- operation:处理键和值。
- operationEntries:处理 Map.Entry 对象。
对于上述各个操作,需要指定一个参数化阈值(parallelism threshold)。如果映射包含的元素多于这个阈值,就会并行完成批处理操作。如果希望批处理操作在一个线程中运行,可以使用阈值 Long.MAX_VALUE。如果希望用尽可能多的线程运行批操作,可以使用阈值 1。
假设你想要的是一个大的线程安全的集而不是映射,并没有一个 ConcurrentHashSet 类,而且你肯定不想自己创建这样一个类。当然,可以使用 ConcurrentHashMap(包含“假”值),不过这会得到一个映射而不是集,而且不能应用 Set 接口的操作。
静态 newKeySet 方法会生成一个 Set<K>,这实际上是 ConcurrentHashMap<K, Boolean> 的一个包装器。(所有映射值都为 Boolean.TRUE,不过因为只是要把它用作一个集,所以并不关心具体的值。)Set<String> words = ConcurrentHashMap.newKeySet();
当然,如果原来有一个映射,keySet 方法可以生成这个映射的键集。这个集是可变的。如果删除这个集元素,这个键(以及相应的值)会从映射中删除。不过,不能向键集增加元素,因为没有相应的值可以增加。Java SE 8 为 ConcurrentHashMap 增加了第二个 keySet 方法,包含一个默认值,可以在为集增加元素时使用:
Set<String> words = map.keySet(1L);
words.add("Java");
如果“Java”在 words 中不存在,现在它会有一个值 1。
在 Java SE 8 中,Arrays 类提供了大量并行化操作。静态 Arrays.parallelSort 方法可以对一个基本类型值或对象的数组排序。
从 Java 的初始版本开始,Vector 和 Hashtable 类就提供了线程安全的动态数组和散列表的实现。现在这些类被弃用了,取而代之的是 ArrayList 和 HashMap 类。这些类不是线程安全的,而集合库中提供了不同的机制。任何集合类都可以通过使用同步包装器(synchronization wrapper)变成线程安全的:List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());
Map<K, V> synchHashMap = Collections.synchronizedMap(new HashMap<K, V>());
结果集合的方法使用锁加以保护,提供了线程安全访问。
应该确保没有任何线程通过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并立即传递给包装器。
如果在另一个线程可能进行修改时仍要对集合进行迭代,仍然需要使用“客户端”锁定:
synchronized (synchHashMap) {
Iterator<K> iter = synchHashMap.keySet().iterator();
while(iter.hasNext()) {
...
}
}
如果使用“for each”循环必须使用同样的代码,因为循环使用了迭代器。注意:如果在迭代过程中,别的线程修改集合,迭代器会失效,抛出 ConcurrentModificationException 异常。同步仍然是需要的,因为并发的修改可以被可靠的检测出来。
最好使用 java.util.concurrent 包中定义的集合,不是用同步包装器中的。特别是,假如它们访问的是不同的桶,由于 ConcurrentHashMap 已经精心地实现了,多线程可以访问它而且不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的 ArrayList 可以胜过 CopyOnWriteArrayList。
14.8 Callable 与 Future
Runnable 封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable 和 Runnable 类似,但是有返回值。Callable 接口是一个参数化的类型,只有一个方法 call。
public interface Callable<V> {
V call() throws Exception;
}
类型参数是返回值的类型。
Future 保存异步计算的结果。可以启动一个计算,将 Future 对象交给某个线程,然后忘掉它。Future 对象的所有者在结果计算好之后就可以获得它。
public interface Future<V> {
V get() throws ...;
V get(long timeout, TimeUnit unit) throws ...;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDone();
}
FutureTask 包装器是一种非常便利的机制,可将 Callable 转换成 Future 和 Runnable,它同时实现二者的接口。
14.9 执行器
构建一个新的线程是有一定代价的,因为涉及到与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将 Runnable 对象交给线程池,就会有一个线程调用 run 方法。当 run 方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。
执行器(Executor)类有许多静态工厂方法用来构建线程池,下表中对这些方法进行了汇总。
方法 | 描述 |
newCachedThreadPool | 必要时创建线程;空闲线程会被保留 60 秒 |
newFixedThreadPool | 该池包含固定数量的线程;空闲线程会一直被保留 |
newSingleThreadExecutor | 只有一个线程的“池”,该线程顺序执行每一个提交的任务(类似于 Swing 事件分配线程) |
newScheduledThreadPool | 用于预定执行而构建的固定线程池,替代 java.util.Timer |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程”池“ |
表中前三个方法返回实现了 ExecutorService 接口的 ThreadPoolExecutor 类的对象。可用下面的方法之一将一个 Runnable 对象或 Callable 对象提交给 ExecutorService:
Future<?> submit(Runnable task)
Future<T> submit(Runnable task, T result)
Future<T> submit(Callable<T> task)
该池会在方便的时候尽早执行提交的任务。调用 submit 时,会得到一个 Future 对象,可用来查询该任务的状态。
ScheduledExecutorService 接口具有为预定执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的 java.util.Timer 的泛化。Executors 类的 newScheduledThreadPool 和 newSingleThreadScheduledExecutor 方法将返回实现了 ScheduledExecutorService 接口的对象。
可以预定 Runnable 或 Callable 在初始的延迟之后只运行一次。也可以预定一个 Runnable 对象周期性地运行。详细内容见 API 文档。
当用完一个线程池的时候,调用 shutdown。该方法启动该池的关闭序列,被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。另一种方法是调用 shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。
下面总结在使用连接池时应该做的事:
- 调用 Executors 类中静态的方法 newCachedThreadPool 或 newFixedThreadPool。
- 调用 submit 提交 Runnable 或 Callable 对象。
- 如果想要取消一个任务,或如果提交 Callable 对象,那就要保存好返回的 Future 对象。
- 当不再提交任何任务时,调用 shutdown。
14.10 同步器
java.util.concurrent 包包含了几个能帮助人们管理互相合作的线程集的类:
类 | 它能做什么 | 说明 |
CyclicBarrier | 允许线程集等待直至其中预定数目的线程到达一个公共障栅(barrier),然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在它们的结果可用之前完成时 |
Phaser | 类似于循环障栅,不过有一个可变的计数 | Java SE 7 中引入 |
CountDownLatch | 允许线程集等待直到计数器减为 0 | 当一个或多个线程需要等待直到指定数目的事件发生 |
Exchanger | 允许两个线程在要交换的对象准备好时交换对象 | 当两个线程工作在同一数据结构的两个实例上的时候,一个向实例添加数据而另一个从实例清除数据 |
Semaphore | 允许线程集等待直到被允许继续运行为止 | 限制访问资源的线程总数。如果许可数是 1,常常阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时 |
这些机制具有为线程之间的*共用集结点模式(common rendezvous patterns)*提供的”预置功能“(canned functionality)。如果有一个互相合作的线程集满足这些行为模式之一,那么应该直接重用合适的库类而不要试图提供手工的锁与条件的集合。
14.11 线程与 Swing
Swing 不是线程安全的。如果试图在多个线程中操纵用户界面的元素,那么用户界面可能崩溃。
完