《Java核心技术 第11版》学习笔记,引用原书部分内容仅供学习,版权归原作者、出版社所有。
Java 基本程序设计结构
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Date;
import java.util.Scanner;
/**
* Java的基本程序设计结构
*
* @author AUTHOR_NAME
* @version 0.1
*/
public class Chapter3 {
public static final double CM_PER_INCH = 2.54;
public static void main(String[] args) {
//关键字public称为访问修饰符(access modifier)
//Java应用程序中的全部内容都必须放置在类(class)中
//关键字class后面跟类名,必须以字母开头,后面可跟字母和数字的任意组合,长度基本无限制,不能用Java保留字
//[标准的命名规范是]:类名是以大写字母开头的名词,如果由多个单词组成,每个单词首字母都应大写
//(在一个单词中间使用大写的方式称为骆驼命名法CamelCase)
//源代码的文件名必须与公共类的名字相同(区分大小写),并用.java作为扩展名
//源代码编译后得到.class扩展名的字节码文件
//运行已编译的程序时,Java虚拟机总是从指定类中的main方法开始执行
//根据Java语言规范,main方法必须声明为public
//Java中的main方法必须是静态的,与C不同的是main方法没有为操作系统返回「退出码」
//如果main方法正常退出,那么Java应用程序的退出码为0,表示成功运行了程序
//如果希望返回其他的退出码,需要使用System.exit方法
String hlw = "Hello World!";
System.out.println(hlw);
for (int i = 0; i < hlw.length(); i++) {
System.out.print("=");
}
System.out.println();
//使用了System.out对象并调用了它的println方法,若调用print方法则不在输出后增加换行符
//点号用于调用方法,Java使用的通用语法是:object.method(parameters)
//以/**开始*/结束的注释可以用来自动生成文档
//注意:/* */注释不能嵌套
primitiveType();
varAndConstant();
operator();
stringAndAPI();
//inputAndOutput();
controlFlow();
bigNumber();
array();
}
public static void primitiveType() {
//Java是一种强类型语言,必须为每一个变量声明一种类型,Java中共有8种基本类型(primitive type)
//注:Java有一个能表示任意精度的算书包,通常称为「大数(big number)」,它不是一种基本类型而是一个Java对象
//4种整型:int(4字节)、short(2字节)、long(8字节)、byte(1字节);在Java中,整数的范围与运行的平台无关;Java没有任何无符号(unsigned)类型
//字面量:long后缀L或l,十六进制前缀0x或0X,八进制前缀0,二进制数前缀0b或0B(0b1001为9);可以为字面量加下划线便于阅读,Java编译器会自动去除下划线
//如果确实需要,也可以把有符号整数解释为无符号数,例如byte类型,只要不溢出,加减乘都能正常计算
//但其他运算需要调用Byte.toUnsignedInt(b)来得到0到255的int值,处理这个值再把它转换回byte;Integer和Long类都提供了处理无符号除法和求余数的方法
//2种浮点型:float(4字节,有效位数6~7位)、double(8字节,有效位数15位)
//字面量:float后缀F或f,double后缀D或d,无后缀默认为double
//可以用十六进制表示浮点数,0.125 = 2^-3 = 0x1.0p-3,p表示指数,尾数采用十六进制,指数采用十进制,指数的基数是2
//三个表示溢出和出错情况的特殊浮点值:正无穷大(Double.POSITIVE_INFINITY)、负无穷大(Double.NEGATIVE_INFINITY)、NaN(Not a Number)(Double.NaN)
//不能这样检测一个值是否为NaN:x == Double.NaN;所有的「非数值」的值都认为是不相同的,可以这样:Double.isNaN(x)
//浮点数值不适用于无法接受舍入误差的金融计算
System.out.println(2.0 - 1.1);
//二进制系统无法精确表示1/10;如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal类
//1种字符类型char:char原本用于表示单个字符,但如今有些Unicode字符可以用一个char值描述,另外一些则需要两个char值
//char类型的值可以表示为十六进制值,范围从u0000到uFFFF
//转义序列反斜杠u可以出现在字符常量和字符串之外,其他的转义序列(\b \t \n \r \" \' \\)不可以
//警告:Unicode转义序列会在解析代码之前被处理,要当心注释中的u,反斜杠u000A会产生一个错误,它会被替换成一个换行符
//「C:\ users」也会产生一个语法错误,因为反斜杠u后面没有跟4个十六进制数
System.out.println("\u0022+\u0022");
//输出的字符串会被转换为空串""+""
//1980年代开始Unicode设计工作时,人们认为两字节的代码宽度足以对世界各种语言的所有字符进行编码,1991年发布的Unicode1.0仅占用65536个代码值中不到一半
//设计Java时决定采用了16位的Unicode字符集
//遗憾的是,现在16位的char类型已经不能满足描述所有Unicode字符的需要了
//从Java5开始是这样解决这个问题的:
//码点(code point)是指与一个编码表中的某个字符对应的代码值;Unicode标准中,码点采用十六进制书写并加上前缀U+
//Unicode的码点可分成17个代码平面(code plane),第一个代码平面称为基本多语言平面(basic multilingual plane),包括码点从U+0000到U+FFFF的“经典”Unicode代码
//其余的16个平面的码点为从U+10000到U+10FFFF,包括辅助字符(supplementary character)
//UTF-16编码采用不同长度的编码表示所有Unicode码点,在基本多语言平面中,每个字符用16位表示,通常称为代码单元(code unit)
//而辅助字符编码为一对连续的代码单元,采用这种编码对表示的各个值落入基本多语言平面中未用的2048个值范围内,通常称为替代区域(surrogate area)
//在Java中,char类型描述了UTF-16编码中的一个代码单元
//不建议在程序中使用char类型,除非确实需要处理UTF-16代码单元,最好将字符串作为抽象数据类型处理
//1种表示真值的boolean类型:布尔类型有两个值false和true,整型值和布尔值之间不能相互转换
//C++中,数值甚至指针可以代替布尔值,0相当于false,在Java中不是这样的
}
public static void varAndConstant() {
boolean done = true;
//Java变量名必须是一个以字母开头并由字母或数字构成的序列
//Java中的「字母」和「数字」的范围比大多数程序设计语言更大,字母包括A~Z a~z _ $或在某种语言中表示字母的任何Unicode字符
//数字包括0~9和在某种语言中表示数字的任何Unicode字符;变量名中所有的字符都是有意义的,且大小写敏感,变量名长度基本无限制
//注:可以用Character.isJavaIdentifierStart和Character.isJavaIdentifierPart方法来检查是否属于Java中的「字母」
//不要在自己的代码中使用$字符,它只用在Java编译器或其他工具生成的名字中
//可以在一行中声明多个变量,但不提倡使用这种风格,逐一声明变量可以提高程序可读性
//变量的声明应尽可能靠近变量第一次使用的地方,这是一种良好的编程风格
var vacationDays = 12;
//从Java10开始,对于局部变量,如果可以从变量的初始值推断出它的类型,只需使用关键字var而无需指定类型
//在Java中不区分变量的声明和定义
final double PI = 3.14;
//在Java中,利用关键字final指示常量
//final表示这个变量只能被赋值一次,习惯上,常量名使用全大写
//如果希望某个常量可以在一个类的多个方法中使用,可以使用关键字static final设置一个类常量(class constant),类常量定义在main方法的外部
//如果一个常量被声明为public,那么其他类的方法也可以使用这个常量
enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE}
//可以自定义枚举类型
Size s = Size.MEDIUM;
//然后可以声明这种类型的变量
//Size类型的变量只能存储这个类型声明中给定的某个枚举值,或者特殊值null(表示没有设置任何值)
}
public static void operator() {
//【运算符】P37
//算术运算符:+-*/(/运算两个操作数都是整数时表示整数除法,否则表示浮点除法),整数求余用%(取模用Math.floorMod)
//整数被零除会产生一个异常,浮点数被零除会得到无穷大或NaN结果
//标记为strictfp的方法或类中所有指令使用严格的浮点计算
//Math类中包含各种数学函数,例如Math.sqrt(x)计算平方根
//double y = Math.pow(x, a); 设置y的值为x的a次幂
//表达式n % 2,n是偶数时为0,n是奇数时为1,n是负数时为-1
//如果计算一个时钟时针的位置,要调整后归一化为一个0~11间的数,(position + adjustment) % 12,如果调整为负可能会得到一个负数
//使用 floorMod(position + adjustment, 12) 就总会得到一个0~11间的数
//取余,遵循尽可能让商向0靠近的原则,取模,遵循尽可能让商向负无穷靠近的原则
//x和y符号不同时,取余结果的符号与x相同,而取模与y相同
//Java还提供两个用于表示π和e常量的最接近的近似值:Math.PI、Math.E
//如果需要结果完全可预测比运算速度更重要,应使用StrictMath类,它可以确保在所有平台上得到相同的结果
//Math类提供一些方法使整数有更好的运算安全性,比如计算溢出,10亿*3会返回错误的结果,Math.multiplyExact(1000000000, 3)就会生成一个异常
//还有一些方法(addExact、subtractExact、incrementExact、decrementExact、negateExact)也可以正确处理int和long参数
//无信息丢失的数值类型间转换:byte、short、char转int,int转long、double,float转double
//可能有精度损失的转换:int、long转float,long转double
//二元运算符连接2个值先转换再计算:有double转double,否则有float转float,否则有long转long,否则都转int
double x = 9.997;
int nx1 = (int) x;
int nx2 = (int) Math.round(x);
System.out.println(nx1);
System.out.println(nx2);
//强制类型转换通过截断小数部分转换浮点值到整型,如果想做舍入计算需使用Math.round方法(返回值为long)
//不要再boolean类型与任何数值类型间转换,如确实需要可以使用条件表达式 b?1:0
int y = 1;
y += 3.5;
System.out.println(y);
//y被设置为(int)(y+3.5)
y++;
//自增与自减运算符前缀形式、后缀形式语法与C相同
//关系和boolean运算符沿用C++的做法,&&和||运算符按照「短路」方式求值,如果第一个操作数已能确定表达式的值,第二个操作数就不会被计算
System.out.println(x < y ? x : y);
//三元操作符?:语法与C相同
//位操作符:&(and) |(or) ^(xor) ~(not) <<(左移) >>(右移) >>>(无符号右移,忽略符号位,空位补0)
int fourthBitFromRight = (y & 0b1000) / 0b1000;
System.out.println(fourthBitFromRight);
//处理整形数据时可以直接对组成整数的各个位完成操作,可以使用掩码技术得到整数中的各个位
//应用在布尔值上时,&和|运算符也会得到一个布尔值;&和|运算符不采用「短路」方式来求值,得到结果前两个操作数都需要计算
//移位运算符右操作数要完成模32的运算(左操作数是long则模64),例如 1<<35 等同于 1<<3 = 8
//(C/C++中不能保证>>是完成算术移位还是逻辑移位,对负数生成的结果依赖具体实现,Java消除了这种不确定性)
//运算符优先级P44
//与C/C++不同,Java不使用逗号运算符,但可以在for语句的第1部分和第3部分中使用逗号分隔表达式列表
}
public static void stringAndAPI() {
//【字符串】P44
//Java字符串就是Unicode字符序列,Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类String,每个双引号括起来的字符串都是String类的一个实例
String s1 = "Hello" + "World";
//String类的substring方法可以从一个较大的字符串中提取出一个字串
String s2 = s1.substring(0, 3);
System.out.println(s2);
//substring方法的而第二个参数是「不想」复制的第一个位置
//substring的工作方式有一个优点,容易计算字串长度,字符串s.substring(a, b)的长度为b-a
//将一个字符串与一个非字符串值拼接时,后者会转换成字符串(任何一个Java对象都可以转换成字符串)
int y = 5;
String s3 = s1 + y;
System.out.println(s3);
//如果要把多个字符串放在一起,用一个界定符分隔,可以用静态join方法
String s4 = String.join(" / ", "S", "M", "L", "XL");
System.out.println(s4);
//Java11中还提供了一个repeat方法
System.out.println("Java".repeat(3));
//在Java中修改字符串需要提取想要保留的字串,再与希望替换的字符拼接
//由于不能修改Java字符串中的单个字符,所以在Java文档中将String类对象称为是不可变的(immutable)
//不可变字符串的优点是,编译器可以让字符串共享
//不能认为Java字符串是字符数组,它大致类似于char*指针,重新赋值字符串也不会产生内存泄漏,Java将自动进行垃圾回收
//可以用equals方法检测两个字符串是否相等;s1可以是字符串变量,也可以是字符串字面量
if (s1.equals("HelloWorld")) {
System.out.println(s1);
//如果要不区分大小写,可以用equalsIgnoreCase方法
if ("Hello".equalsIgnoreCase("hello")) {
System.out.println("true");
}
}
//使用==运算符只能确定两个字符串是否存放在同一个位置,而程序有可能将内容相同的多个字符串副本放置在不同位置
//(实际上只有字符串字面量是共享的,而+或substring等操作得到的字符串并不共享)
//C++的string类重载了==运算符以便检测字符串内容的相等性,而Java没有采用这种方式
//还可以使用 if (s1.compareTo("Hello") == 0)
//可以调用以下代码检查一个字符串是否为空
if (s1.length() == 0) {
//或者
if (s1.equals("")) {
//空串是一个Java对象,有自己的串长度(0)和内容(空)
}
//String变量还可以存放一个null值,表示目前没有任何对象与该变量关联
if (s1 == null) {
//要这样检查一个字符串是否为null
}
}
//如果要检查一个字符串既不是null也不是空串,要使用以下条件
if (s1 != null && s1.length() != 0) {
//首先要检查s1不为null,如果在一个null值上调用方法,会出现错误
}
boolean b1 = s1.isBlank();
//如果字符串为空或仅包含空白,返回true
//Java字符串由char值序列组成,char数据类型是采用UTF-16编码表示Unicode码点的代码单元,最常用的Unicode字符使用一个代码单元,而辅助字符需要一对代码单元表示
//length方法返回采用UTF-16编码表示给定字符串所需要的代码单元数量
//要想得到实际的长度,即码点数量,可以调用
int cpCount = s1.codePointCount(0, s1.length());
//调用s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间,要想得到第i个码点,应该使用下列语句
int i = 3;
int index = s1.offsetByCodePoints(0, i);
int cp = s1.codePointAt(index);
//如果想遍历一个字符串,依次查看每个码点,可以用下列语句
String ts = "中文🍺Hi!";
for (int offset = 0; offset < ts.length(); offset++) {
int ch = ts.codePointAt(offset);
System.out.print((char) ch + " ");
if (Character.isSupplementaryCodePoint(ch)) offset++;
}
for (int offset = 0; offset < ts.length(); ) {
int ch = ts.codePointAt(offset);
System.out.print((char) ch + " ");
offset += Character.charCount(ch);
}
//更容易的办法是使用codePoints方法,它生成一个int值的「流」,每个int值对应一个码点,可以将它转换为一个数组,再完成遍历
int[] codePoints = ts.codePoints().toArray();
for (int count = 0; count < codePoints.length; count++) {
System.out.print((char) codePoints[count] + " ");
}
//要把一个码点数组转换为一个字符串,可以使用构造器
String s5 = new String(codePoints, 0, codePoints.length);
//注意:虚拟机不一定把字符串实现为代码单元序列,在Java9中,只包含单字节代码单元的字符串使用byte数组实现,其他字符串使用char数组
//String API P49
//Java中的String类包含很多有用且使用频率很高的方法
b1 = s1.startsWith(s2);
b1 = s1.endsWith(s2);
//如果字符串以指定字符串开头或结尾,返回true
y = s1.indexOf("He");
//返回与指定字符串或码点(int)匹配的最后一个子串的开始位置,从原始字符串末尾或第二个参数(int fromIndex)开始匹配
s1 = s1.replace("lo", "p!");
//返回一个新字符串,用参数2代替原始字符串中所有参数1,可以用String或StringBuilder对象作为参数
s1 = s1.toLowerCase();
s1 = s1.toUpperCase();
//返回一个新字符串,将原始字符串所有字母小写或大写
s1 = s1.trim();
s1 = s1.strip();
//返回一个新字符串,删除原始字符串头部和尾部小于等于U+0020的字符(trim)或空格(strip)
//在API注释中,CharSequence形参是一种接口类型,所有字符串都属于这个接口,可以传入String类型的实参
//在线API文档:https://docs.oracle.com/en/java/javase/17/docs/api/index.html
//构建字符串
//如果需要用多段较短的字符串构建字符串,应使用StringBuilder类
//首先构建一个空的字符串构建器
StringBuilder builder = new StringBuilder();
//每次需要添加内容时,调用append方法
builder.append('A');
builder.append("BC");
//构建完成后调用toString方法得到String对象
String completedString = builder.toString();
System.out.println(completedString);
System.out.println("=".repeat(15));
}
public static void inputAndOutput() {
//【输入与输出】P55
//读取「标准输入流」需要先构造一个与标准输入流System.in关联的Scanner对象
Scanner in = new Scanner(System.in);
//现在就可以使用Scanner类的各种方法读取输入了,例如,nextLine方法读取一行输入
System.out.println("Input something:");
String s6 = in.nextLine();
System.out.println(s6);
//要想读取一个单词(以空白符作为分隔符)可以调用next
s6 = in.next();
System.out.println(s6);
//要想读取一个整数,调用nextInt方法
System.out.println("Input a number:");
int i1 = in.nextInt();
System.out.println(i1);
//类似地,读取下一个浮点数,调用nextDouble方法
//import java.util.*;
//Scanner类定义在java.util包中,使用的类不是定义在基本java.lang包中时,一定要用import指令导入相应的包
//in.hasNext();
//检测输入中是否还有其他单词
//in.hasNextInt();
//in.hasNextDouble();
//检测是否还有下一个表示整数或浮点数的字符序列
//hasNext在扫描缓冲区为空时并不返回false而是将方法阻塞,等待输入继续扫描
//hasNext在等待键盘输入时阻塞,按Ctrl+D可以使其返回false,也可以使用hasNext的重载方法通过特殊终止符使其返回false
in.close();
//要想读取密码,可以用下列代码
Console cons = System.console();
if (cons != null) {
String username = cons.readLine("User name:");
char[] passwd = cons.readPassword("Password:");
//为了安全起见,密码存放在一个字符数组中,处理完密码后应立刻用一个填充值覆盖数组元素
Arrays.fill(passwd, 'x');
}
//格式化输出P58
double d1 = 10000.0 / 3.0;
System.out.println(d1);
//上面的打印命令将以d1的类型所允许的最大非0数位个数打印d1
//Java沿用了C语言函数库中的printf方法
System.out.printf("%8.2f\n", d1);
//转换符:dxo十、十六、八进制整数,f定点浮点数,e指数浮点数,g通用浮点数(e和f中较短的一个),a十六进制浮点数
// sc字符串字符,b布尔,h散列码,%百分号,n与平台有关的行分隔符
//还可以指定控制格式化输出外观的各种标志:
//+ 打印正数负数符号,空格 在正数前添加空格,0 数字前面补0,- 左对齐,( 将负数括在括号内,, 添加分组分隔符
//# 对于f包含小数点 对于xo添加前缀0x或0,$ 指定要格式化的参数索引(%1$d %1$x将以十进制和十六进制打印第1个参数)
//< 格式化前面说明的数值(%d%<x将以十进制和十六进制打印同一个数值)
boolean b1 = true;
System.out.printf("%,(.2f %s\n", -d1, b1);
//可以用s转换符格式化任意的对象,对于实现了Formattable接口的任意对象将调用这个对象的formatTo方法,否则调用toString方法将这个对象转换为字符串
//可以使用静态的String.format方法创建一个格式化的字符串
String s1 = "HelloWorld";
String s7 = String.format("%s,%d", s1, i1);
//t转换符打印日期时间方法已经过时P59,新代码应使用java.time包的方法
System.out.printf("%tc\n%<Tc\n%<tF\n%1$tT\n", new Date());
//日期的格式化规则特定于本地化环境
System.out.println("=".repeat(15));
//文件的输入与输出P61
//写入文件需要构造一个PrintWriter对象
try {
PrintWriter out = new PrintWriter("1.txt", StandardCharsets.UTF_8);
//如果文件不存在则创建,可以像输出到System.out一样使用print、println、printf
out.println("FILE!");
out.close();
} catch (IOException e) {
e.printStackTrace();
}
//要想读取一个文件,需要构造一个Scanner对象
try {
Scanner fin = new Scanner(Path.of("1.txt"), StandardCharsets.UTF_8);
//如果文件名中包含反斜杠符号需要转义
//如果直接构造一个带有字符串参数的Scanner,Scanner会把字符串解释为数据而不是文件名
System.out.println(Path.of("first", "more", "path", "to", "1.txt"));
System.out.println(fin.nextLine());
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
File file = new File("1.txt");
b1 = file.delete();
//可以利用shell的重定向语法将任意文件关联到System.in和System.out
}
public static void controlFlow() {
//【控制流程】P62
//Java的控制流程结构与C/C++一样,只有很少的例外
//Java中没有goto语句,但break语句可以带标签,可以利用它从内部循环跳出(对于这种情况,C可能就要使用goto了)
//还有一种变形的for循环,它有点类似于C++中基于范围的for循环和C#中的foreach循环
//块作用域
//块(block)是由一对大括号括起的若干条Java语句组成的语句,一个块可以嵌套在另一个块中
//但不能在嵌套的两个块中声明同名变量
//C++可以在嵌套块中重定义一个变量,内层定义会覆盖外层定义,Java不允许这样做
//与C++一样,尽管Java允许在for循环的各个部分放置任何表达式,但有一条不成文的规则:
//for语句的3个部分应该对同一个计数器变量进行初始化、检测和更新;若不遵守这一规则,编写的循环常常晦涩难懂
//注意:在循环中,检测两个浮点数是否相等需要格外小心,由于舍入误差,可能永远达不到精确的最终值
//在for语句内部定义的变量不能在循环体之外使用,可以在不同的for循环中定义同名的变量
//Java有一个与C/C++完全一样的switch语句
//switch语句将从与选项值相匹配的case标签开始执行,直到遇到break语句或switch语句结束处,无匹配case执行default字句(如果有)
//case标签可以是:类型为char、byte、short、int的常量表达式,枚举常量,字符串字面量
String input = "yes";
switch (input.toLowerCase()) {
case "yes":
System.out.println("yes");
break;
case "no":
System.out.println("no");
break;
default:
System.out.println("error");
break;
}
//带标签的break语句用于跳出多重嵌套的循环语句
//标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号
boolean b1 = true;
read_data:
while (b1) {
System.out.println(b1);
while (b1) {
b1 = false;
break read_data;
}
}
//事实上,可以将标签应用到任何语句,甚至可以将其应用到if语句或者块语句
label:
{
if (false) {
break label;
}
}
//注意:只能跳出语句块,不能跳入语句块
//还有一种带标签的continue语句,将跳到与标签匹配的循环的首部
out:
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i == j) {
continue out;
}
System.out.print(i);
System.out.print(j + " ");
}
}
System.out.println();
}
public static void bigNumber() {
//【大数】P76
//java.math包的两个类,BigInteger和BigDecimal可以处理包含任意长度数字序列的数值
//BigInteger类实现任意精度的整数运算,BigDecimal实现任意精度的浮点数运算
BigInteger bi1 = BigInteger.valueOf(100);
//使用静态的valueOf方法可以和将普通的数值转换为大数
BigInteger reallyBig = new BigInteger("98045641946854089748464979874895639867897496516947984966904940498486423165");
//对于更大的数,可以使用一个带「字符串参数」的构造器
//另外还有一些常量
reallyBig = BigInteger.ZERO;
reallyBig = BigInteger.ONE;
reallyBig = BigInteger.TWO;
reallyBig = BigInteger.TEN;
//不能使用算术运算符处理大数,要使用大数类中的方法
//add、subtract、multiply、divide、mod、sqrt:返回和、差、积、商、余数、平方根
if (bi1.compareTo(reallyBig) == 0) {
//相等返回0,小于参数返回负数,否则返回正数
}
BigInteger bi2 = reallyBig.add(bi1);
BigInteger bi3 = bi2.multiply(bi1.add(BigInteger.valueOf(2)));
BigDecimal bd1 = BigDecimal.valueOf(3.14);
bd1 = bd1.divide(bd1, RoundingMode.HALF_UP);
//如果商是无限循环小数,divide方法会抛出一个异常
//要得到一个舍入的结果,需要加上第二个参数舍入方式,RoundingMode.HALF_UP是四舍五入
bd1 = BigDecimal.valueOf(1, 3);
//返回值等于x/10^scale的一个大实数
//与C++不同,Java没有提供运算符重载功能,程序员无法重定义算术运算符来提供BigInteger的运算
//Java的设计者确实为字符串的连接重载了+运算符,但没有重载其他的运算符,也没有给Java程序员再自己的类中重载运算符的机会
}
public static void array() {
//【数组】P79
//声明数组需要指出数组类型(数据元素类型紧跟[])
//然后使用new操作符创建数组
int[] arr1 = new int[10];
//或者
var arr2 = new int[10];
//声明并初始化
//也可以使用 int arr[]; 风格
//数组长度不要求是常量
int i1 = 5;
var arr3 = new int[i1];
//一旦创建了数组,就不能再改变它的长度
//如果运行中经常需要扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)
//Java提供一种创建数组对象同时提供初始值的简写形式
int[] arr4 = {2, 4, 6, 8, 10,};
//这个语法中不需要使用new,甚至不用指定长度
//最后一个值后面允许有逗号
//还可以声明一个匿名数组,这会分配一个新数组并填入大括号中的值,数组大小设置为初始值个数
arr4 = new int[]{1, 3, 5, 7, 9};
//Java允许有长度为0的数组,编写一个结果为数组的方法时,如果结果为空,这样一个长度为0的数组就很有用
arr4 = new int[0];
//或者
arr4 = new int[]{};
//长度为0的数组与null并不相同
//在数组中填入元素,可以使用array.length获取数组中的元素个数
for (int i = 0; i < arr1.length; i++) {
arr1[i] = i;
}
//创建一个数字数组时,所有元素都初始化为0;boolean数组的元素会初始化为false;对象数组的元素则初始化为null
System.out.println(arr2[0]);
boolean[] barr = new boolean[1];
System.out.println(barr[0]);
//for each循环
//Java有一种循环结构,可以用来依次处理数组(或其他元素集合)中的每个元素而不必考虑下标值
//for (variable : collection) statement
//它定义一个变量用于暂存集合中的每一个元素
//collection这一集合表达式必须是一个数组或是一个实现了Iterable接口的类对象(例如ArrayList)
for (int element : arr1) {
System.out.print(element);
//打印数组arr1的每一个元素
}
System.out.println();
//这个循环应该读作循环arr1中的每一个元素(for each element in arr1)
//有一个更简单的方式可以打印数组中的所有值
System.out.println(Arrays.toString(arr1));
//Arrays.toString(a)返回一个包含数组元素的字符串
//数组拷贝
//Java允许将一个数组变量拷贝到另一个数组变量,两个变量将引用同一个数组
int[] arr5 = arr1;
arr5[0] = 10;
System.out.println(arr1[0]);
//修改arr5等于修改arr1
//如果希望拷贝一个数组的所有值,要使用Arrays类的copyOf、copyOfRange方法
int[] arr6 = Arrays.copyOf(arr1, 2 * arr1.length);
//第2个参数是新数组的长度,这个方法通常用来增加数组的大小
//额外的元素赋值遵循创建数组自动初始化规则
//相反,如果长度小于原始数组的长度,则只拷贝前面的值
System.out.println(Arrays.toString(arr6));
//Java数组与堆栈上的C++数组有很大不同,但基本上与在堆(heap)上分配的数组指针一样
//也就是Java的 int[] a = new int[100]; 不同于 int a[100]; 而等同于 int* a = new int[100];
//命令行参数
//每个Java应用程序都有一个带String[] args参数的main方法
//这个参数接收命令行上指定的参数
//如果使用下面的形式调用这个程序
//java ThisApp -a hello world
//args数组将包含:
//args[0]: "-a" ; args[1]: "hello" ; args[2]: "world"
//注意:程序名并没有存储在args数组中
//数组排序
for (int i = 0; i < arr6.length; i++) {
arr6[i] = (int) (Math.random() * 50);
}
//Math.random方法返回一个0到1之间的随机浮点数(包含0,不含1)
//用n乘这个浮点数,就能得到从0到n-1之间的一个随机数
//对数值型数组进行排序,可以使用Arrays类中的sort方法
Arrays.sort(arr6);
//这个方法使用了优化的快速排序(QuickSort)算法,对大多数数据集合来说都是效率比较高的
System.out.println(Arrays.toString(arr6));
int index = Arrays.binarySearch(arr1, 5);
index = Arrays.binarySearch(arr1, 0, arr1.length, 5);
//使用二分查找算法在有序数组中查找key值,如果找到返回对应下标,否则返回一个负值r,-r-1是保持数组有序key应插入的位置
//将数组的所有元素设置为val
Arrays.fill(arr1, 1);
System.out.println(Arrays.toString(arr1));
if (Arrays.equals(arr1, arr6)) {
//如果两个数组大小相同,且下标相同的元素都对应相等,返回true
}
//多维数组
//可以使用二维数组(也称为矩阵)存储一个数值表格
double[][] balances;
balances = new double[5][5];
//如果知道数组元素,就可以不调用new,直接使用简写形式对多维数组进行初始化
int[][] magicSquare = {
{15, 3, 2, 12},
{5, 10, 14, 8}
};
//注意:for each循环不能自动处理二维数组的每一个元素,它会循环处理行,要想访问二维数组的所有元素,需要使用两个嵌套的循环
for (int[] temp1 : magicSquare) {
for (int temp2 : temp1) {
System.out.print(temp2);
}
System.out.println();
}
//要快速打印一个二维数组的数据元素列表,也可以调用
System.out.println(Arrays.deepToString(magicSquare));
//不规则数组
//Java实际上没有多维数组,只有一维数组;多维数组被解释为数组的数组
//可以这样交换两行
int[] temp = magicSquare[0];
magicSquare[0] = magicSquare[1];
magicSquare[1] = temp;
System.out.println(Arrays.deepToString(magicSquare));
//还可以构造一个「不规则」数组,即数组的每一行有不同的长度
int[][] irregularArray = new int[10][];
for (int i = 0; i < 10; i++) {
irregularArray[i] = new int[i + 1];
}
System.out.println(Arrays.deepToString(irregularArray));
//可以这样访问不规则数组
for (int i = 0; i < irregularArray.length; i++) {
for (int j = 0; j < irregularArray[i].length; j++) {
System.out.print(irregularArray[i][j]);
}
System.out.println();
}
//Java声明 double[][] a = new double[10][6]; 不同于C++的 double a[10][6]; 也不同于 double (*a)[6] = new double[10][6];
//而是分配了一个包含10个指针的数组 double** a = new double*[10];
//然后指针数组的每一个元素被填充了一个包含6个数字的数组 for (i = 0; i < 10; i++) a[i] = new double[6];
//当调用new double[10][6]时,这个循环是自动的;当需要不规则数组时,只能单独分配行数组
}
}
对象与类
import java.text.NumberFormat;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Date;
/**
* 对象与类
*
* @author AUTHOR_NAME
* @version 0.1
*/
public class Chapter4 {
public static void main(String[] args) {
/*【面向对象程序设计概述】P92*/
//面向对象程序设计(Object-Oriented Programming,OOP)是当今主流的程序设计范型
//面向对象的程序是由对象组成的,每个对象包含 对用户公开的特定功能部分 和 隐藏的实现部分
//程序中的很多对象来自标准库,还有一些是自定义的
//是自己构造对象还是购买对象完全取决于开发项目的预算和时间
//但从根本上说,只要对象能够满足要求,就不必关心其功能到底是如何实现的
//传统的结构化程序设计通过设计一系列的过程(算法)来求解问题,确定了过程再考虑存储数据的方式
//而OOP调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法
//规模较小的问题将其分解为过程的开发方式比较理想
//面向对象更加适合解决规模较大的问题
/*类*/
//类(class)是构造对象的模板或蓝图;由类构造(construct)对象的过程称为创建类的实例(instance)
//Java编写的所有代码都位于某个类里面,标准Java库提供了几千个类,可用于各种目的
//封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念
//从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式
//对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)
//作为一个类的实例,特定对象都有一组特定的实例字段值,这些值的集合就是这个对象的当前状态(state)
//无论何时,只要在对象上调用一个方法,它的状态就有可能发生改变
//实现封装的关键在于:绝对不能让类中的方法直接访问其他类的实例字段,程序只能通过对象的方法与对象数据进行交互
//这是提高重用性和可靠性的关键,这意味着一个类可以完全改变存储数据的方式
//只要仍使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化
//OOP的另一个原则会让用户自定义Java类变得更容易,就是可以通过扩展其他类来构建新类
//事实上,Java中所有其他类都扩展自Object类
//扩展后的新类具有被扩展类的全部属性和方法,只需在新类中提供适用于新类的新方法和数据字段就可以了
//通过扩展一个类来建立另外一个类的过程称为继承(inheritance)
/*对象*/
//使用OOP要想清楚对象的三个主要特征
//对象的行为(behavior):可以对对象完成哪些操作,或者可以对对象应用哪些方法
//对象的状态(state):调用方法时,对象会如何响应
//对象的标识(identity):如何区分具有相同行为与状态的不同对象
//同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性
//对象的行为是用可调用的方法来定义的
//每个对象都保存着描述当前状况的信息,这就是对象的状态
//对象的状态可能随时间改变,但改变不会是自发的;对象状态的改变必须通过调用方法实现
//(如果不经过方法调用就可以改变对象状态,只能说明破坏了封装性)
//但对象的状态并不能完全描述一个对象,每个对象都有一个唯一的标识(identity,或称身份)
//总之,作为同一个类的实例,每个对象的标识[总是]不同的,状态也[往往]存在着差异
//对象的这些关键特性会彼此相互影响,例如对象的状态影响它的行为(如一个订单状态为已发货,就应该拒绝调用增删订单内容的方法)
/*识别类*/
//传统的过程式程序从顶部的main函数开始编写程序,面向对象程序设计时没有所谓的「顶部」
//[应该首先从识别类开始,然后再为各个类添加方法]
//识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词
//例如从订单处理系统中的名词可以得到商品(Item)类、订单(Order)类
//对于每一个动词,(添加、发货、取消、完成付款),都要识别出负责完成相应动作的对象
//例如,添加商品的add应该是Order类的一个方法,它要取一个Item对象作为参数
/*类之间的关系*/
//在类之间最常见的关系有:依赖(dependence, "uses-a")、聚合(aggregation, "has-a")、继承(inheritance, "is-a")
//依赖:一个类的方法使用或操纵另一个类的对象;是一种最明显最常见的关系
//应尽可能减少相互依赖的类,因为如果类A不知道B的存在,它就不会关心B的任何改变,意味着不会产生bug
//用软件工程术语就是,应尽可能减少类之间的[耦合]
//聚合:类A的对象包含类B的对象;例如一个Order对象包含一些Item对象
//继承:表示一个更特殊的类与一个更一般的类之间的关系
//例如RushOrder类由Order类继承而来,在更特殊的RushOrder类中包含了一些用于优先处理的特殊方法,还提供一个计算运费的不同方法
//而其他的方法如添加商品生成账单都是从Order类继承来的
//一般而言,如果类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能
//很多程序员采用UML(Unified Modeling Language 统一建模语言)绘制[类图],用来描述类之间的关系
//类用矩形表示,类之间的关系用带有各种修饰的箭头表示
//表达类关系的UML符号P95
preDefinedClass();
userDefinedClass();
staticFieldAndStaticMethod();
methodParameters();
objectConstruct();
usePackage();
jarFile();
javadoc();
classDesign();
}
public static void preDefinedClass() {
/*【使用预定义类】P96*/
//不是所有的类都表现出面向对象的典型特征,例如Math类只封装了功能
//因为它没有数据,所以也不必考虑创建对象和初始化它们的实例字段(因为根本没有实例字段)
/*对象与对象变量*/
//要使用对象,首先必须构造对象,并指定其初始状态,然后对对象应用方法
//Java中使用构造器(constructor,或称构造函数)构造新实例
//[构造器是一种特殊的方法],用来构造并初始化对象
//标准Java库中包含一个Date类,它的对象可以描述一个时间点
//[构造器的名字应该与类名相同],要构造一个Date对象,[需要在构造器前面加上new操作符]
//new Date() 这个表达式构造了一个新对象,这个对象被初始化为当前的日期和时间
System.out.println(new Date());
//需要的话,也可以将这个对象传递给一个方法
//也可以对刚刚创建的对象应用一个方法
String s1 = new Date().toString();
//上面两个例子中构造的对象仅使用了一次,要多次使用构造的对象,需要将对象存放在一个变量中
Date birthday = new Date();
//对象变量birthday,[引用了新构造的对象]
//对象和对象变量之前存在一个重要区别
Date deadline;
//上面的语句定义了一个对象变量,它可以[引用]Date类型的对象
//但变量deadline不是一个对象,实际上它也没有引用任何对象,此时还不能在这个变量上使用任何Date方法
//必须首先初始化变量deadline,这里可以初始化它,让它引用一个新构造的对象,也可以让它引用一个已有的对象
deadline = birthday;
//现在,这两个变量都[引用同一个对象]
//[对象变量并没有实际包含一个对象,它只是引用一个对象]
//在Java中[任何对象变量的值都是对存储在另一个地方的某个对象的引用]
//[new操作符的返回值也是一个引用]
//可以显式地将对象变量设置为null,指示这个对象变量目前没有引用任何对象
deadline = null;
//注:Java中的对象变量并不相当于C++中的引用,C++没有null引用且引用不能赋值
//可以把Java中的对象变量看作类似于C++的对象指针
//所有的Java对象都存储在堆中,当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针
//在Java中,如果使用一个未初始化的指针,运行时系统会产生一个运行时错误而不是生成一个随机结果
//垃圾回收器会处理内存管理相关事宜
//在Java中必须使用clone方法获得对象的完整副本
/*Java类库中的LocalDate类*/
//Date类的实例有一个状态,即特定的时间点
//尽管使用Date类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(正或负)表示的
//这个时间点就是纪元(epoch),它是UTC时间 1970/1/1 00:00:00
//UTC就是国际协调时间(coordinated universal time),与格林尼治时间GMT(Greenwich Mean Time)一样,是一种实用的科学标准时间
//类库设计者决定将保存时间与给时间点命名分开
//标准Java类库包含两个类:一个是表示时间点的Date类,一个是用日历表示法表示日期的LocalDate类
//这是很好的面向对象设计,通常最好使用不同的类表示不同的概念
//不要使用构造器来构造LocalDate类的对象
//应当使用静态工厂方法(factory method),它会代表你调用构造器
LocalDate.now();
//该表达式构造一个新对象,表示构造这个对象时的日期
//可以提供年月日来构造对应一个特定日期的对象
LocalDate.of(2022, 02, 22);
//将构造的对象保存在一个对象变量中
LocalDate newYearsEve = LocalDate.of(2022, 12, 31);
//有了LocalDate对象,就可以用方法得到年月日
int year = newYearsEve.getYear();
int month = newYearsEve.getMonthValue();
int day = newYearsEve.getDayOfMonth();
//有时可能会计算一个日期,plusDays方法得到一个新的LocalDate对象,是距当前对象指定天数的新日期
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
System.out.println(aThousandDaysLater.toString());
//Date类也有得到日月年的方法,但这些方法已经废弃,不鼓励使用,将来的某个类库版本可能会将它们完全删除
//JDK提供了jdeprscan工具来检查代码中是否使用了JavaAPI已经废弃的特性
/*更改器方法与访问器方法*/
//上面的plusDays方法会生成一个新的对象,它[没有更改]调用这个方法的对象
//而会改变对象状态的方法称为[更改器方法(mutator method)]
//相反,只访问对象而不修改对象的方法有时称为[访问器方法(accessor method)],例如getYear方法
//在C++中,带有const后缀的方法是访问器访问;没有声明为const的方法默认为更改器方法
//但在Java中,访问器方法与更改器方法在语法上没有明显区别
LocalDate date = LocalDate.now();
month = date.getMonthValue();
day = date.getDayOfMonth();
date = date.minusDays(day - 1);
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++) {
System.out.print(" ");
}
while (date.getMonthValue() == month) {
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == day) {
System.out.print("*");
} else {
System.out.print(" ");
}
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) {
System.out.println();
}
}
//minusDays方法生成当前日期之前n天的日期
System.out.println();
System.out.println("=".repeat(27));
}
public static void userDefinedClass() {
/*【用户自定义类】P103*/
//编写复杂应用程序所需要的主力类(workhorse class)通常没有main方法,却有自己的实例字段和实例方法
//Java中,最简单的类定义形式为
/*
class ClassName {
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}
*/
//下面是一个简单的类,可能用在工资管理系统中
//Employee.java
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl", 75000, 2017, 12, 15);
staff[1] = new Employee("Harry", 50000, 2019, 10, 1);
staff[2] = new Employee("Tony", 40000, 2020, 3, 15);
for (Employee e : staff) {
e.raiseSalary(5);
}
for (Employee e : staff) {
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay());
}
//源文件名必须与public类的名字相匹配,在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类
//许多程序员习惯于将每一个类存放在一个单独的源文件中
//编译时,如果Java编译器发现源文件中使用了Employee类,会查找Employee.class
//如果没有找到,会自动搜索Employee.java然后编译它
//如果Employee.java版本比已有的Employee.class文件版本更新,Java编译器就会自动重新编译这个文件
//Employee这个类的所有方法都被标记为public,关键字[public意味着任何类的任何方法都可以调用这些方法]
//Employee类的实例中有3个实例字段,用于存放将要操作的数据,关键字[private确保只有Employee类自身的方法才能访问这些实例字段]
//注:用public标记实例字段将允许程序中的任何方法对其进行读写,这就完全破坏了封装,强烈建议将实例字段标记为private
//有两个实例字段本身就是对象,name字段是String类对象,hireDay字段是LocalDate类对象
//这种情况十分常见:类包含的实例字段通常属于某个类类型
/*构造器*/
//构造器与类同名,构造Employee类的对象时,构造器将实例字段初始化为希望的初始状态
//构造器与其他方法有一个重要不同,构造器总是结合new运算符来调用
//不能对已存在的对象调用构造器来重新设置实例字段,这会产生编译错误
//构造器与类同名、每个类可以有一个以上的构造器、构造器可以有零或任意数量参数、构造器没有返回值、构造器总是伴随new操作符一起调用
//Java构造器的工作方式与C++一样,但要记住所有的Java对象都是在堆中构造的,构造器总是结合new操作符一起使用
//注意:尽量在所有的方法中都不要使用与实例字段同名的变量
/*[var关键字只能用于方法中的局部变量,参数和字段的类型必须声明]*/
/*使用null引用*/
//一个对象变量包含一个对象的引用,或null
//使用null值要非常小心,如果对null值应用一个方法,会产生NullPointerException异常
//这是一个很严重的错误,如果你的程序没有捕获异常,程序就会终止
//定义一个类时,最好清楚地知道哪些字段可能为null
//在上例中不用担心salary字段,因为它是基本类型,不可能是null
//hireDay字段肯定是非null的,因为它初始化为一个新的LocalDate对象
//但是name可能为null,如果调用构造器时为n提供的实参是null,name就会是null
//对此有2个解决方法,「宽容型」方法是把null参数转换为一个适当的非null值
//「严格型」方法是干脆拒绝null参数,如果有人用null名字构造一个Employee对象,就会产生NullPointerException异常
//Objects类对此提供了一个便利方法
//注:如果要接受一个对象引用作为构造参数,就要问问自己,是不是真的希望接受可有可无的值,如果不是,严格型方法更合适
/*隐式参数与显式参数*/
//raiseSalary方法有两个参数,第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类型的对象
//第二个参数是位于方法名后面括号中的数值,这是显式(explicit)参数
//(有人把隐式参数称为 方法调用的目标/接收者)
//显式参数显式地列在方法声明中
//在每一个方法中,关键字this指示隐式参数,可以改写为 this.salary += raise;
//有些程序员更偏爱这样的风格,因为可以将实例字段与局部变量明显地区分开来
//在C++中,通常在类的外面定义方法,如果在类的内部定义方法,这个方法将自动成为内联(inline)方法
//在Java中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法
//是否将某个方法设置为内联方法是Java虚拟机的任务,即时编译器会监视那些简短、经常调用且没有被覆盖的方法调用,并进行优化
/*封装的优点*/
//getName、getSalary、getHireDay方法都是典型的访问器方法;由于它们只返回实例字段值,因此又称为字段访问器
//标记字段为private,可以确保字段不会受到外界的破坏,一旦某个值出现错误,调试修改它的方法即可,如果字段是公共的,破坏这个字段值的代码可能出现在任何地方
//如果想要获得或设置实例字段的值,你需要提供下面三项内容:一个私有的数据字段,一个公共的字段访问器方法,一个公共的字段更改器方法
//相比直接提供一个公共字段,这样的好处是:
//可以改变内部实现,而除了该类的方法之外不会影响其他代码;更改器方法可以完成错误检查
//注意:[不要编写返回可变对象引用的访问器方法]
//LocalDate类没有更改器方法,而Date类有一个更改器方法setTime,Date对象是可变的,这一点就破坏了封装性
//这会使返回值与实例字段引用同一个对象,对返回值调用更改器方法就可以改变这个对象的私有状态
//如果需要返回一个可变对象的引用,[首先应该对它进行克隆(clone)]
/*基于类的访问权限*/
//方法可以访问 调用这个方法的对象 的私有数据,一个方法可以访问 [所属类的所有对象] 的私有数据
//例如Employee类的equals方法
//典型调用方式为 if (harry.equals(boss))...
//这个方法访问了boss的私有字段,这是合法的,因为boss也是Employee类型的对象,Employee类的方法可以访问任何Employee类型对象的私有字段
//注:C++也有同样的原则;方法可以访问所属类任何对象的私有特性(feature),而不仅限于隐式函数
/*私有方法*/
//有时你可能希望将一个计算代码分解成若干个独立的辅助方法,通常这些辅助方法不应该成为公共接口的一部分
//要实现私有方法,只需将关键字public改为private
//如果你改变了方法的实现方式,将没有义务保证私有方法仍然可用,只要方法是私有的,类的设计者就可以确信它不会在别处使用
/*final实例字段*/
//将实例字段定义为final,这样的字段必须在构造对象时初始化
//必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段
//例如,可以将name字段声明为final,因为对象构造之后,这个值不会再改变(没有setName方法)
//final修饰符对于类型为 基本类型 或 不可变类 的字段尤其有用(类中的所有方法都不会改变其对象,这样的类就是不可变的类,例如String类就是不可变的)
//对于可变的类,使用final修饰符可能会造成混乱:
//final关键字只表示存储在这个变量中的对象引用不会再指示另一个不同的对象,不过这个对象可以更改
System.out.println("=".repeat(50));
}
public static void staticFieldAndStaticMethod() {
/*【静态字段与静态方法】P115*/
/*静态字段*/
//如果将一个字段定义为static,每个类只有一个这样的字段;而非静态的实例字段,每个对象都有自己的一个副本
//即如果有1000个对象,则有1000个非静态实例字段id,分别对应每一个对象,但只有一个静态字段nextId
//[即使没有对象,静态字段也存在;它属于类,而不属于任何单个的对象]
//在一些面向对象程序设计语言中,静态字段被称为[类字段],属于「静态」只是沿用了C++的叫法,并无实际意义
//为Employee对象调用setId方法,对象的id字段被设置为静态字段nextId当前的值,并且静态字段nextId的值加1
/*静态常量*/
//静态变量使用得比较少,静态常量却很常用,例如在Math类中声明 public static final double PI = 3.1415926535;
//程序中就可以用Math.PI来访问这个常量
//如果省略关键字static,PI就变成了Math类的一个实例字段;也就是需要通过Math类的一个对象来访问PI,并且每个Math对象都有它自己的一个PI副本
//System.out也是一个静态常量,它在System类中的声明为 public static final PrintStream out = ...;
//前面提到最好不要有公共字段,而公共常量却没问题,因为它被声明为final,不允许重新赋值
//注:System类中有一个setOut方法可以将out设置为不同的流,这个方法能修改final变量的原因在于:
//setOut方法是一个原生方法,而不是在Java语言中实现的,原生方法可以绕过Java的访问控制机制,这是一种特殊的解决方法,不要模仿
/*静态方法*/
//[静态方法是不在对象上执行的方法],例如Math类的pow方法就是一个静态方法
//可以认为静态方法是没有this参数的方法(在一个非静态方法中,this指示这个方法的隐式参数)
//静态方法不能访问实例字段,因为它不能在对象上执行操作,但[静态方法可以访问静态字段]
int n = Employee.getNextId();
//可以提供类名调用这个方法,如果这个方法省略关键字static,就需要通过Employee类对象的引用来调用这个方法
//注意:可以用对象调用静态方法,但这样很容易造成混淆,因为静态方法的计算结果与对象毫无关系,建议使用类名而不是对象来调用静态方法
//下面两种情况下可以使用静态方法:
//方法不需要访问对象状态,它需要的所有参数都由显式参数提供;方法只需要访问类的静态字段
//Java的静态字段与静态方法在功能上与C++相同,但语法稍有不同
//C++要使用::操作符访问作用域之外的静态字段和静态方法
//起初,C引入关键字static是为了表示退出一个块后仍然存在的局部变量,在这种情况下术语静态是有意义的
//随后,static在C中有了第二个含义,表示不能从其他文件访问的全局变量和函数,为了避免引入新的关键字,重用了static
//最后,C++第三次重用了这个关键字,与前面赋予的含义完全无关,它指示属于类而不属于任何类对象的变量和函数,这个含义与Java中这个关键字的含义相同
/*工厂方法*/
//静态方法还有另一种常见的用途,类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象
//例如工厂方法LocalDate.now和LocalDate.of
//NumberFormat类如下生成不同风格的格式化对象
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x));
System.out.println(percentFormatter.format(x));
//为什么NumberFormat类不利用构造器完成这些操作呢,这主要有两个原因:
//1.无法命名构造器。构造器的名字必须与类名相同,但这里希望有两个不同的名字,分别得到货币实例和百分比实例。
//2.使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回DecimalFormat类的对象,这是NumberFormat的一个子类
/*main方法*/
//可以调用静态方法而不需要任何对象,例如不需要构造Math类的任何对象就可以调用Math.pow
//同理,main方法也是一个静态方法
//main方法不对任何对象进行操作;事实上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。
//提示:每一个类都可以有一个main方法,这是常用于对类进行单元测试的一个技巧
//如果该类是一个更大型应用程序的一部分,这个类的main方法就永远不会执行
}
public static void methodParameters() {
/*【方法参数】P121*/
//按值调用(call by value)表示方法接收的是调用者提供的值
//按引用调用(call by reference)表示方法接收的是调用者提供的变量地址
//方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值
//「按...调用(call by)」是一个标准的计算机科学术语,用来描述各种程序设计语言中方法参数的传递方式
//(以前还有按名调用call by name,Algol是最古老的高级程序设计语言之一,它使用的就是这种参数传递方式,但这种传递方式已经成为历史)
//[Java总是采用按值调用],方法得到的是所有参数值的一个副本
//然而,有两种类型的方法参数:基本数据类型、对象引用
//改变对象的状态是可以的,方法得到对象引用的副本,原来的对象引用和这个副本都引用同一个对象
//因为Java的对象引用是按值传递的,所以方法不能让一个对象参数引用一个新的对象
//C++中有按值调用和按引用调用,引用参数标有&符号
}
public static void objectConstruct() {
/*【对象构造】P126*/
//Java提供了多种编写构造器的机制
/*重载(overloading)*/
//如果多个方法有相同的名字、不同的参数,便出现了重载
var messages = new StringBuilder();
var todoList = new StringBuilder("To do:\n");
//编译器用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法
//查找匹配的过程被称为重载解析(overloading resolution)
//Java允许重载任何方法,因此要完整描述一个方法,需要指定方法名以及参数类型,这叫作方法的签名(signature)
//[返回类型不是方法签名的一部分],不能有两个名字、参数类型相同却有不同返回类型的方法
/*默认字段初始化*/
//如果在构造器中没有显式地为字段设置初值,那么它们会被自动赋为默认值:数值为0、布尔值为false、对象引用为null
//这是字段与局部变量的一个重要区别,方法中的局部变量必须明确初始化。但在类中,没有初始化的类字段会自动初始化为默认值。
/*无参数的构造器*/
//很多类都包含一个无参数的构造器,由它创建对象时,对象的状态会设置为适当的默认值
//例:Employee类的无参数构造器
//[如果写一个类时[没有编写构造器],就会为你提供一个无参数构造器,这个构造器将所有的实例字段设置为默认值]
//如果类中提供了至少一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的
//仅当类没有提供任何其他构造器的时候,你才会得到一个默认的无参数构造器
/*显式字段初始化*/
//确保不管怎样调用构造器,每个实例字段都设置为一个有意义的初值总是一个好主意
//可以在类定义中直接为任何字段赋值,在执行构造器之前先完成这个赋值操作
//初始值不一定是常量值,例如Employee.id
//C++不能直接初始化类的实例字段,所有的字段必须在构造器中设置
/*参数名*/
//避免使用单个字母作为参数名,可以在每个参数前面加上一个前缀a(例如aName、aSalary),也可以用this访问同名的实例字段
//C++中经常用下划线或某个固定字母作为实例字段的前缀(例如_name、mName、xName),Java程序员通常不这样做
/*调用另一个构造器*/
//关键字this除指示一个方法的隐式参数外还有其他含义
//如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器
//例如 Employee(double)
//在Java中,this引用等价于C++中的this指针,但C++的一个构造器不能调用另一个构造器
//在C++中,必须将抽取出的公共初始化代码编写成一个独立的方法
/*初始化块*/
//前面已经介绍了两种初始化数据字段的方法:在构造器中设置值、在声明中赋值
//Java还有第三种机制,称为初始化块(initialization block)
//在一个类的声明中,可以包含任意多个代码块,只要构造这个类的对象,这些块就会被执行
//例如 Employee 类的 object initialization block,无论哪个构造器构造对象,都会首先运行初始化块,然后才运行构造器的主体部分
//这种机制不是必需的,也不常见,通常会直接将初始化代码放在构造器中
//可以在初始化块中设置字段,即使这些字段在类后面才定义
//但为了避免循环定义,不允许读取在后面初始化的字段
//这些规则太过复杂,较早的Java版本中实现存在一些小错误,因此建议总是将初始化块放在字段定义之后
//如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块
//将代码放在一个块中,并标记关键字static
//例如 static initialization block 将员工ID的起始值赋予一个小于10000的随机整数
//在类第一次加载的时候,将会进行静态字段的初始化;与实例字段一样,除非显式设置为其他值,否则默认初始值是0、false、null
//所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行
/*下面是调用构造器的具体处理步骤*/
//1.如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
//2.否则
// a.所有数据字段初始化为默认值
// b.按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块
//3.执行构造器主体代码
/*对象析构与finalize方法*/
//C++有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码
//在析构器中,最常见的操作是回收分配给对象的存储空间
//由于Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不支持析构器
//但如果某些对象使用了内存之外的其他资源,例如文件或使用了系统资源的另一个对象的句柄
//在这种情况下,当资源不再需要时,将其回收和再利用显得十分重要
//如果一个资源一旦使用完就需要立即关闭,那么应当提供一个close方法来完成必要的清理工作,可以在对象使用完时调用这个close方法
//如果可以等到虚拟机退出,那么可以用方法 Runtime.addShutdownHook 增加一个「关闭钩」
//在Java9中,可以使用Cleaner类注册一个动作,当对象不再可达时(除了清洁器,其他对象都无法访问这个对象),就会完成这个动作
//在实际中这些情况很少见
//警告:不要使用finalize方法来完成清理,该方法已经被废弃
}
public static void usePackage() {
/*【包】P134*/
//Java允许使用包(package)将类组织在一个集合中
//借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理
/*包名*/
//使用包的主要原因是确保类名的唯一性,同名的类放在不同的包中,就不会产生冲突
//为保证包名的绝对唯一性,要用一个域名以逆序的形式作为包名,[然后对于不同的工程使用不同的子包]
//例如 hostname.com 得到包名 com.hostname,追加一个工程名得到 com.hostname.core,再把一个类放在这个包里 com.hostname.core.Employee
//从编译器的角度来看,嵌套的包之间没有任何关系,java.util包与java.util.jar包毫无关系,每个包都是独立的类集合
/*类的导入*/
//一个类可以使用[所属包中的所有类],以及[其他包中的公共类(public class)]
//可以采用两种方式访问另一个包中的公共类:
//一是用完全限定名(fully qualified name),即包名后跟类名,例如 java.time.LocalDate today = java..time.LocalDate.now();
//更简单常用的方式是使用import语句,import语句是一种引用包中各个类的简捷方式,使用了import语句,使用类时就不必写出类的全名了
//可以用import导入一个特定的类或者整个包,import语句应该位于源文件的顶部(但位于package语句的后面)
//下面的语句导入java.util包中的所有类 import java.util.*;
//还可以导入一个包中的特定类 import java.time.LocalDate;
//java.util.*的语法比较简单,[对代码的规模也没有任何负面影响],但明确指出所导入的类可以让代码的读者更准确知道你使用了哪些类
//只能用星号导入一个包,而不能使用 import java.* 或 java.*.*
//如果发生命名冲突的时候就要注意包了,例如java.util和java.sql包都有Date类,如果在程序中导入了这两个包,程序中使用Date类时就会出现一个编译错误
// 可以增加一个特定的import语句导入要用的Date类来解决这个问题,也可以在每个类名前加上完整的包名
//在包中定位类是编译器(compiler)的工作,类文件中的字节码总是使用完整的包名引用其他类
//C++的#include和Java的import之间没有共同之处
//C++中,必须使用#include来加载外部特性的声明,这是因为,除了正在编译的文件以及在头文件中明确包含的文件,C++编译器无法查看任何其他文件的内部
//Java编译器则不同,可以查看其他文件的内部,只要告诉它到哪里去查看就可以了
//在Java中通过显式给出类名可以避免使用import机制,而C++中无法避免使用#include指令
//[import语句的唯一好处是简捷],可以使用简短的名字来引用一个类
//在C++中,与包机制类似的是命名空间特性,可以认为Java中的package和import语句类似于C++中的namespace和using指令
/*静态导入*/
//这种import语句允许导入静态方法和静态字段,而不只是类
//import static java.lang.System.*;
//就可以使用System类的静态方法和静态字段,而不必加类名前缀:out.printLn();
//还可以导入特定的方法或字段
//import static java.lang.System.out;
/*在包中增加类*/
//要将类放入包中,就必须将包的名字放在源文件的开头,例如
/*
package com.hostname.core
public class Employee {
...
}
*/
//如果没有在源文件中放置package语句,这个源文件中的类就属于无名包(unnamed package),无名包没有包名,目前为止我们定义的所有类都在这个无名包中
//将源文件放在与完整包名匹配的子目录中,例如 com.hostname.core 包中的所有源文件应该放在子目录 com/hostname/core 中,编译器将类文件也放在相同的目录结构中
/*
.(base directory)
|-Unnamed.java
|-Unnamed.class
|-com/
|-hostname/
|-core/
|-Employee.java
|-Employee.class
*/
//在 Unnamed.java 中 import com.hostname.core.*; 并编译,编译器会自动到子目录查找文件并进行编译
//需要注意,编译器处理文件,而Java解释器加载类
//javac com/hostname/core/Employee.java
//java com.hostname.core.Employee
//警告:编译器在编译源文件时不检查目录结果,如果一个源文件开头有 package com.hostname; 即使这个源文件不在子目录com/hostname下也可以进行编译
//如果它不依赖于其他包,就可以通过编译而不会出现编译错误
//但最终的程序将无法运行,除非先将所有类文件移到正确的位置上
//如果包与目录不匹配,虚拟机就找不到类
/*包访问*/
//前面介绍的访问修饰符public和private
//标记为public的部分可以由任意类使用,private的部分只能由定义它们的类使用
//如果未指定public或private,这个部分(类、方法、变量)可以被同一个包(可以是无名包)中的所有方法访问
//注意,变量也必须显式标记为private,否则默认为包可见,这样会破坏封装性!
//在默认情况下,包不是封闭的实体,也就是说任何人都可以向包中添加更多的类
//恶意代码可能利用包的可见性添加一些能修改变量的代码;从1.2版本开始,JDK的实现者修改了类加载器,明确禁止加载包名以java.开头的用户自定义的类
/*类路径*/
//前面看到,类存储在文件系统的子目录中,类的路径必须与包名匹配
//类文件也可以存储在JAR(Java Archive,Java归档)文件中
//一个JAR文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能
//在程序中用到第三方的库文件时,你通常要得到一个或多个需要包含的JAR文件
//提示:JAR文件使用zip格式组织文件和子目录,可以用任何zip工具查看JAR文件
//为了是类能够被多个程序共享,需要设置类路径(class path),类路径是所有包含类文件的路径的集合
//在UNIX环境,类路径中的各项之间用冒号分隔:(假设包的基目录在/home/user/class,例如/home/user/class/com/hostname/core)
// /home/user/class:.:/home/user/archives/archive.jar
//在Windows环境,以分号分隔; 无论是UNIX还是Win,都用点(.)表示当前目录
//类路径包括基目录、当前目录、JAR文件
//从Java6开始,可以在JAR文件目录中指定通配符 /home/user/archives/'*' 或 C:\archives\*
//在UNIX中,*必须转义以防止shell扩展
//archives目录中的所有JAR文件(但不包括.class文件)都包含在这个类路径中
//由于总是会搜索Java API的类,所以不必显式包含在类路径中
//警告:javac编译器总是在当前的目录中查找文件,但Java虚拟机仅在类路径中包含.的时候才查看当前目录
//如果没有设置类路径,那没什么问题,因为默认的类路径会包含.目录
//但如果设置了类路径却忘记了.目录,那么可以通过编译但不能运行
//【类路径所列出的目录和归档文件是搜寻类的起始点】
//假定虚拟机要搜寻com.hostname.core.Employee类的类文件,它首先要查看Java API类
//显然,找不到相应的类文件,所以转而查看类路径,然后查找文件:
// /home/user/class/com/hostname/core/Employee.class
// (当前目录)com/hostname/core/Employee.class
// (/home/user/archives/archive.jar中)com/hostname/core/Employee.class
//编译器查找文件比虚拟机复杂得多
//如果引用了一个类而没有指定这个类的包,编译器将首先查找包含这个类的包
//它会查看所有的import指令,确定其中是否包含这个类,例如假设源文件包含指令:
// import java.util.*;
// import com.hostname.core.*;
//若源代码引用了Employee类,编译器将尝试查找:
// java.lang.Employee (因为 java.lang 包总是会默认导入)
// java.util.Employee
// com.hostname.core.Employee
// 当前包中的 Employee
//它会在类路径所有位置中搜索以上各个类,如果找到了一个以上的类,就会产生编译时错误
//(因为完全限定类名必须是唯一的,所以import语句的次序并不重要)
//编译器还要查看源文件是否比类文件新,如果是这样,那么源文件就会被自动重新编译
//只能导入其他包中的公共类,因为一个源文件只能包含一个公共类,文件名与公共类必须匹配,所以编译器能轻易找到公共类的源文件
//不过,[还可以从当前包中导入非公共类]
//这些类有可能在与类名不同的源文件中定义,[如果从当前包中导入一个类,编译器就要搜索当前包中的所有源文件],查看哪个源文件定义了这个类
/*设置类路径*/
//最好用-classpath(或 -cp,或Java9中的 --class-path)选项指定类路径
//>java -classpath /home/user/class:.:/home/user/archives/archive.jar MyProg
//也可以通过设置 CLASSPATH 环境变量来指定
//在 IntelliJ IDEA 中,每个蓝色的文件夹都可以理解为类路径的起始地点
//在 Java 9 中,还可以从模块路径加载类
}
public static void jarFile() {
/*【JAR文件】P143*/
//在打包应用程序时,你一定希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java归档(JAR)文件就是为此目的而设计的
//一个JAR文件既可以包含类文件,也可以包含图像声音等其他类型的文件
//此外,JAR文件是压缩的,它使用ZIP压缩格式
/*创建JAR文件*/
//可以使用jar工具制作JAR文件,常用以下的命令
//>jar cvf jarFileName.jar file1 file2 ...
//jar的选项有点类似于UNIX tar命令的选项
/*清单文件*/
//除类文件、图像和其他资源外,每个JAR文件还包含一个清单文件(manifest),用于描述归档文件的特殊特性
//清单文件被命名为 MANIFEST.MF,它位于JAR文件的一个特殊的 META-INF 子目录中
//符合标准的最小清单文件:
// Manifest-Version: 1.0
//复杂的清单文件可能包含更多条目,这些清单条目被分成多个节
//第一节被称为主节(main section),它作用于整个JAR文件
//随后的条目指定命名实体的属性,如单个文件、包、URL,它们都必须以一个Name条目开始,节与节之间用空格分开,例如:
/*
Manifest-Version: 1.0
lines describing this archive
Name: Woozle.class
lines describing this file
Name: com/mycompany/mypkg/
lines describing this package
*/
//编辑清单文件
//>jar cfm jarFileName manifestFileName ...
//创建一个包含清单文件的JAR文件
//>jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
//更新现存JAR文件的清单
//>jar ufm MyArchive.jar manifest-additions.mf
/*可执行JAR文件*/
//可以使用jar命令中的e选项指定程序的入口点
//>jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass [files to add]
//或者可以在清单文件中指定程序的主类
//Main-Class: com.mycompany.mypkg.MainAppClass
//注意:清单文件的最后一行必须以换行符结束,否则将无法被正确读取
//然后,用户可以简单地通过下面的命令来启动程序
//>java -jar MyProgram.jar
//在Windows上可以使用第三方的包装器工具将JAR转换成Windows可执行文件
//包装器可以查找和加载JVM,或在没有找到JVM时告诉用户应该做什么
//有许多商业的和开源的产品
/*多版本JAR文件*/
//随Java版本的更新,一些旧的API不再可用,但如果直接改用新版的API,你还需要向旧版Java环境用户发布不同的应用程序,或者需要用类加载和反射等一些技巧
//为解决类似问题,Java9引入了多版本JAR(multi-release JAR),其中可以包含面向不同Java版本的类文件
//为保证向后兼容,额外的类文件放在 META-INF/versions 目录中
/*
Application.class
BuildingBlocks.class
Util.class
META-INF
|-MANIFEST.MF (with line "Multi-Release: true")
|-versions
|-9
|-Application.class
|-BuildingBlocks.class
|-10
|-BuildingBlocks.class
*/
//Java8完全不知道 META-INF/versions 目录,而Java9读取这个JAR文件时则会使用新版本
//要增加不同版本的类文件,可以使用 --release 标志:jar uf MyProgram.jar --release 9 Application.class
//要从头构建一个多版本JAR文件,可以使用-C选项,对应每个版本要切换到一个不同的类文件目录:
//jar cf MyProgram.jar -C bin/8 . --release 9 -C bin/9 Application.class
//面向不同版本编译时,要使用--release标志和-d标志来指定输出目录:
//javac -d bin/8 --release 8 ...
//在Java9中,编译时可以将--release标志设置为9、8、7
//多版本JAR的唯一目的是能够在多个不同的JDK版本上运行,如果增加了功能或者改变了一个API,就应当提供一个新版本的JAR
/*关于命令行选项的说明*/
//P147
//Java9开始转向一种更常用的选项格式
//带--和多字母的选项的参数用空格或=分隔;单字母选项的参数用空格分隔
}
public static void javadoc() {
/*【文档注释】P148*/
//JDK包含的javadoc工具可以由源文件生成一个HTML文档
//Java API文档就是通过对标准Java类库的源代码运行javadoc生成的
/*注释的插入*/
//javadoc实用工具从下面几项中抽取信息:
//模块、包、公共类与接口、公共的和受保护的字段、公共的和受保护的构造器及方法
//可以且应该位以上各个特性编写注释,注释放置在所描述特性的前面
//注释以/**开始,并以*/结束
//每个/**..*/文档注释包含[标记]以及之后紧跟的[自由格式文本(free-form text)]
//[标记以@开始],如@since或@param
//自由格式文本的[第一句]应该是一个概要性的句子,javadoc工具自动地将这些句子抽取出来生成概要页
//自由格式文本可以使用HTML修饰符,例如用于强调的<em>...</em>、用于着重强调的<strong>...</strong>、用于项目符号列表的<ul>/<li>以及用于包含图像的<img ...>等
//要键入等宽代码,需要使用{@code ...}而不是<code></code>,这样就不用操心对代码中的<字符转义了
//如果文档中有到其他文件的链接,就应将这些文件放到包含源文件的目录下的一个子目录doc-files中,javadoc工具将从源目录将doc-files目录拷贝到文档目录中
//在链接中需要使用doc-files目录,例如:<img src="doc-files/uml.png" alt="UML diagram"/>
/*类注释*/
//类注释必须放在import语句之后,类定义之前;没有必要在每一行的开始都添加星号,但大部分IDE会自动提供星号
/*方法注释*/
//每个方法注释必须放在所描述的方法之前;除通用标记外还可以使用下面的标记:
//@param variable description
// 这个标记将给当前方法的参数(parameters)部分添加一个条目,这个描述可以占据多行,并可以使用HTML标记,一个方法的所有@param标记必须放在一起
//@return description
// 这个标记将给当前方法的返回(returns)部分,这个描述可以跨多行,并可以使用HTML标记
//@throws class description
// 这个标记表示这个方法有可能抛出异常
/*字段注释*/
//只需要对公共字段(通常指的是静态常量)建立文档
/*通用注释*/
//@since 会建立一个始于(since)条目,后跟文本可以是引入这个特性的版本的任何描述,例如 @since 1.7.1
//@author 这个标记将产生一个作者条目,可以使用多个@author标记,每个标记对应一个作者
//@version 将产生一个版本条目,这里的文本可以是对当前版本的任何描述
//@see和@link标记可以使用超链接,连接到javadoc文档的相关部分或外部文档
//@see reference 将在「see also(参见)」部分增加一个超链接,它可以用于类、方法
//这里的reference(引用)可以有以下选择:
// package.class#feature label
// <a href="...">label</a>
// "text"
//第一种情况是最有用的,只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接,例如:
// @see com.hostname.core.Employee#raiseSalary(double)
//这会建立一个链接到指定类的指定方法的超链接,可以省略包名、类名,这样默认位当前包或当前类中
//如果@see后面有一个<字符,就需要指定一个超链接,可以链接到任何URL
// @see <a href="hostname.com/core.html">Home page</a>
//如果@see后面有一个双引号字符,文本就会显示在 see also 部分
//可以为一个特性添加多个@see标记,但必须将它们放在一起
//还可以在文档注释中的任何位置放置指向其他类或方法的超链接,可以在注释中的任何位置插入一个形式如下的特殊标记:
// {@link package.class#feature label}
//在Java9中还可以使用 {@index entry} 标记为搜索框增加一个条目
/*包注释*/
//类、方法、变量的注释放置在Java源文件中
//而包注释需要在每一个包目录中添加一个单独的文件,可以有如下两个选择
// 1. 提供一个名为 package-info.java 的文件,它必须包含一个初始的以/**和*/界定的javadoc注释,后面是一个package语句,不能包含更多的代码或注释
// 2. 提供一个名为 package.html 的文件,会抽取标记<body>...</body>之间的所有文本
/*注释抽取*/
//P151
//javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...
//javadoc -d docDirectory *.java
//使用-author和-version选项在文档中包含@author和@version标记(默认情况下这些标记会被省略)
//还可以为所有的源文件提供一个概要注释,放在类似overview.html的文件中,提供选项-overview filename,将抽取<body></body>间的所有文本
}
public static void classDesign() {
/*【类设计技巧】P152*/
/* 1. 一定要保证数据私有 */
//这是最重要的,绝对不要破坏封装性
/* 2. 一定要对数据进行初始化 */
//Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化,不要依赖于系统的默认值,应该显式初始化所有的数据,可以提供默认值也可以在所有构造器中设置
/* 3. 不要在类中使用过多的基本类型 */
//要用其他的类替换使用多个相关的基本类型,这样会使类更易于理解,也更易于修改;例如用一个名为Address的类替换一堆String实例字段
/* 4. 不是所有的字段都需要单独的字段访问器和字段更改器 */
/* 5. 分解有过多职责的类 */
//如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解
/* 6. 类名和方法名要能体现它们的职责 */
//与变量应该有一个能够反映其含义的名字一样,类也应该如此
//惯例是:类名应该是一个名词,或是前面有形容词修饰的名词,或是有动名词(有-ing后缀)修饰的名词
//方法的标准惯例是:访问器方法用小写get开头,更改器方法用小写的set开头
/* 7. 优先使用不可变的类 */
//优先使用不可变的类(没有方法能修改对象的状态)
//如果多个线程试图同时更新一个对象,就会发生并发更改,其结果是不可预料的
//如果类是不可变的,就可以安全地在多个线程间共享其对象
}
}
Employee.java
import java.time.LocalDate;
import java.util.Objects;
import java.util.Random;
public class Employee {
/*instance fields 实例字段*/
/**
* Next employee ID.
*/
private static int nextId = 1;
private int id = assignId();
private final String name;
private double salary = 0;
private final LocalDate hireDay;
/*static initialization block*/
static {
//构造一个新的随机数生成器
var generator = new Random();
//返回一个0~n-1之间的随机数
nextId = generator.nextInt(10000);
}
/*object initialization block*/
{
//id = nextId;
//nextId++;
}
/*constructor 构造器*/
public Employee() {
this.name = "";
this.salary = 0;
this.hireDay = LocalDate.now();
}
public Employee(double aSalary) {
//calls Employee(String, double)
this("Employee #" + nextId, aSalary);
nextId++;
}
public Employee(String aName, double aSalary) {
name = aName;
salary = aSalary;
hireDay = LocalDate.now();
}
public Employee(String name, double salary, int hireYear, int hireMonth, int hireDay) {
//把null参数转换为一个适当的值
//if (name == null) this.name = "unknown"; else this.name = name;
//或者
//this.name = Objects.requireNonNullElse(name, "unknown");
//拒绝null参数
Objects.requireNonNull(name, "The name cannot be null.");
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(hireYear, hireMonth, hireDay);
//this关键字可用于引用当前类的实例变量
}
/*单元测试用*/
public static void main(String[] args) {
var e = new Employee("Romeo", 50000, 2003, 3, 31);
e.raiseSalary(10);
System.out.println(e.getName() + " " + e.getSalary());
}
/*method 方法*/
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
/**
* Raises the salary of an employee.
*
* @param byPercent the percentage by which to raise the salary (e.g., 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
return raise;
}
public boolean equals(Employee other) {
return this.name.equals(other.name);
}
public void setId() {
this.id = nextId;
nextId++;
}
public static int assignId() {
int temp = nextId;
nextId++;
return temp;
}
public static int getNextId() {
return nextId;
//返回静态字段
}
}
继承
package top.konoka.newbie.chapter5;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
/**
* 继承
*/
public class Chapter5 {
public static void main(String[] args) throws Exception {
//继承(inheritance)是面向对象程序设计的另一个基本概念
//继承的基本思想是,可以基于已有的类创建新的类
//继承已存在的类就是复用(继承)这些类的方法,且可以增加一些新的方法和字段,使新类能够适应新的情况
//反射(reflection)是指在程序运行期间更多地了解类及其属性的能力
classSuperclassAndSubclass();
objectClass();
genericArrayList();
objectWrapperAndAutoboxing();
varargsMethod();
enumClass();
reflection();
designInheritance();
}
public static void classSuperclassAndSubclass() {
/*【类、超类和子类】P155*/
//"is-a"(是)关系是继承的一个明显特征,例如每个经理都是一个员工
/*定义子类*/
//可以继承 Employee 类来定义 Manager 类,使用关键字 extends 表示继承
// public class Manager extends Employee { }
//Java用关键字extends代替了C++中的冒号,在Java中,所有继承都是公共继承,没有C++中的私有继承和保护继承
//extends 表明正在构造的新类派生于一个已存在的类
//这个已存在的类称为超类(superclass)、基类(base class)或父类(parent class)
//新类称为子类(subclass)、派生类(derived class)或孩子类(child class)
//超类和子类是Java程序员最常用的两个术语;子类比超类拥有的功能更多
//尽管 Manager 类中没有显式定义getName和getHireDay等方法,但可以对 Manager 对象使用这些方法
//因为 Manager 类自动继承了超类 Employee 中的这些方法
//Manager类还从超类中继承了 name、salary、hireDay 这3个字段,每个Manager对象就包含4个字段
//通过扩展超类定义子类的时候,只需要指出子类与超类的不同之处
//因此在设计类时,应该将最一般的方法放在超类中,而将更特殊的方法放在子类中
//这种将通用功能抽取到超类的做法在面向对象程序设计中十分普遍
/*覆盖方法*/
//超类中的有些方法对子类不一定适用,为此,需要提供一个新的方法来覆盖(override)超类中的这个方法
//注意,只有 Employee 方法能直接访问 Employee 类的私有字段,Manager 类的 getSalary 方法不能直接访问 salary 字段
//而要像所有其他方法一样使用公共接口 getSalary()
//但还不能直接调用getSalary(),这只是在调用自身,因为Manager类也有一个getSalary方法(就是我们正在实现的方法),所以这条语句将会导致无限次调用自身
//需要使用关键字 super 表示我们希望调用超类中的方法
//super.getSalary()
//注意:super与this引用不是类似的概念,[super不是一个对象的引用],例如不能将值super赋给另一个对象变量
//super只是一个指示编译器调用超类方法的特殊关键字
//在子类中可以增加字段、方法或覆盖方法,[但继承绝对不会删除任何字段或方法]
//注:Java使用关键字super调用超类的方法,而C++则采用超类名加::操作符的形式,例如 super.getSalary 要写成 Employee::getSalary
/*子类构造器*/
//子类构造器中的关键字super具有不同的含义,它是「调用超类中带有指定参数的构造器」的简写形式
//由于子类不能访问超类的私有字段,所以必须通过一个构造器来初始化这些私有字段
//[使用super调用构造器的语句必须是子类构造器的第一条语句]
//如果子类构造器没有显式调用超类构造器,[将自动调用超类的无参数构造器]
//如果超类没有无参数构造器,且子类构造器中又没有显式调用超类的其他构造器,[Java编译器就会报告一个错误]
//【注】关键字this有两个含义:指示隐式参数的引用、调用该类的其他构造器
//类似地,super也有两个含义:调用超类的方法、调用超类的构造器
//[调用构造器的语句只能作为另一个构造器的第一条语句出现],构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器
//在C++的构造器中,会使用初始化列表语法调用超类的构造器,例:
// Manager::Manager(String name, double salary, int year, int month, int day)
// : Employee(name, salary, year, month, day) {
// bonus = 0;
// }
//构建一个Manager对象
Manager boss = new Manager("Carl Cracker", 80000, 2020, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
//以Manager和Employee对象填充staff数组
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 2021, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 2022, 3, 15);
//输出所有Employee对象的信息
for (Employee e : staff) {
System.out.println(e.getName() + " " + e.getSalary());
}
//注意,尽管这里将 e 声明为 Employee 类型,但实际上 e 既可以引用 Employee 类型的对象,也可以引用 Manager 类型的对象
//虚拟机知道e实际引用的对象类型,因此能正确调用相应的方法
//[一个对象变量可以指示多种实际类型的现象称为多态(polymorphism)]
//[在运行时能自动选择适当的方法,称为动态绑定(dynamic binding)]
//在C++中如果希望实现动态绑定,需要将成员函数声明为virtual
//在Java中动态绑定是默认行为,如果不希望让一个方法是虚拟的,可以将它标记为final
/*继承层次*/
//继承并不仅限于一个层次,例如可以由Manager类派生Executive类
//由一个公共超类派生出来的所有类的集合称为继承层次(inheritance hierarchy)
//从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)
//通常,一个祖先类可以有多个子孙链,例如可以由Employee类派生出子类Programmer和Secretary,它们与Manager类没有任何关系
//注:在C++中一个类可以有多个超类;Java不支持多重继承,但提供了一些类似多重继承的功能,详见接口
/*多态*/
//「is-a」规则可以用来判断是否应该将数据设计为继承关系
//「is-a」规则的另一种表述是替换原则(substitution principle),它指出[程序中出现超类对象的任何地方都可以使用子类对象替换]
//例如,可以将子类的对象赋给超类变量
//在Java中,对象变量是多态的(polymorphic)
//一个超类类型的变量可以引用该超类的任何一个子类的对象
//例如,上例中staff[0]与boss引用同一个对象,但编译器只将staff[0]看成是一个Employee对象
//这意味着可以调用 boss.setBonus(5000); 但不能调用 staff[0].setBonus(5000);
//这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法
//不过,不能将超类的引用赋给子类变量,原因很清楚,不是所有的员工都是经理
//警告:在Java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换
Manager[] managers = new Manager[10];
Employee[] staff2 = managers;
//这样做似乎不会有问题,因为Manager一定是一个Employee
//但实际上会发生一些奇怪的事情,要切记managers和staff2引用的是同一个数组
//staff2[0] = new Employee("TEST", 10000, 2020, 1, 1);
//在这里,staff2[0]和managers[0]是相同的引用,似乎我们把一个普通员工擅自归入经理行列中了
//当调用 manager[0].setBonus(1000) 的时候,将会试图调用一个不存在的实例字段,进而搅乱相邻存储空间的内容
//为确保不发生这类破坏,所有数组都要牢记创建时的元素类型,并负责监督仅将类型兼容的引用存储到数组中
//上面试图存储一个Employee类型的引用到使用new Manager[10]创建的数组会引发ArrayStoreException异常
/*理解方法调用*/
//假设要调用 x.f(args),隐式参数 x 声明为类 C 的一个对象,下面是调用过程的详细描述:
// 1. 编译器查看对象的声明类型和方法名。编译器会一一列举C类中所有名为f的方法及其超类中所有名为f且可访问的方法(超类的私有方法不可访问)
// 2. 接下来,编译器确定方法调用中提供的参数类型。如果在所有名为f的方法中存在一个参数类型完全匹配的方法,就选择它
// 这个过程称为重载解析(overloading resolution),由于允许类型转换,情况可能会变得很复杂
// 如果没有匹配的方法或经过类型转换后有多个方法匹配,编译器就会报告一个错误
// (方法的名字和参数列表称为方法的签名,在子类中覆盖一个超类方法时,需要保证返回类型的兼容性)
// ([允许覆盖方法返回原返回类型的子类型],例如超类返回Employee类型子类返回Manager类型的覆盖方法,我们说这两个方法有[可协变]的返回类型)
// 3. 如果是private、static、final方法、构造器,编译器可以准确地知道应调用哪个方法,[这称为静态绑定(static binding)]
// 如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定
// 在本例中,编译器会利用动态绑定生成一个调用f(args)的指令
// 4. 程序运行并采用动态绑定调用方法时,虚拟机必须调用x所引用对象的实际类型对应的方法
// 假设x的实际类型是D(C的子类),如果D定义了签名匹配的方法则调用该方法,否则在D的超类中寻找签名匹配的方法,以此类推
// 每次调用方法都要完成这个搜索,时间开销相当大,因此虚拟机预先为每个类计算了一个方法表(method table)
// [方法表中列出了所有方法的签名和要调用的实际方法,这样在真正调用方法时,虚拟机仅查找这个表就行了]
// 在本例中,虚拟机搜索D类的方法表,寻找匹配的方法,它可能是D.f(),也有可能是X.f(),X是D的某个超类
// 如果调用是super.f(param),那么编译器将对隐式参数超类的方法表进行搜索
//上面调用e.getSalary()的过程:
//e声明为Employee类型,Employee类只有一个名叫getSalary的方法,没有参数,因此这里不必担心重载解析的问题
//由于getSalary不是private、static或final方法,所以将采用动态绑定
//虚拟机为Employee和Manager类生成方法表,在Employee的方法表中列出了这个类本身定义的所有方法:(这里略去了Object方法)
/*
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
//Manager方法表稍有不同,其中3个方法是继承而来的,1个方法是重新定义的,还有1个方法是新增加的
Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)
*/
//调用e.getSalary()的解析过程为:
// 1.虚拟机获得e的实际类型的方法表,可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表
// 2.虚拟机查找定义了getSalary()签名的类,此时虚拟机已经知道应调用哪个方法
// 3.虚拟机调用该方法
//[动态绑定有一个非常重要的特性:无须对现有代码进行修改就可以扩展程序]
//假设新增一个Executive类,且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码重新进行编译
//警告:覆盖一个方法时,「子类方法不能低于超类方法的可见性」,特别是如果超类方法是public,子类方法必须也要声明为public
//如果子类方法遗漏了public修饰符,编译器就会报错,指出你试图提供更严格的访问权限
/*阻止继承:final类和方法*/
//不允许扩展的类被称为final类,使用final修饰符定义:(阻止派生Executive类的子类)
// public final class Executive extends Manager {}
//类中的某个特定方法也可以声明为final,这样子类就不能覆盖这个方法 (final类中的所有方法自动称为final方法)
//对于final字段来说,构造对象之后就不允许改变值了,如果将一个类声明为final,只有其中的方法自动成为final,[而不包括字段]
//声明方法或类为final的主要原因是:确保它们不会在子类中改变语义
//String类也是final类,这意味着不允许任何人定义String的子类,换言之,如果有一个String引用,它引用的一定是一个String对象而不可能是其他类的对象
//注:有些程序员认为除非有足够的理由使用多态性,否则应该将所有的方法都声明为final;事实上,在C++和C#中若没有特别说明,所有的方法都不使用多态性
//这两种做法可能都有些偏激,在设计类层次时,要仔细思考应该将哪些方法和类声明为final
//在早期的Java中,有程序员为了避免动态绑定带来的系统开销而使用final
//如果一个方法没有被覆盖且很短,编译器就能对它进行内联(inlining)处理优化,例如内联调用e.getName()将被替换为访问字段e.name
//这是一项很有意义的改进,CPU在处理当前指令时,分支会扰乱预取指令的策略,所以CPU不喜欢分支
//然而,如果getName在另一个类中被覆盖,那么编译器就无法知道覆盖的代码会做什么操作,也就不能进行内联处理了
//幸运的是,虚拟机中的即时编译器比传统编译器的处理能力强得多
//这种编译器可以准确知道类之间的继承关系,并能检测出是否有类确实覆盖了给定的方法
//如果方法很简短、被频繁调用且确实没有被覆盖,那么即时编译器就会将这个方法内联处理
// 如果虚拟机加载了另外一个子类,而这个子类覆盖了一个内联方法,那么优化器将取消对这个方法的内联;这个过程很慢,不过很少会发生这种情况
/*强制类型转换*/
//正像有时候需要将浮点数转换成整数一样,有时候也需要将某个类的对象引用转换成另一个类的对象引用
//进行强制类型转换的唯一原因是:[要在暂时忽视对象的实际类型之后使用对象的全部功能]
Manager boss0 = (Manager) staff[0];
//staff[i]引用一个Employee对象(因此它还可以引用Manager对象)
//将一个值存入变量时,编译器将检查你是否承诺过多
//将一个子类的引用赋给一个超类变量,编译器是允许的
//但将一个超类引用赋给一个子类变量时,承诺就过多了;必须进行强制类型转换,这样才能通过运行时检查
//如果试图在继承链上进行向下的强制类型转换,并且「谎报」对象包含的内容
//Manager boss1 = (Manager) staff[1];
//Java运行时系统会注意到你的承诺不符,并产生一个ClassCastException异常
//因此,应该养成这样一个良好的程序设计习惯:[在进行强制类型转换前,先查看是否能够成功地转换]
//使用instanceof操作符就可以实现
if (staff[1] instanceof Manager) {
boss = (Manager) staff[1];
}
//如果这个类型转换不可能成功,编译器就不会让你完成这个转换
// · 只能在继承层次内进行强制类型转换
// · 在将超类强制转换成子类之前,应该使用instanceof进行检查
//注:如果x为null,x instanceof C 不会产生异常,只是返回false,因为null没有引用任何对象,当然也不会引用C类型的对象
//实际上,通过强制类型转换来转换对象的类型通常不是一个好的做法,因为实现多态性的动态绑定机制能够自动地找到正确的方法
//只有在使用Manager中特有的方法时才需要进行强制类型转换,例如setBonus
//如果出于某种原因发现需要在Employee上调用setBonus,那么就应该自问超类的设计是否合理;可能需要重新设计超类并添加setBonus方法,这才是更合适的选择
//一般情况下,最好尽量少用强制类型转换和instanceof运算符
//Java的强制类型转换处理过程有点像C++的dynamic_cast操作,它们之间只有一点重要区别,转换失败时Java不会生成null对象,从这个意义上看有点像C++中的引用转换
//上面的例子在C++中可以写成
// Manager* boss = dynamic_cast<Manager*>(staff[1]);
// if (boss != NULL) ...
/*抽象类*/
//类的继承层次位于上层的类更有一般性,可能更加抽象
//从某种角度看,祖先类更有一般性,人们只将它作为派生其他类的基类,而不是用来构造你想使用的特定的实例
//例如,可以让Employee从Person类继承,引入一个更高层次抽象的公共超类,就可以把getName方法放到继承层次结构中更高的一层
//现在增加一个getDescription方法返回对一个人的简短描述,在Employee类很容易实现这个方法,但Person类对这个人一无所知
//可以让Person.getDescription()返回空字符串,但还有一个更好的方法
//使用 abstract (抽象) 关键字,就完全不需要实现这个方法了
// public abstract String getDescription();
//为了提高程序的清晰度,[包含一个或多个抽象方法的类本身必须被声明为抽象的]
// public abstract class Person {...}
//除了抽象方法外,抽象类还可以包含字段和具体方法
//提示:建议尽量将通用的字段和方法(不管是否是抽象的)放在超类(不管是否是抽象的)中
//抽象方法充当着占位方法的角色,它们在子类中具体实现
//扩展抽象类有两种选择:在子类中保留抽象类中的部分或所有抽象方法(这样就必须将子类也标记为抽象类)、定义全部方法(这样子类就不是抽象的了)
//[即使类不含抽象方法,也可以声明为抽象类]
//[抽象类不能实例化]
//可以定义一个抽象类的对象变量,但这样一个变量[只能引用非抽象子类的对象]
//在C++中有一种抽象方法称为纯虚函数(pure virtual function),要在末尾用 =0 标记
/*
class Person {
public:
virtual string getDescription() = 0;
...
}
*/
//如果至少有一个纯虚函数,这个C++类就是抽象类,C++中没有提供用于表示抽象类的特殊关键字
//可以在抽象类的对象变量上调用只在子类定义了的抽象方法,由于不能构造抽象类的对象,所以抽象类的对象变量只能引用具体子类的对象,而这些对象中都定义了该方法
//但如果省略了超类中的抽象方法,就不能在该抽象类的变量上调用对应方法了,编译器只允许调用在类中声明的方法
//在Java中,抽象方法是一个重要的概念,在接口(interface)中将会看到更多抽象方法
/*受保护访问*/
//任何声明为private的内容对其他类都是不可见的,这对于子类来说也完全适用,子类也不能访问超类的私有字段
//但有时候你可能希望限制超类中的某个方法、字段[只允许子类访问]
//为此,需要将这些类方法或字段声明为受保护(protected)
//在Java中,[保护字段只能由同一个包中的类访问],如果子类在另一个不同的包中,就不允许访问,这可以避免滥用保护机制,不能通过派生子类来访问受保护的字段
//实际应用中要谨慎使用受保护字段,其他程序员可能会由这个类再派生出新类并访问你的受保护字段,这样如果要修改你的类的实现,就会影响这些人
// 这违背了OOP提倡的数据封装精神
//受保护的方法更具有实际意义,声明为protected表明子类(可能很熟悉祖先类)得到了信任,可以正确地使用这个方法,而其他类则不行
//这个方法的一个很好的示例就是Object类的clone方法
//事实上,Java中的受保护部分对所有子类及同一个包中的所有其他类都可见,这与C++中的保护机制稍有不同,Java中的protected概念要比C++中的安全性差
/*Java的4个访问控制修饰符*/
// 仅对本类可见——private
// 对外部完全可见——public
// 对本包和所有子类可见——protected
// 对本包可见——默认
}
public static void objectClass() {
/*【Object:所有类的超类】P174*/
//Object类是Java中所有类的始祖,再Java中每个类都扩展了Object
//如果没有明确指出超类,Object就被认为是这个类的超类,Java中每个类都是由Object类扩展而来的
/*Object类型的变量*/
//可以用Object类型的变量引用任何类型的对象
Object obj = new Employee("Harry", 35000, 2021, 12, 20);
//当然,Object类型的变量只能用于作为各种值的一个泛型容器,要想进行具体操作还需要清楚对象的原始类型,并进行强制类型转换
Employee e = (Employee) obj;
//在Java中,[只有基本类型(primitive type)不是对象]
//所有的数组类型,不管是对象数组还是基本类型的数组,都扩展了Object类
Employee[] staff = new Employee[10];
obj = staff;
obj = new int[10];
//C++中没有所有类的根类,但每个指针都可以转换成void*指针
/*equals方法*/
//Object类中的equals方法用于检测一个对象是否等于另外一个对象
//Object类中实现的equals方法确定两个对象引用是否相等,这是一个合理的默认行为
//但我们经常需要基于状态检测对象的相等性,如果两个对象有相同的状态,才认为这两个对象是相等的
// 见 Employee.equals()
//getClass()方法返回一个对象所属的类,在本例只有两个对象属于同一个类时,才有可能相等
//提示:为了防备name或hireDay可能为null的情况,应该使用 Object.equals 方法
// 如果ab都为null,Objects.equals(a, b)返回true,如果其中一个为null返回false;否则,如果两个参数都不为null,则调用a.equals(b)
//在子类中定义equals方法时,首先调用超类的equals,如果检测失败对象就不可能相等
//超类中的字段都相等,才需要比较子类中的实例字段
// 见 Manager.equals()
/*相等测试与继承*/
//若隐式和显式参数不属于同一个类,equals方法将如何处理呢,前例中如果发现类不匹配,equals方法就返回false
//但许多程序员却喜欢用 instanceof 进行检测
// if (!(otherObject instanceof Employee)) return false;
//[这样就允许 otherObject 属于一个子类],但这种方法可能会招致一些麻烦,所以建议不要采用这种处理方式
// [Java语言规范要求equals方法具有下面的特征]:
//1.自反性:对于任何非空引用x,x.equals(x) 应该返回true
//2.对称性:对于任何引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)返回true
//3.传递性:对于任何引用x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true
//4.一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果
//5.对于任意非空引用x,x.equals(null)应该返回false
//就对称性规则来说,参数不属于同一个类时会有一些微妙的结果,例如调用 e.equals(m),e是一个Employee对象,m是一个Manager对象
// 并且两个对象有相同的名字薪水和雇佣日期;如果在 Employee.equals() 中用 instanceof 进行检测,这个调用将返回true
// 然而这意味着反过来调用m.equals(e)也需要返回true,对称性规则不允许这个调用返回false或抛出异常
// 这就使Manager类受到了束缚,它的equals方法必须愿意将自己与任何一个Employee对象进行比较而不考虑经理特有的那部分信息
//这让人感觉 instanceof 测试并不是那么好
//有些作者认为getClass的检测是有问题的,因为它违反了替换原则
// 有一个经常提到的例子是AbstractSet类的equals方法,它检测两个集合是否有相同的元素
// AbstractSet类有两个具体提的子类:TreeSet和HashSet,它们分别用不同的算法查找集合元素
// 但无论集合采用何种方式实现,你肯定希望能够比较任意的两个集合
//不过,集合是非常特殊的一个例子,应该将AbstractSet.equals声明为final,因为没有任何一个子类需要重新定义集合相等的语义
// (事实上,这个方法没有被声明为final,这是为了让子类实现更高效的算法来完成相等性检测)
// [就现在来看,有两种完全不同的情形]:
// · 如果子类可以有自己的相等性概念,则对称性需求将强制使用 getClass 检测
// · 如果由超类决定相等性概念,就可以使用 instanceof 检测,这样可以在不同子类的对象之间进行相等性比较
//在上例中,如果使用员工的ID作为相等性检测标准,且这个相等性概念适用于所有的子类,就可以使用 instanceof 检测,且应该将 Employee.equals 声明为final
//注:在标准Java库中包含上百个equals方法的实现,包括使用instanceof检测、调用getClass、捕获ClassCastException或者什么也不做等各种做法
// [下面给出编写一个完美的equals方法的建议]:
// 1. 显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量
// 2. 检测 this 与 otherObject 是否相等
// 3. 检测 otherObject 是否为 null
// 4. 比较 this 与 otherObject 的类;如果equals的语义可以在子类中改变,就使用getClass检测;如果所有的子类都有相同的相等性语义,可以使用instanceof检测
// 5. 将 otherObject 强制转换为相应类类型的变量
// 6. 根据相等性概念的要求来比较字段,使用==比较基本类型字段,使用 Objects.equals 比较对象字段
// 如果在子类中重新定义equals,就要在其中包含一个 super.equals(other)调用
// 对于数组类型的字段,可以使用静态的 Arrays.equals 方法(两数组长度相同且对应位置上数据元素也相同则返回true)检测相应的数组元素是否相等
//注意:如果声明 public boolean equals(Employee other),这个方法的显式参数类型是Employee,因此它没有覆盖Object类的equals方法
//为了避免这种错误,[可以用 @Override 标记要覆盖超类方法的那些子类方法],如果出现错误且正在定义一个新方法,编译器就会报告一个错误
/*hashCode方法*/
//散列码(hash code)是由对象导出的一个整数值;如果x和y是两个不同对象,x.hashCode()与y.hashCode()基本上不会相同
//字符串的hash是由内容导出的,而 Object 类的默认 hashCode 方法会从[对象的存储地址]得出散列码
System.out.println("OK".hashCode() + ", " + "OK".hashCode());
var sb = new StringBuilder("OK");
System.out.println(sb.hashCode());
/*String类使用以下算法计算hash code:
int hash = 0;
for (int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
*/
// [如果重定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法]
//hashCode方法应该返回一个整数(可以是负数),要合理组合实例字段的散列码,以便能够让不同对象产生的散列码分布更加均匀
/*例如
public int hashCode() {
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
}
*/
//最好使用 null 安全的方法 Objects.hashCode,如果参数为null这个方法会返回0
//更好的做法是:需要组合多个散列值时,可以调用Objects.hash并提供所有这些参数
//这个方法会对各个参数调用Objects.hashCode并组合这些散列值
// [equals与hashCode的定义必须相容]:
// 如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()返回相同的值
//例如,如果定义Employee.equals比较员工的ID,那么hashCode方法就需要散列ID而不是员工的姓名或存储地址
//提示:可以使用静态的 Arrays.hashCode 方法计算一个数组的散列码,这个散列码由数组元素的散列码组成
/*toString方法*/
//返回表示对象值的一个字符串
//Point类的toString方法返回:java.awt.Point[x=10,y=10]
//绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值
//例如 Employee.toString()
//最好通过 getClass().getName() 获得类名的字符串,而不要将类名硬编码写进去
System.out.println(e.toString());
//设计子类的程序员应该定义自己的toString方法并加入子类的字段
//如果超类使用了 getClass().getName(),那么子类只要调用 super.toString() 就可以了
Manager m = new Manager("Tim", 60000, 2022, 1, 1);
System.out.println(m.toString());
//随处可见toString方法的主要原因是:
// [只要对象与一个字符串通过操作符+连接起来,Java编译器就会自动调用toString方法来获得这个对象的字符串描述]
//提示:可以不写为x.toString(),而写作 ""+x
//将一个空串与x的字符串表示连接;与toString不同的是,即使x是基本类型,这条语句照样能执行
//Object类定义了toString方法,可以打印对象的类名和散列码
System.out.println(System.out);
//PrintStream类的实现者没有覆盖toString方法
//注意:令人烦恼的是,数组继承了Object类的toString方法,且数组类型将采用一种古老的格式打印
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers);
//前缀[I表明是一个整型数组
System.out.println(staff);
//应该调用静态方法 Arrays.toString 打印数组
System.out.println(Arrays.toString(numbers));
//要打印多维数组,则需要调用 Arrays.deepToString 方法
//toString方法是一种非常有用的调试工具
//标准类库中许多类都定义了toString方法,以便用户能获得一些有关对象状态的有用信息
//强烈建议为自定义的每一个类添加toString方法,所有使用这个类的程序员都会从这个日志记录支持中受益匪浅
}
public static void genericArrayList() {
/*【泛型数组列表】P186*/
//Java允许在运行时确定数组的大小
int n = 1;
var staff0 = new Employee[n];
//但这并没有完全解决运行时动态更改数组的问题
//ArrayList类类似于数组,但在添加或删除元素时,它能够自动调整数组容量
//ArrayList是一个有类型参数(type parameter)的泛型类(generic class)
//为了指定数组列表保存的元素对象的类型,需要用一对尖括号将类名追加到ArrayList后面
/*声明数组列表*/
ArrayList<Employee> staff1 = new ArrayList<Employee>();
//可以如下简写
var staff2 = new ArrayList<Employee>();
ArrayList<Employee> staff3 = new ArrayList<>();
//这称为菱形语法,可以结合new操作符使用菱形语法
//编译器会检查新值要做什么
//[如果赋值给一个变量,或传递给某个方法,或从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>中]
var elements = new ArrayList<>();
//不要这样调用,这样会生成一个ArrayList<Object>
//添加元素到数组列表中
staff1.add(new Employee("Tony", 50000, 2020, 12, 1));
//数组列表管理着一个内部的对象引用数组,这个数组的空间有可能全部用尽
//如果调用add而内部数组已满,数组列表就会自动创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中
//如果已知或能估计出数组可能存储的元素数量,就可以在填充数组之前调用
staff2.ensureCapacity(100);
//这个方法调用将分配一个包含100个对象的内部数组,这样前100次add就不会带来开销很大的重新分配空间
//还可以把初始容量直接传递给构造器
var staff4 = new ArrayList<Employee>(100);
//size方法返回数组列表中包含的实际元素个数
System.out.println(staff1.size());
//等价于数组a的a.length
//一旦能确定数组列表的大小不再变化,就可以调用trimToSize方法,将存储块的大小调整为保存当前元素数量所需的存储空间,垃圾回收器将回收多余的存储空间
staff1.trimToSize();
//一旦削减了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会再向数组列表添加任何元素时再调用trimToSize
//ArrayList类似于C++的vector模板,它们都是泛型类型,但C++的vector模板为了便于访问元素重载了[]运算符
//由于Java没有运算符重载,所以必须调用显式的方法
//此外,C++向量是按值拷贝,a=b将构造一个与b长度相同的新向量a,并将所有元素由b拷贝到a,而在Java中这只会让ab引用同一个数组列表
/*访问数组列表元素*/
//不能用[]语法访问数组列表元素,而要用get和set方法
Employee e = new Employee("Tim", 20000, 2020, 10, 1);
staff1.set(0, e);
e = staff1.get(0);
//注意:要使用add添加新元素,set只用于替换已经加入的元素
//可以使用toArray方法将数组元素拷贝到一个数组中
var a = new Employee[staff1.size()];
staff1.toArray(a);
//add方法提供一个索引参数可以在数组列表中间插入元素
n = 0;
staff1.add(n, e);
//位置n及之后的所有元素都要向后移动一个位置
//从数组列表中删除一个元素
e = staff1.remove(n);
//位于这个位置之后的所有元素都向前移动一个位置,并且数组大小减1
//插入和删除元素的操作效率很低,如果存储的元素数较多又经常要插入删除元素,应该考虑使用链表
//可以使用for each遍历数组列表
for (Employee e0 : staff1) {
System.out.println(e0);
}
/*类型化与原始数组列表的兼容性*/
//下面介绍如何与没有使用类型参数的遗留代码交互操作
/*
public class EmployeeDB {
public void update(ArrayList list) {...}
public ArrayList find(String query) {...}
}
*/
//可以将一个类型化的数组列表传递给update方法而不需要任何强制类型转换
//ArrayList<Employee> staff = ...;
//EmployeeDB.update(staff);
//在update方法中,添加到数组列表中的元素可能不是Employee类型
//相反。将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告
//ArrayList<Employee> result = EmployeeDB.find(query);
//使用强制类型转换并不能避免出现警告
//在这种情形下,你并不能做什么,在与遗留代码交互时,要研究编译器的警告,确保这些警告不太严重就行了
//一旦确保问题不大,可以用 @SuppressWarnings("unchecked") 注解来标记接受强制类型转换的变量
//@SuppressWarnings("unchecked") ArrayList<Employee> result = (ArrayList<Employee>) EmployeeDB.find(query);
}
public static void objectWrapperAndAutoboxing() {
/*【对象包装器与自动装箱】P192*/
//所有基本类型都有一个与之对应的类,通常这些类称为包装器(wrapper)
//包装器类有显而易见的名字:Integer、Long、Float、Double、Short、Byte、Character、Boolean (前六个类派生于公共的超类Number)
//[包装器类是不可变的],一旦构造了包装器就不允许更改包装在其中的值;同时,[包装器类还是final],不能派生它们的子类
//定义一个整型数组列表就需要用到包装器类,尖括号中的类型参数不允许是基本类型
var list = new ArrayList<Integer>();
//警告:由于每个值分别包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组
//只有操作方便性比执行效率更重要时,才会考虑对较小的集合使用这种构造
//有一个很有用的特性,可以很容易向ArrayList<Integer>添加int类型的元素
list.add(3);
//这个调用会自动变换成 list.add(Integer.valueOf(3));
//这种变换称为自动装箱(autoboxing)
//相反地,将一个Integer对象赋给一个int时,将会自动地拆箱
int n1 = list.get(0);
//转换成 list.get(0).intValue();
//自动装箱和拆箱也适用于算术表达式,可以将自增运算符应用于一个包装器引用
Integer n2 = 3;
n2++;
//编译器将自动插入一条对象拆箱的指令,然后进行自增运算,最后再将结果装箱
//==运算符可以应用于包装器对象,不过检测的是对象是否有相同的内存位置
//比较两个包装器对象时应调用equals方法
//注:自动装箱规范要求boolean、byte、char<=127,介于-128和127之间的short和int呗包装到固定的对象中
//由于包装器类引用可以为null,自动装箱有可能会抛出一个NullPointerException异常
//装箱和拆箱是编译器要做的工作,而不是虚拟机
//编译器在生成类的字节码时会插入必要的方法调用,虚拟机只是执行这些字节码
//使用数值包装器通常还有一个原因,Java设计者发现可以将某些基本方法放在包装器中,这会很方便
//例如将字符串转换成整型
int x = Integer.parseInt("123");
System.out.println(x);
//这里与Integer对象没有任何关系,parseInt是一个静态方法,但Integer类是放置这个方法的一个好地方
//包装器类不能用于实现修改数值参数的方法,Integer对象是不可变的
//如果确实想编写一个修改数值参数的方法,可以使用 org.omg.CORBA 包中定义的某个持有者(holder)类型,包括IntHolder、BooleanHolder等
//每个持有者类型都包含一个公共字段value,通过它可以访问存储在其中的值
}
public static void varargsMethod() {
/*【参数数量可变的方法】P195*/
//可以提供参数数量可变的方法,有时这些方法被称为变参(varargs)方法
//例如 public PrintStream printf(String format, Object... args)
//这里的省略号表明这个方法可以接收任意数量的对象(除format参数之外)
//实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有其他参数(如果提供的是基本类型值,会自动装箱)
//换句话说,对于printf实现者来说,Object...参数类型与Object[]完全一样
//编译器需要转换每个printf调用,将参数绑定到数组中,并在必要的时候进行自动装箱
//System.out.println("%d %s", new Object[] { new Integer(n), "widgets" });
//可以为参数指定任意类型,甚至是基本类型,示例如下
System.out.println(max(3.1, 40.4, -5, 50.2, 71.8));
//编译器将 new double[] {...} 传递给 max 方法
//允许将数组作为最后一个参数传递给有可变参数的方法
System.out.printf("%d %s", new Object[]{Integer.valueOf(1), "widgets"});
//因此,如果一个已有方法的最后一个参数是数组,可以把它重新定义为有可变参数的方法,而不会破坏任何已有的代码
//如果愿意,甚至可以将main声明为 public static void main(String... args)
}
private static double max(double... values) {
double largest = Double.NEGATIVE_INFINITY;
for (double v : values) {
if (v > largest)
largest = v;
}
return largest;
}
public static void enumClass() {
/*【枚举类】P196*/
//前面已介绍如何定义枚举类型
//public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
//实际上,这个声明定义的类型是一个类,它刚好有4个实例,不可能构造新的对象
//因此,在比较两个枚举类型的值时,不需要调用equals,直接用==即可
//如果需要的话,可以为枚举类型增加构造器、方法和字段,当然构造器只在构造枚举常量时调用
//[枚举的构造器总是私有的],可以省略,如果声明一个enum构造器为public或protected,会出现语法错误
//[所有的枚举类型都是Enum类的子类],继承了这个类的许多方法
//最有用的一个是toString,这个方法返回枚举常量名,例如 Size.SMALL.toString() 返回字符串 "SMALL"
//toString的逆方法是静态方法valueOf
Size s = Enum.valueOf(Size.class, "SMALL");
//将s设置为Size.SMALL
//每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组
Size[] values = Size.values();
System.out.println(Arrays.toString(values));
//ordinal方法返回enum声明中枚举常量的位置,从0开始计数,例如 Size.MEDIUM.ordinal() 返回1
System.out.println(Size.MEDIUM.ordinal());
System.out.println(s.getAbbreviation());
//注:Enum类有一个类型参数,为简单起见省略了这个类型参数
//例如,实际上枚举类型Size扩展了Enum<Size>
//类型参数会在compareTo方法中使用
}
public enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}
public static void reflection() throws Exception {
/*【反射】P198*/
//反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序
//使用反射,Java可以支持用户界面生成器、对象关系映射器以及很多其他需要动态查询类能力的开发工具
//能够分析类能力的程序称为反射(reflective)
//反射机制可以用来:
// · 在运行时分析类的能力
// · 在运行时检查对象,例如编写一个适合于所有类的toString方法
// · 实现泛型数组操作代码
// · 利用Method对象,这个对象很像C++中的函数指针
//反射是一种功能强大且复杂的机制,主要是开发工具的程序员对它感兴趣,一般的应用程序员不需要考虑反射机制
/*Class类*/
//在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类
//虚拟机利用运行时类型信息选择要执行的正确的方法
//可以使用一个特殊的Java类访问这些信息;保存这些信息的类名为Class
//Object类中的getClass()方法将会返回一个Class类型的实例
Employee e = new Employee("Hacker", 50000, 2020, 12, 1);
Class cl = e.getClass();
//Class对象会描述一个特定类的属性,最常用的Class方法可能是getName,它返回类的名字
System.out.println(cl.getName());
System.out.println(e.getClass().getName() + " " + e.getName());
//如果类在一个包里,包的名字也作为类名的一部分
//还可以使用静态方法forName获得类名对应的Class对象
try {
String className = "top.konoka.newbie.chapter5.Employee";
cl = Class.forName(className);
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
//如果className是一个类名或接口名,这个方法可以正常执行,否则forName方法抛出一个检查型异常(checked exception)
//无论何时使用这个方法,都应该提供一个异常处理器(exception handler)
//提示:在启动时,包含main方法的类呗加载;它会加载所有需要的类,这些被加载类又要加载它们需要的类
//对一个大型程序来说,这会花费很长时间;可以使用这个技巧给用户一种启动速度较快的假象
// (不过,要确保包含main方法的类没有显式地引用其他的类)
//首先,显示一个启动画面,然后通过调用Class.forName手工地强制加载其他类
//获得Class类的第三种方法是一个很方便的快捷方式
//如果T是任意的Java类型(或void关键字),T.class 将代表匹配的类对象
Class cl1 = Random.class;
Class cl2 = int.class;
Class cl3 = Double[].class;
//注意,一个Class对象实际上代表的是一个类型,这可能是类也可能不是类,例如int不是类,但int.class是一个Class类型的对象
//注:Class类实际上是一个泛型类,例如Employee.class的类型是Class<Employee>
//大多数实际问题中,可以忽略类型参数,而使用原始的Class类
//警告:因历史原因,getName方法在应用于数组类型的时候会返回有些奇怪的名字
System.out.println(Double[].class.getName());
System.out.println(int[].class.getName());
//虚拟机为每个类型管理一个唯一的Class对象,因此可以用==运算符比较两个类对象
if (e.getClass() == Employee.class) ;
//如果e是一个Employee实例,这个测试将通过;与条件e instanceof Employee不同,如果e是某个子类的实例,这个测试将失败
//如果有一个Class类型的对象,可以用它构造类的实例
//调用getConstructor方法将得到一个Constructor类型的对象,然后使用newInstance方法来构造一个实例
try {
String className = "top.konoka.newbie.chapter5.Employee";
Class cl0 = Class.forName(className);
Object obj = cl0.getConstructor().newInstance();
//如果这个类没有无参数的构造器,getConstructor方法会抛出一个异常
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
ex.printStackTrace();
}
//newInstance方法相当于C++中的虚拟构造器概念;不过C++中的虚拟构造器不是一个语言特性而是需要专业库支持的习惯用法
/*声明异常入门*/
//运行时发生错误,程序会抛出一个异常,可以提供一个处理器(handler)捕获异常并进行处理;如果没有提供处理器,程序就会终止
//异常有两种类型:非检查型(unchecked)异常、检查型(checked)异常
//检查型异常编译器会检查你是否知道这个异常并做好准备来处理后果;但有很多常见的异常例如越界和访问null,都属于非检查型异常,编译器并不期望你为它提供处理器
//毕竟,你应该集中精力避免这些错误的发生,而不要将精力花在编写异常处理器上
//不是所有的错误都是可以避免的,如果竭尽全力还是可能发生异常,大多数JavaAPI都会抛出一个检查型异常
//Class.forName方法就是一个例子,没有办法确保指定名字的类一定存在
//异常处理有多种策略,这里先介绍最简单的一个:
// 如果一个方法包含一条可能抛出检查型异常的语句,则在方法名上增加一个throws字句 (调用这个方法的任何方法也都需要一个throws声明,包括main)
// public static void reflection() throws ClassNotFoundException {
//为检查型异常提供一个throws字句,就很容易找出哪些方法会抛出检查型异常
//只要你调用了一个可能抛出检查型异常的方法而没有提供相应的异常处理器,编译器就会报错
/*资源*/
//类通常有一些关联的数据文件,例如图像和声音文件、包含消息字符串和按钮标签的文本文件
//在Java中,这些关联的文件被称为资源(resource)
//例如一个显示版本信息的窗口,每次更新都会发生变化,为了易于追踪这个变化,我们希望将文本放在一个文件中,而不是以字符串的形式硬编码写到代码中
//应该将about.txt这样的文件放在哪里呢,当然,将它与其他程序文件一起放在JAR文件中是最方便的
//Class类提供了一个很有用的服务可以查找资源文件
//1.获得拥有资源的类的Class对象,例如 ResourceTest.class
//2.有些方法,如ImageIcon类的getImage方法,接受描述资源位置的URL,则要调用 URL url = cl.getResource("about.gif")
//3.否则,使用 getResourceAsStream 方法得到一个输入流来读取文件中的数据
//这里的重点在于,Java虚拟机知道如何查找一个类,所以它能搜索相同位置上的关联资源
//例如,假设类ResourceTest在一个resources包中,ResourceTest.class文件就位于resources目录中,可以把一个图标文件放在同一目录下
//除了可以将资源文件与类文件放在同一个目录中,还可以提供一个相对或绝对路径,如 data/about.txt
//另一个经常使用资源的地方是程序的国际化。与语言相关的字符串,如消息和用户界面标签都存放在资源文件中,每种语言对应一个文件
//国际化API(internationalization API)支持一种标准方法来组织和访问本地化文件
//例程:ResourceTest.java
/*利用反射分析类的能力*/
//反射机制最重要的内容——检查类的结构
//java.lang.reflect包中有三个类Field、Method、Constructor分别用于描述类的字段、方法和构造器
//这3个类都有一个叫getName的方法,用来返回字段、方法或构造器的名称
//Field类有一个getType方法,用来返回描述字段类型的一个对象,这个对象的类型同样是Class
//Method、Constructor类有报告参数类型的方法,Method类还有一个报告返回类型的方法
//这3个类都有一个getModifiers方法,它返回一个整数,用不同的0/1位描述所使用的修饰符,如public和static
//还可以用java.lang.reflect包中Modifier类的静态方法分析getModifiers返回的这个整数
//例如,可以用Modifier类中的isPublic、isPrivate、isFinal判断方法或构造器的修饰符
//还可以利用Modifier.toString方法将修饰符打印出来
//Class类的getFields、getMethods、getConstructors方法分别返回这个类支持的公共字段、方法、构造器的数组,其中包括超类的公共成员
//Class类的getDeclareFields、getDeclareMethods、getDeclareConstructors返回类声明的全部字段、方法、构造器的数组,包括私有、包、受保护成员,但不包括超类成员
//例程:ReflectionTest.java
//这个程序可以分析Java解释器能够加载的任何类,而不仅仅是编译程序时可以使用的类
/*使用反射在运行时分析对象*/
var hacker = new Employee("Hacker", 50000, 2021, 10, 1);
Class cl4 = hacker.getClass();
Field f = cl4.getDeclaredField("name");
//Object v = f.get(hacker);
//f.set(hacker, "Harry");
//只能对可访问字段使用get、set
//反射机制的默认行为受限于Java的访问控制
//覆盖Java访问控制
//f.setAccessible(true);
// 若不允许访问,setAccessible抛出一个异常,访问可以被模块系统、安全管理器拒绝
f.trySetAccessible();
Object v = f.get(hacker);
System.out.println(v);
f.set(hacker, "RESET");
System.out.println(hacker.getName());
// todo P209~213 可用于任意类的通用toString方法
// - 用不上,跳过
/*使用反射编写泛型数组代码*/
// todo P213~216
/*调用任意方法和构造器*/
//C/C++中,可以通过一个函数指针执行任意函数,表面上Java没有提供方法指针
//Java的设计者曾说过,方法指针是很危险的,且容易出错;它们认为Java的接口(interface)和lambda表达式是一种更好的解决方案
//不过,反射机制允许你调用任意方法
//类似Field类的get方法,Method类有一个invoke方法,允许你调用包装在当前Method对象中的方法
//该方法第一个参数是隐式参数,其余的对象提供显式参数,对于静态方法可以忽略第一个参数,即设为null
//如果返回类型是基本类型,invoke方法会返回其包装器类型
Class cl5 = Employee.class;
Method m1 = cl5.getMethod("getSalary");
double s = (Double) m1.invoke(hacker);
System.out.println(s);
//可以调用getDeclareMethods搜索返回的对象数组找到想要的方法,也可以调用getMethod
//getField方法根据标识字段名的字符串返回一个Field对象,但有可能存在重名方法,所以getMethod还需要提供想要的方法的参数类型
Method m2 = cl5.getMethod("raiseSalary", double.class);
//可以使用类似的方法调用任意的构造器
Constructor cons = cl5.getConstructor();
Object obj = cons.newInstance();
Employee e1 = (Employee) obj;
System.out.println(e1.getName());
//如果调用方法时提供了错误的参数,invoke会抛出一个异常
//invoke的参数和返回值必须是Object类型,这意味着必须来回进行多次强制类型转换,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误
//使用反射获得方法指针的代码要比直接调用方法的代码慢得多
//建议仅在绝对必要的时候才在你的程序中使用Method对象,通常更好的做法是使用接口以及lambda表达式
//建议Java开发者不要使用回调函数的Method对象,可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护
}
public static void designInheritance() {
/*【继承的设计技巧】P219*/
// 1. 将公共操作和字段放在超类中
// 2. 不要使用受保护的字段
// protected机制不能带来更多的保护,一是子类集合是无限制的,任何人都能派生一个子类,从而破坏封装性
// 二是Java中,同一个包中的所有类都能访问protected字段,不管它们是否为这个类的子类
// 但protected方法对指示那些不提供一般用途而应在子类中重新定义的方法很有用
// 3. 使用继承实现「is-a」关系
// 4. 除非所有继承的方法都有意义,否则不要使用继承
// 5. 在覆盖方法时,不要改变预期的行为
// 6. 使用多态,而不要使用类型信息
// 只要看到类似下面的代码
// if (x is of type 1) action1(x);
// else if (x is of type 2) action2(x);
// 都应该考虑使用多态
// action1和2表示的是相同的概念吗?如果是,就应该为这个概念定义一个方法,并将其放置在两个类型的超类或接口中,然后就可以调用
// x.action();
// 使用多态性固有的动态分配机制执行正确的动作;使用多态方法或接口实现的代码比使用多个类型检测的代码更易于维护和扩展
// 7. 不要滥用反射
}
}
Employee.java
package top.konoka.newbie.chapter5;
import java.time.LocalDate;
import java.util.Objects;
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee() {
this.name = "Employee" + LocalDate.now();
this.salary = 0.0;
this.hireDay = LocalDate.now();
}
public Employee(String name, double salary, int hireYear, int hireMonth, int hireDay) {
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(hireYear, hireMonth, hireDay);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
salary += raise;
}
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) {
return true;
}
if (otherObject == null) {
return false;
}
if (getClass() != otherObject.getClass()) {
return false;
}
Employee other = (Employee) otherObject;
return Objects.equals(name, other.name)
&& salary == other.salary
&& Objects.equals(hireDay, other.hireDay);
}
@Override
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
@Override
public String toString() {
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
}
Manager.java
package top.konoka.newbie.chapter5;
import java.util.Objects;
public class Manager extends Employee {
private double bonus;
/**
* A Manager
*
* @param name the employee's name
* @param salary the salary
* @param hireYear the hire year
* @param hireMonth the hire month
* @param hireDay the hire day
*/
public Manager(String name, double salary, int hireYear, int hireMonth, int hireDay) {
super(name, salary, hireYear, hireMonth, hireDay);
this.bonus = 0;
}
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
@Override
public boolean equals(Object otherObject) {
if (!super.equals(otherObject)) return false;
//超类已确认this和otherObject属于相同类
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), bonus);
}
@Override
public String toString() {
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
}
ResourceTest.java
package top.konoka.newbie.chapter5.resource;
import javax.swing.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class ResourceTest {
public static void main(String[] args) throws IOException {
Class cl = ResourceTest.class;
URL aboutURL = cl.getResource("about.gif");
var icon = new ImageIcon(aboutURL);
InputStream stream1 = cl.getResourceAsStream("data/about.txt");
var about = new String(stream1.readAllBytes(), StandardCharsets.UTF_8);
InputStream stream2 = cl.getResourceAsStream("data/title.txt");
var title = new String(stream2.readAllBytes(), StandardCharsets.UTF_8).trim();
JOptionPane.showMessageDialog(null, about, title, JOptionPane.INFORMATION_MESSAGE, icon);
}
}
ReflectionTest.java
package top.konoka.newbie.chapter5;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/**
* 利用反射打印一个类的信息
*/
public class ReflectionTest {
public static void main(String[] args) throws Exception {
//读入类名
String name;
if (args.length > 0) {
name = args[0];
} else {
var in = new Scanner(System.in);
System.out.println("Enter class name (e.g. java.util.Date): ");
name = in.next();
}
//打印类名和超类名
Class cl = Class.forName(name);
Class superclass = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (superclass != null && superclass != Object.class) System.out.print(" extends " + superclass.getName());
System.out.print(" {\n");
printFields(cl);
System.out.println();
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println("}");
}
/**
* 打印类的所有字段
*
* @param cl 一个类
*/
public 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 + ";");
}
}
/**
* 打印类的所有构造器
*
* @param cl 一个类
*/
public 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());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
//打印参数类型
Class[] paramTypes = c.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) System.out.print(", ");
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* 打印类的所有方法
*
* @param cl 一个类
*/
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
//打印修饰符、返回类型、类名
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
//打印参数类型
Class[] paramTypes = m.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) System.out.print(", ");
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
}
接口、lambda表达式与内部类
import interfaces.Employee;
import interfaces.LengthComparator;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.IntConsumer;
public class Chapter6 {
public static void main(String[] args) {
/*接口(interface)*/
//接口用来描述类应该做什么,而不指定它们具体应该如何做
//一个类可以实现(implement)一个或多个接口
/*lambda表达式*/
//这是一种很简洁方法,用来创建可以在将来某个时间点执行的代码块
//通过使用lambda表示,可以用一种精巧而简介的方式表示使用回调或可变行为的代码
/*内部类(inner class)机制*/
//内部类定义在另外一个类的内部,它们的方法可以访问包含它们的外部类的字段
//内部类在设计具有协作关系的类集合时很有用
/*代理(proxy)*/
//代理是一种实现任意接口的对象
//它是一种非常专业的构造工具,可以用来构建系统级的工具
interfaceAndImplement();
lambdaExp();
innerClass();
serviceLoader();
proxy();
}
public static void interfaceAndImplement() {
/*【接口】P222*/
//接口的概念
//在Java中,接口不是类,而是对希望符合这个接口的类的一组需求
//示例
//Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:
// 对象所属的类必须实现Comparable接口
/* 下面是Comparable接口的代码
public interface Comparable {
int compareTo(Object other);
}
*/
//这说明,任何实现Comparable接口的类都需要包含compareTo方法,这个方法有一个Object参数,且返回一个整数
/* 注:在Java5中,Comparable接口已经提升为一个泛型类型
public interface Comparable<T> {
int compareTo(T other);
}
*/
//仍然可以使用不带类型参数的「原始」Comparable类型,这样compareTo就有一个Object类型的参数,你必须手动将这个参数强制转换为所希望的类型
//接口中的所有方法都自动是public方法,因此在接口中声明方法时,不必提供关键字public
//当然,这个接口还有一个没有明确说明的附加要求:在调用x.compareTo(y)时,这个方法必须确实能够比较两个对象,并返回比较的结果
// 当x小于y时返回一个负数,x等于y时返回0,否则返回一个正数
//接口还可以定义常量
//但接口绝不会有实例字段,在Java8之前,接口中不会实现方法(现在已经可以在接口中提供简单方法了;当然这些方法不能引用实例字段——接口没有实例)
//提供实例字段和方法实现的任务应该由实现接口的那个类来完成
//为了让类实现一个接口,通常需要完成下面两个步骤:
// 1. 将类声明为实现给定的接口
// 2. 对接口中的所有方法提供定义
//要将类声明为实现某个接口,需要使用关键字implements
//例程:Employee.java
//为泛型Comparable接口提供一个类型参数,就不需要对Object参数进行类型转换了
//注意:声明接口不需要声明public,因为接口中所有方法都自动是public,但实现接口时必须把方法声明为public
// 否则,编译器将认为这个方法的访问属性是包可见性,这是类的默认访问属性,之后编译器就会报错,指出你试图提供更严格的访问权限
//提示:如果 return id - other.id; 整数的范围不能过大,以免造成减法运算的溢出
// 如果能确定ID为非负整数或它们的绝对值不会超过(Integer.MAX_VALUE-1)/2,就不会出现问题
// 否则,调用静态 Integer.compare 方法
// 这不适用于浮点数,因为salary和other.salary很接近时,它们的差经过四舍五入后可能变成0
// 当x<y时,Double.compare(x, y) 会返回-1,x>y则返回1
//注释:Comparable接口的文档建议compareTo方法应当与equals方法兼容,也就是x.equals(y)时,x.compareTo(y)就应当等于0
// 大多数实现Comparable接口的类都遵从了这个建议,但有一个重要的例外,就是BigDecimal
// x = new BigDecimal("1.0") 和 y = ...("1.00") 这里 x.equals(y) 为false,因为两个数精度不同,但x.compareTo(y)为0
// 理想结果应该不返回0,但没有明确的方法能够确定这两个数哪一个更大
//现在我们已经知道,要让一个类使用排序服务必须让它实现compareTo方法,这是理所当然的,因为要向sort方法提供对象的比较方式
//但为什么不能直接在Employee类中直接提供compareTo方法而必须实现Comparable接口呢?
// 主要原因在于Java是一种强类型(strongly typed)语言
// 在调用方法的时候,编译器要能检查这个方法确实存在
//注:语言标准规定,对于任意的x和y,实现者必须能够保证sgn(x.compareTo(y)) = -sgn(y.compareTo(x))
// (也就是说,如果y.compareTo(x)抛出一个异常,那么反过来也应该抛出一个异常)
// 这里的sgn是一个数值的符号,如果n是负值,sgn(n)=-1,n是0等于0,n是正值等于1
// 简单地讲,如果翻转compareTo的参数,结果的符号也应该翻转(但具体值不一定)
// 与equals方法一样,在继承中有可能出现问题
// Manager扩展了Employee,而Employee实现的是Comparable<Employee>,而不是Comparable<Manager>
// 如果Manager要实现compareTo,就必须做好准备比较经理与员工,绝不能仅仅将员工强制转换成经理
// 因为这违反了反对称规则,如果x是一个Employee对象,y是一个Manager对象,调用x.compareTo(y)不会抛出异常,但反过来会抛出一个ClassCastException
//
// 补救方法:如果不同子类中的比较有不同的含义,就应该将属于不同类的对象之间的比较视为非法,每个compareTo都应该进行以下检测
// if(getClass() != other.getClass()) throw new ClassCastException();
// 如果存在一个能够比较子类对象的通用算法,那么可以在超类中提供一个compareTo方法,并将它声明为final
/*接口的属性*/
//接口不是类,不能使用new运算符实例化一个接口
//但尽管不能构造接口的对象,却能声明接口的变量
//[接口变量必须引用实现了这个接口的类对象]
Comparable x = new Employee("Tim", 50000);
//接下来,如同使用instanceof检查一个对象是否属于某个特定类一样,也可以用instanceof检查一个对象是否实现了某个特定的接口
Employee jim = new Employee("Jim", 30000);
if (jim instanceof Comparable) System.out.println("jim instanceof Comparable");
//与建立类的继承层次一样,也可以扩展接口
//这里允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口 public interface Powered extends Movable {
//虽然接口中不能包含实例字段,但可以包含常量 double SPEED_LIMIT = 95; //a public static final constant
//与接口中的方法都被自动设置为public一样,接口中的字段总是 public static final
//有些接口只定义常量,而没有定义方法
//尽管每个类只能有一个超类,但却[可以实现多个接口]
//例如Java中有一个非常重要的内置接口,Cloneable
//如果某个类实现了Cloneable,Object类中的clone方法就可以创建你的类对象的一个准确副本
//可以用逗号将想要实现的接口分隔开
// public class Employee implements Cloneable, Comparable<Employee> {
/*接口与抽象类*/
//为什么Java要引入接口概念而不将Comparable直接设计成一个抽象类呢?abstract class Comparable { public abstract int compareTo(Object o); }
//使用抽象类表示通用属性存在一个严重的问题:每个类只能扩展一个类
//但每个类可以实现多个接口
//有些语言如C++允许一个类有多个超类,这称为多重继承(multiple inheritance)
//Java选择不支持多重继承,主要原因是多重继承会让语言变得更加复杂(如C++)
//实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性
//注:C++的多重继承特性随之带来了一些复杂的特性,如虚基类、控制规则和横向指针类型转换,等等
//很少有C++程序员使用多重继承,也有些人建议只对“混合”风格的继承使用多重继承
//在“混合”风格中,一个主要基类描述父对象,其他的基类(所谓的混合类)提供辅助特性
//这种风格类似于一个Java类扩展一个基类且派生并实现多个接口
/*静态和私有方法*/
//在Java8中允许在接口中增加静态方法,理论上没有理由认为这是不合法的,只是这有违于将接口作为抽象规范的初衷
//目前为止,通常的做法都是将静态方法放在伴随类中
//在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections或Path/Paths
Path s1 = Paths.get("conf", "security", "admin");
//Path接口提供了等价的方法
s1 = Path.of("conf", "security", "admin");
//这样一来,Paths类就不再是必要的了
//类似地,实现你自己的接口时,没有理由再为实用工具方法另外提供一个伴随类
//Java9中,接口中的方法可以是private,private方法可以是静态方法或实例方法
//由于私有方法只能在接口本身的方法中使用,所有它们的用法很有限,只能作为接口中其他方法的辅助方法
/*默认方法*/
//可以为接口方法提供一个默认实现,必须用default修饰符标记它
// public interface Comparable<T> {
// default int compareTo(T other) { return 0; }
//当然,这并没有太大用处,因为Comparable的每一个具体实现都会覆盖这个方法
//不过有些情况下,默认方法可能很有用
//默认方法可以调用其他方法,例如Collection接口可以定义一个便利方法
// public interface Collection {
// int size(); //an abstract method
// default boolean isEmpty() { return size() == 0; }
//这样实现Collection的程序员就不用再操心实现isEmpty方法了
//默认方法的一个重要作用是「接口演化」(interface evolution)
//假设很久以前你提供了一个这样的类
// public class Bag implements Collection
//后来,这个接口又增加了一个stream方法,假设它不是一个默认方法,那么Bag类将不能编译,因为它没有实现这个新方法
// [为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)]
//但使用原先包含这个类的JAR文件,这个类仍能正常加载,不会有意外发生
// [为接口增加方法可以保证“二进制兼容”]
//不过,如果程序再一个Bag实例上调用stream方法,就会出现一个AbstractMethodError
//将方法实现为一个默认方法就可以解决这两个问题
/*解决默认方法冲突*/
//如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?
//Java对于解决这种二义性的规则比较简单:
// 1. 超类优先。如果超类中提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
// 2. 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名且参数类型相同的方法(无论是否是默认),必须覆盖这个方法来解决冲突
// [警告:千万不要让一个默认方法重新定义Object类中的某个方法]
//例如不能为toString或equals定义默认方法,由于类优先规则,这样的方法绝对无法超越Object.toString或Object.equals
/*接口与回调*/
//回调(callback)是一种常见的程序设计模式
//在这种模式中,可以指定某个特定事件发生时应该采取的动作
//java.swing包中有一个Timer类,如果希望经过一定时间间隔就得到通知,Timer类就很有用
//如何告诉定时器要做什么呢?在很多语言中,可以提供一个函数名,定时器要定期调用这个函数
//但Java标准类库中的类采用的是面向对象方法,你可以向定时器传入某个类的对象,然后定时器调用这个对象的方法
//由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活得多
//但定时器还需要知道调用哪个方法,并要求传递地对象所属地类实现了java.awt.event包的ActionListener接口,下面是这个接口:
// public interface ActionListener { void actionPerformed(ActionEvent event); }
//当到达指定的时间间隔时,定时器就调用actionPerformed方法
//假设希望每隔一秒打印一条信息,然后响一声,那么可以定义一个实现ActionListener接口的类,然后将想执行的语句放在actionPerformed方法中
//例程:TimePrinter.java
//actionPerformed方法的ActionEvent参数提供了事件的相关信息,例如,发生这个事件的时间
//e.getWhen()返回事件时间,表示为纪元(1970.1.1)以来的毫秒数,把它传给静态方法Instant.ofEpochMilli,可以得到一个更可读的描述
//接下来构造一个这个类的对象,并将它传给Timer构造器
// var listener = new TimePrinter();
// Timer t = new Timer(1000, listener);
//最后,启动定时器
// t.start();
//例程:TimerTest.java
//如果关闭对话框,一旦main方法退出,程序就终止
/*Comparator接口*/
//前面提到可以对一个实现了Comparable接口的类的对象数组进行排序
// 例如可以对字符串数组排序,因为String类实现了Comparable<String>,且String.compareTo可以按字典顺序比较字符串
//现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序
//肯定不能让String类用两种不同的方式实现compareTo方法——String类也不应由我们来修改
//要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例
// public interface Comparator<T> { int compare(T first, T second); }
//例程:LengthComparator.java
String[] strings = {"Hi", "Hello", "Hero", "Hey"};
System.out.println(Arrays.toString(strings));
//具体完成比较时,需要建立一个实例
var comp = new LengthComparator();
//尽管LengthComparator对象没有状态,但还是需要建立这个对象的一个实例,我们需要这个实例调用compare方法,它不是一个静态方法
Arrays.sort(strings, comp);
System.out.println(Arrays.toString(strings));
//利用lambda表达式可以更容易地使用Comparator
/*对象克隆*/
//Cloneable接口指示一个类提供了一个安全的clone方法
//clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法
// (protected规则比较微妙,子类只能调用受保护的clone方法来克隆它自己的对象,必须重定义clone为public才能允许所有方法克隆对象)
//只有Employee类可以克隆Employee对象,这个限制是有原因的,Object类如何实现clone?
//它对这个对象一无所知,只能逐个字段进行拷贝,如果对象包含子对象的引用,拷贝就会得到相同子对象的另一个引用,这样原对象和克隆的对象仍然会共享一些信息
//如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的,例如String
//或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的
//不过,通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝(deep copy),同时克隆所有子对象
//对于每一个类,需要确定:
// 1. 默认的clone方法是否满足要求
// 2. 是否可以在可变的子对象上调用clone来修补默认的clone方法
// 3. 是否不该使用clone
//如果选择1或2,类必须:
// 1. 实现Cloneable接口
// 2. 重定义clone方法,并指定public访问修饰符
//Cloneable接口其实并没有指定clone方法,它只是作为一个标记,指示类设计者了解克隆过程
//如果一个对象请求克隆,但没有实现这个接口,就会生成一个检查型异常
//注:Cloneable接口是Java提供的少数标记接口(tagging interface)之一(有些程序员称为记号接口marker interface)
//标记接口不包含任何方法,它唯一的作用就是允许在类型查询中使用instanceof
//即使clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将clone重定义为public,再调用super.clone()
//例程:Employee.java
//注:Java1.4前clone的返回类型总是Object,而现在可以指定正确的返回类型,这是协变返回类型的一个例子
//要建立深拷贝,还需要克隆对象中可变的实例字段
// Employee cloned = (Employee) super.clone();
// cloned.hireDay = (Date) hireDay.clone();
//如果在一个对象上调用clone,但这个对象的类没有实现Cloneable接口,Object的clone方法就会抛出一个CloneNotSupportedException
//必须当心子类的克隆
//一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象
//而Manager有可能需要深拷贝或有不可克隆的字段,不能保证子类的实现者一定会修正clone方法让它正常工作
//clone相当笨拙,有些人认为应该完全避免使用clone
//克隆也没有你想象中那么常用,标准库中只有不到5%的类实现了clone
//注:[所有数组类型都有一个公共的clone方法],且不是受保护的
//可以用这个方法建立一个新数组,包含原数组所有元素的副本
}
public static void lambdaExp() {
/*【lambda表达式】P242*/
//lambda表达式是一个可传递的代码块,可以在以后执行一次或多次
//例 actionPerformed方法包含希望以后执行的代码
// 用定制比较器完成排序时也要将Comparator对象传入sort方法 Arrays.sort(strings, new LengthComparator());
// [这两个例子有一些共同点,都是将一个代码块传递到某个对象,这个代码块会在将来某个时间调用]
//到目前为止,在Java中传递一个代码段并不容易,你不能直接传递代码段
//Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码
//下面介绍Java中如何处理代码块
/*lambda表达式的语法*/
//考虑上面排序的例子,我们传入代码检查字符串长度,这里要计算
//first.length() - second.length()
//Java是一种强类型语言,所以还要指定first和second的类型
// (String first, String second)
// -> first.length() - second.length()
//这就是你看到的第一个lambda表达式
// [lambda表达式就是一个代码块,以及必须传入代码的变量规范]
//P243
//从那以后,带参数变量的表达式就被称为lambda表达式
//如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显式的return语句
// (String first, String second) ->
// {
// if (first.length() < second.length()) return -1;
// else if (first.length() > second.length()) return 1;
// else return 0;
// }
//即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样
// () -> { for (int i = 100; i >= 0; i--) System.out.println(i); }
//如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型:(first, second)等同于(String first, String second)
Comparator<String> comp = (first, second) -> first.length() - second.length();
//这里编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器
//如果方法只有一个参数,且这个参数的类型可以推导出,那么甚至还可以省略小括号
ActionListener listener = event -> System.out.println("The time is " + Instant.ofEpochMilli(event.getWhen()));
//等同于 (event) -> ... 或 (ActionEvent event) -> ...
// [无需指定lambda表达式的返回类型],lambda表达式的返回类型总是会由上下文推导得出
//例如 (first, second) -> first.length() - second.length(); 可以在需要int类型结果的上下文中使用
//注意:如果一个lambda表达式只在某些分支返回一个值,而另一些分支不返回值,这是不合法的
//例程:LambdaTest.java
/*函数式接口*/
//Java中有很多封装代码块的接口,如ActionListener或Comparator,lambda表达式与这些接口是兼容的
//对于[只有一个抽象方法的接口],需要这种接口的对象时,就可以提供一个lambda表达式
// [这种接口称为函数式接口(functional interface)]
//JavaAPI中一些接口会重新声明Object方法来附加javadoc注释
//Arrays.sort方法的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式
String[] words = {"00", "0", "000"};
Arrays.sort(words, (f, s) -> f.length() - s.length());
//在底层,Arrays.sort方法接收实现了Comparator<String>的某个类的对象
//在这个对象上调用compare方法会执行这个lambda表达式的体;这些对象和类的管理完全取决于具体实现
//最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口
//lambda表达式可以转换为接口,这一点让其很有吸引力,具体的语法很简短,可读性要好很多
var timer = new Timer(1000, event -> {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
});
//实际上,在Java中,对lambda表达式能做的也只是转换为函数式接口
//甚至不能把lambda表达式赋给类型为Object的变量,Object不是函数式接口
//JavaAPI在java.util.function包中定义了很多非常通用的函数式接口
// BiFunction<T, U, R> 描述了参数类型为T和U且返回类型为R的函数,可以把我们的字符串比较lambda表达式保存在这个类型的变量中
BiFunction<String, String, Integer> comp2 = (f, s) -> f.length() - s.length();
//不过,这对于排序并没有帮助,没有哪个Arrays.sort方法想要接收一个BiFunction
//类似Comparator的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法
//想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口
//java.util.function包中有一个尤其有用的接口 Predicate
// public interface Predicate<T> { boolean test(T t); }
//ArrayList类有一个removeIf方法,它的参数就是Predicate,这个接口专门用来传递lambda表达式
ArrayList<String> arrayList = new ArrayList<>();
arrayList.removeIf(e -> e == null);
//另一个有用的函数式接口是 Supplier<T>
// public interface Supplier<T> { T get(); }
//供应者(supplier)没有参数,调用时会生成一个T类型的值,它用于实现懒计算
//例如 LocalDate hireDay = Objects.requireNonNullElse(day, new LocalDate(1970, 1, 1)); 这不是最优的
//如果我们预计day很少为null,希望只在必要时才构造默认的LocalDate,通过使用供应者,我们就能延迟这个计算
// LocalDate hireDay = Objects.requireNonNullElseGet(day, () -> new LocalDate(1970, 1, 1));
LocalDate day = LocalDate.now();
LocalDate hireDay = Objects.requireNonNullElseGet(day, () -> LocalDate.of(1970, 1, 1));
//requireNonNullElseGet方法只在需要值时才调用供应者
/*方法引用*/
//有时,lambda表达式涉及一个方法
//例如你希望只要出现一个定时器事件就打印这个事件对象
var timer2 = new Timer(1000, e -> System.out.println(e));
//但如果直接把println方法传递到Timer构造器就更好了
var timer3 = new Timer(1000, System.out::println);
//表达式 System.out::println 是一个方法引用(method reference)
//它指示编译器生成一个函数式接口的实例,[覆盖这个接口的抽象方法来调用给定的方法]
//在这个例子中,会生成一个ActionListener,它的actionPerformed(ActionEvent e)方法要调用System.out.println(e)
//类似lambda表达式,方法引用也不是一个对象
//不过,为一个类型为函数式接口的变量赋值时会生成一个对象
//假设你像对字符串排序,不考虑字母的大小写,可以传递以下方法表达式
Arrays.sort(words, String::compareToIgnoreCase);
//从这些例子可以看出,要用::运算符分隔方法名与对象或类名,主要有3种情况
// 1. object::instanceMethod
// 2. Class::instanceMethod
// 3. Class::staticMethod
//第1种情况下,方法引用等价于向方法传递参数的lambda表达式
// 对于System.out::println,对象是System.out,所以方法表达式等价于 x -> System.out.println(x)
//第2种情况下,第1个参数会成为隐式参数
// 例如 String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y)
//第3种情况下,所有参数都传递到静态方法
// Math::pow 等价于 (x, y) -> Math.pow(x, y)
//更多示例 P248
//注意:只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用
//如果有多个同名的重载方法,编译器会尝试从上下文中找出你指的是哪一个方法
//类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例
//有时API包含一些专门用作方法引用的方法,例如Objects类有一个isNull方法,乍看上去好像没啥用,因为obj == null比Objects.isNull(obj)更有可读性
//不过可以把方法引用传递到任何有Predicate参数的方法,例如
arrayList.removeIf(Objects::isNull);
//等价于 arrayList.removeIf(e -> e == null);
//注意:包含对象的方法引用与等价的lambda表达式还有一个细微的差别
//考虑一个方法引用 separator::equals 如果separator为null,构造这个方法引用时就会立即抛出一个NullPointerException
//lambda表达式 x -> separator.equals(x) 只在调用时才会抛出NullPointerException
// [可以在方法引用中使用 this 和 super]
// this::equals 等同于 x -> this.equals(x)
/*构造器引用*/
//构造器引用与方法引用很类似,只不过方法名为new
//例如 Person::new 是Person构造器的一个引用,具体哪一个构造器会由上下文推导出
//可以用数组类型建立构造器引用
//例如 int[]::new 是一个构造器引用,它有一个参数即数组长度,这等价于 x -> new int[x]
//Java有一个限制,无法构造泛型类型T的数组
//数组构造器引用对于克服这个限制很有用
//表达式 new T[n] 会产生错误,因为这会改为 new Object[n],可以把构造器传入Stream接口的toArray方法
// Person[] people = stream.toArray(Person[]::new);
/*变量作用域*/
//你可能希望在lambda表达式中访问外围方法或类中的变量
/*public static void repeatMessage (String text, int delay) {
ActionListener actionListener = event -> System.out.println(text);
new Timer(delay, actionListener).start();
}*/
//注意text变量并不是在lambda表达式中定义的
//这好像有问题,lambda表达式的代码可能在repeatMessage调用返回很久后才运行,而那时候这个变量已经不存在了
//先巩固一下对lambda表达式的理解,lambda表达式有3个部分:
// [一个代码块、参数、自由变量的值(这是指非参数且不在代码中定义的变量)]
//在上例中,lambda表达式有一个自由变量text,表示lambda表达式的数据结构必须存储自由变量的值
//我们说它被lambda表达式捕获(captured)了
//注释:关于代码块以及自由变量值有一个术语:闭包(closure)
//在Java中,lambda表达式就是闭包
// [lambda表达式可以捕获外围作用域中变量的值]
//在Java中,要确保所捕获的值是明确定义的
//在lambda表达式中,只能引用值不会改变的变量
//下面的做法是不合法的
/*ActionListener actionListener = event -> {
i++; //不能改变捕获的变量
System.out.println(i);
}*/
//这个限制是有原因的,如果在lambda表达式中更改变量,并发执行多个动作时就会不安全
//另外,如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的
/*for (int i = 0; i < count; i++)
ActionListener actionListener = event -> System.out.println(i); //不能引用一个会改变的i */
// 这里有一条规则:
// [lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final)]
//事实最终变量指,这个变量初始化之后就不会再为它赋新值
// [lambda表达式的体与嵌套块有相同的作用域];这里同样适用命名冲突和遮蔽的有关规则
//再lambda表达式中声明与局部变量同名的参数或局部变量是不合法的
// [在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数]
/*public class App {
public void init() {
ActionListener actionListener = event -> System.out.println(this.toString()); */
//表达式 this.toString() 会调用 App对象的 toString方法,而不是 ActionListener 实例的方法
//在lambda表达式中,this的使用并没有任何特殊之处
//lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化
/*处理 lambda 表达式*/
//使用lambda表达式的重点是延迟执行(deferred execution)
//假设要重复一个动作n次,将这个动作和重复次数传递到一个repeat方法
repeat(3, () -> System.out.println("Runnable action×10"));
//要接受这个lambda表达式,需要选择一个函数式接口
//调用action.run()时会执行这个lambda表达式的主体
//Java API 中提供的最重要的函数式接口:P253
// 函数式接口 参数类型 返回类型 抽象方法名 描述
// Runnable 无 void run
// Supplier<T> 无 T get 提供一个T类型的值
// Consumer<T> T void accept 处理一个T类型的值
// BiConsumer<T, U> T, U void accept 处理T和U类型的值
// Function<T, R> T R apply 有一个T类参数返回R类的函数
// BiFunction<T, U, R> T, U R apply 有T和U类型参数返回R的函数
// UnaryOperator<T> T T apply 类型T上的一元操作符
// BinaryOperator<T> T, T T apply 类型T上的二元操作符
// Predicate<T> T boolean test 布尔值函数
// BiPredicate<T, U> T, U boolean test 有两个参数的布尔值函数
//基本类型的函数式接口:P254
//现在让例子更复杂一些,我们希望告诉这个动作它出现在哪一次迭代中
repeat(10, i -> System.out.println("Countdown: " + (9 - i)));
//大多数标准函数式接口都提供了非抽象方法来生成或合并函数
//例如 Predicate.isEqual(a) 等同于 a::equals,不过如果a为null也能正常工作
//已经提供了默认方法and、or、negate来合并谓词,例如:
// Predicate.isEqual(a).or(Predicate.isEqual(b)) 等同于 x -> a.equals(x) || b.equals(x)
//如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口
//这样如果你无意中增加了另一个抽象方法,编译器会产生一个错误消息;另外javadoc页里会指出你的接口是一个函数式接口
//并不是必须使用注解,根据定义,任何有一个抽象方法的接口都是函数式接口
/*再谈 Comparator*/
//todo 难点
//Comparator接口包含很多方便的静态方法来创建比较器;这些方法可以用于lambda表达式或方法引用
//静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String);对要比较的对象应用这个函数,然后对返回的键完成比较
// Arrays.sort(people, Comparator.comparing(Person::getName));
// 对一个Person对象数组按名字排序
//可以把比较器与 thenComparing 方法串起来,来处理比较结果相同的情况
// Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
//这些方法有很多变体形式,可以为comparing和thenComparing方法提取的键指定一个比较器
// Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compares(s.length(), t.length())));
//另外,comparing和thenComparing方法都由变体形式,可以避免int、long、double值的装箱;要完成前一个操作还有一种更容易的做法
// Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
//todo P255
}
private static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++)
action.run();
}
private static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++)
action.accept(i);
}
public static void innerClass() {
/*【内部类】P255*/
//内部类(inner class)是定义在另一个类中的类
//使用内部类的原因主要有两个:
// - 内部类可以对同一个包中的其他类隐藏
// - 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据
//内部类原先对于简洁地实现回调非常重要,不过如今lambda表达式在这方面可以做得更好,但内部类对于构建代码还是很有用的
//C++的嵌套类与Java的内部类很相似,但Java还有一个额外的特性:
// 内部类的对象会有一个隐式引用,指向实例化这个对象的外部类对象,通过这个指针它可以访问外部对象的全部状态
/*使用内部类访问对象状态*/
//一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段
// 为此,内部类的对象总有一个隐式引用,指向创建它的外部类对象,这个引用在内部类的定义中是不可见的
//见下例 TimePrinter 类,beep 是定义在外部的字段
//TimePrinter类位于Chapter6类内部,这并不意味着每个Chapter6都有一个TimePrinter实例字段
//外围类的引用在构造器中设置,编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数
//在start方法中构造一个TimePrinter对象后,编译器就会将当前Chapter6的this引用传递给这个构造器
//注:也可以把TimePrinter类声明为私有,这样就只有Chapter6中的方法才能够构造TimePrinter对象
//只有内部类可以是私有的,而常规类可以有包可见性或公共可见性
/*内部类的特殊语法规则*/
//OuterClass.this 表示外围类引用,例如可以把 beep 改为 Chapter6.this.beep
//反过来,可以采用以下语法更加明确地编写内部类对象的构造器
// outerObject.new InnerClass()
// ActionListener listener = this.new TimePrinter();
//上面这行代码使最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法的this引用
//这是最常见的情况,通常,this.限定词是多余的
//不过,也可以通过显式地命名将外围类引用设置为其他的对象
var jabberer = new Chapter6();
Chapter6.TimePrinter listener = jabberer.new TimePrinter();
//在外围类的作用域之外,可以这样引用内部类 OuterClass.InnerClass
//注意:内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。如果这个字段不是一个常量,就可能不唯一
//内部类不能有static方法。Java语言规范对这个限制没有做任何解释。
// 也可以允许有静态方法,但只能访问外围类的静态字段和方法。显然,Java设计者认为相对于这种复杂性来说,它带来的好处有些得不偿失。
/*内部类是否有用、必要和安全*/
//需要指出,内部类是一个编译器现象,与虚拟机无关
//编译器会把内部类转换为常规的类文件,用$分隔外部类名与内部类名,而虚拟机则对此一无所知
//编译器生成了一个额外的实例字段,对应外部类的引用
//既然内部类会转换成一个常规类,虚拟机对此也不了解,内部类又如何得到额外的访问权限的呢?
//编译器在外围类添加了静态方法,它返回作为参数传递的那个对象的beep字段,内部类方法将调用它
//例如 if(beep) 实际上会产生调用 if(Chapter6.access$0(outer)) 生成的方法名可能不同
//这样存在安全风险,任何人都可以通过调用生成的静态方法读取到私有字段
//当然,access$0不是合法的Java方法名,但熟悉类文件结构的黑客可以使用十六进制编辑器轻松创建一个类文件,其中利用虚拟机指令调用那个方法
//由于隐秘方法具有包可见性,所以攻击代码需要与被攻击类放在同一个包中
//总之,如果内部类访问了私有数据字段,就有可能通过外部类所在包中增加的其他类访问那些字段
//但做这些事情需要技巧和决心,程序员不可能无意之中就获得对类的访问权限,必须刻意构建或修改类文件才有可能达到这个目的
//合成构造器和方法可能非常复杂
//假设将TimePrinter转换为一个私有内部类,在虚拟机中不存在私有类,因此编译器会生成一个近乎最好的结果,生成的这个类有包可见性和一个私有构造器
// private Chapter6$TimePrinter(Chapter6);
//当然,没有人可以调用这个构造器,因此,还有第二个包可见的构造器
// Chapter6$TimePrinter(Chapter6, Chapter6$1);
//它将调用第一个构造器,合成Chapter6$1类只是为了将这个构造器与其他构造器区分开
//编译器将Chapter6类start方法中的构造器调用转换为 new Chapter6$TimePrinter(this, null)
/*局部内部类*/
//下面的示例代码可以看到,TimePrinter的名字只出现了一次,只在start方法中用了一次
//遇到这类情况时,可以在一个方法中局部地定义这个类
/*
public void start() {
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
var listener = new TimePrinter();
//...
}
*/
//声明局部类不能有访问说明符,局部类的作用域被限定在声明这个局部类的块中
//局部类有一个很大的优势,对外部世界完全隐藏,除start方法外,没有任何方法知道TimePrinter类的存在
/*由外部方法访问变量*/
//与其他内部类相比,局部类还有一个优点
//它们不仅能访问外部类的字段,还可以访问局部变量(例如start方法的参数变量)
//但那些局部变量必须是事实最终变量(effectively final),它们一旦赋值就绝不会改变
//为了能在方法执行完毕参数变量不复存在之后,仍然使代码正常工作,TimePrinter类在beep参数值消失之前必须将beep字段复制
//实际上,编译器检测对局部变量的访问,为每一个变量建立相应的实例字段,并将局部变量复制到构造器,从而能初始化这些实例字段
/*匿名内部类*/
//使用局部内部类时,通常还可以再进一步
//加入只想创建这个类的一个对象,甚至不需要为类指定名字,这样的一个类被称为匿名内部类(anonymous inner class)
/*
public void start() {
var listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
//...
}
*/
//上面代码的含义是,创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法在括号内定义
//一般地,语法如下
/*
new SuperType(construction parameters) {
inner class methods and data
}
*/
//其中,SuperType可以是接口,这样内部类就要实现这个接口;也可以是一个类,这样内部类就要扩展这个类
//由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以匿名内部类不能有构造器
//实际上,构造参数要传递给超类构造器
//具体地,只要内部类实现一个接口,就不能有任何构造参数,不过仍要提供一组小括号
//如果构造参数列表的结束小括号后面跟一个开始大括号,就是在定义匿名内部类
//注:尽管匿名类不能有构造器,但可以提供一个对象初始化块
//Java程序员习惯的做法是用匿名内部类实现事件监听器和其他回调,但如今最好还是使用lambda表达式
//下面的技巧称为双括号初始化(double brace initialization),这里利用了内部类语法
//假设你想构造一个数组列表,并将它传递到一个方法
/*
var friends = new ArrayList<String>();
friends.add("Harry");
friends.add("Tony");
invite(friends);
*/
//如果不再需要这个数组列表,最好让它作为一个匿名列表
/*
invite(new ArrayList<String>() {{
add("Harry");
add("Tony");
}});
*/
//这里的外层括号建立了一个ArrayList的匿名子类,内层括号则是一个对象初始化块
//在实际中,这个技巧很少使用,大多数情况下,invite方法会接受任何List<String>,所以可以直接传入 List.of("Harry", "Tony")
//警告:建立一个与超类大体类似的匿名子类通常会很方便,不过对于equals方法要特别当心
// if (getClass() != other.getClass()) return false;
//对匿名子类做这个测试会失败
//提示:生成日志或调试信息时,通常希望包含当前类的类名
//不过静态方法无法调用 getClass(),因为调用它时调用的是 this.getClass(),而静态方法没有this,所以应使用以下表达式
// new Object() {}.getClass().getEnclosingClass();
//new Object(){}会建立Object的匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类
/*静态内部类*/
//有时使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类有外围类对象的一个引用
//为此,可以将内部类声明为static,这样就不会生成那个引用
//Pair是个十分大众化的名字,可能会产生名字冲突,可以把Pair定义成Chapter6的一个公共内部类,此后就可以通过Chapter6.Pair访问它了
// Chapter6.Pair p = Chapter6.minmax(d);
//只有内部类可以声明为static,静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成它的外围类对象的引用
//在上例中必须使用静态内部类,因为内部类对象是在静态方法minmax中构造的
//注:只要内部类不需要访问外围类对象,就应该使用静态内部类
//有些程序员用嵌套类(nested class)表示静态内部类
//与常规内部类不同,静态内部类可以有静态字段和方法
//在接口中声明的内部类自动是static和public
}
private int interval;
private boolean beep;
public void start() {
//...
ActionListener listener = this.new TimePrinter();
class LocalInnerClass implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
var localInnerClass = new LocalInnerClass();
var anonymousInnerClass = new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
}
private class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (Chapter6.this.beep)
Toolkit.getDefaultToolkit().beep();
}
}
public static class Pair {
private double first;
private double second;
public Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() {
return first;
}
public double getSecond() {
return second;
}
}
public static Pair minmax(double[] values) {
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values) {
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
public static void serviceLoader() {
/*【服务加载器】P270*/
//todo 回看
//有时会开发采用一个服务架构的应用
//JDK提供了一个加载服务的简单机制,这种机制由Java平台模块系统提供支持
//通常提供一个服务时,程序希望服务设计者能有一些自由,能够确定如何实现服务的特性;另外还希望有多个实现以供选择
//利用 ServiceLoader 类可以很容易地加载符合一个公共接口的服务
//定义一个接口(或者如果愿意也可以定义一个超类),其中包含服务的各个实例应当提供的方法
// 例如,假设你的服务要提供加密 Cipher.java
//服务提供者可以提供一个或多个实现这个服务的类
// CaesarCipher.java
//每个实现类必须有一个无参数构造器
//现在把这些类的类名增加到 META-INF/services 目录下的一个UTF-8编码文本文件中,文件名必须与完全限定类名一致
//上例中,文件 META-INF/services/serviceLoader.Cipher 必须包含下面这一行
// serviceLoader.impl.CaesarCipher
//可以提供多个实现类,以后可以从中选择
//完成上面的工作后,程序可以如下初始化一个服务加载器
// public static ServiceLoader<Cipher> cipherServiceLoader = ServiceLoader.load(Cipher.class);
//这个初始化工作只在程序中完成一次
//服务加载器的iterator方法会返回一个迭代器来迭代处理所提供的所有服务实现,最容易的做法是使用一个增强for循环进行遍历,在循环中选择一个适当的对象来完成服务
/*
public static Cipher getCipher(int minStrength) {
for (Cipher cipher : cipherServiceLoader) {
if (cipher.strength() >= minStrength)
return cipher;
}
return null;
}
*/
//或者,也可以使用流查找所要的服务
//stream方法会生成ServiceLoader.Provider实例的一个流
//这个接口包含type和get方法,可以用来得到提供这类和提供者实例
//如果按类型选择一个提供者,只需要调用type,而没有必要实例化任何服务实例
/*
public static Optional<Cipher> getCipher2(int minStrength) {
return cipherServiceLoader.stream().filter(descr -> descr.type() == serviceLoader.impl.CaesarCipher.class)
.findFirst().map(ServiceLoader.Provider::get);
}
*/
//最后,如果想要得到任何服务实例,只需要调用findFirst
// Optional<Cipher> cipher = cipherServiceLoader.findFirst();
}
public static void proxy() {
/*【代理】P273*/
//todo 回看
//利用代理可以在运行时创建实现了一组给定接口的新类
//只有在编译时期无法确定需要实现哪些接口时才有必要使用代理,这种情况很少见
/*何时使用代理*/
//假设你想构造一个类的对象,这个类实现了一个或多个接口,但在编译时你可能并不知道这些接口到底是什么
//这个问题确实有些难度,要构造一个具体类只需要使用newInstance方法或者使用反射找出构造器,但不能实例化接口,需要在运行的程序中定义一个新类
//为解决这个问题,有些程序会生成代码,将这些代码放在一个文件中调用编译器,然后再加载得到的类文件
//很自然地,这样做的速度会比较慢,并且需要将编译器连同程序一起部署
//而代理机制则是一种更好的解决方案
//代理类可以在运行时创建全新的类,这样的代理类能够实现你指定的接口
//具体地,代理类包含以下方法:
// · 指定接口所需要的全部方法
// · Object类中的全部方法
//不过,不能在运行时为这些方法定义新代码
//实际上,必须提供一个调用处理器(invocation handler),调用处理器是实现了InvocationHandler接口的类的对象
//这个接口只有一个方法
// Object invoke(Object proxy, Method method, Object[] args)
//无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原调用的参数
//之后调用处理器必须确定如何处理这个调用
/*创建代理对象*/
//要创建一个代理对象,需要使用Proxy类的newProxyInstance方法,这个方法有3个参数:
// · 一个类加载器(class loader);作为Java安全模型的一部分,可以对平台和应用类、从因特网下载的类等使用不同的类加载器
// 在本例我们指定「系统类加载器」加载平台和应用类
// · 一个Class对象数组,每个元素对应需要实现的各个接口
// · 一个调用处理器
//使用代理可能出于很多目的,例如:
// · 将方法调用路由到远程服务器、在运行的程序中将用户界面事件与动作关联起来、为了调试,跟踪方法调用
//在本例,我们使用代理和调用处理器跟踪方法调用
//我们定义了一个 TraceHandler 包装器类存储包装的对象,其中的invoke方法会打印所调用方法的名字和参数,随后用包装的对象作为隐式参数调用这个方法
//本例使用代理对象跟踪一个二分查找
// 这里首先在数组中填充整数1-1000的代理,然后调用Arrays类的binarySearch方法在数组中查找一个随机整数,最后打印出匹配的元素
//Integer类实现了Comparable接口,代理对象属于在运行时定义的一个类,这个类也实现了Comparable接口,不过它的compareTo方法调用了代理对象处理器的invoke方法
//binarySearch方法会有以下调用:
// if(elements[i].compareTo(key) < 0) ...
//由于数组中填充了代理对象,所以compareTo调用了TraceHandler类中的invoke方法,这个方法打印出了方法名和参数,之后在包装的Integer对象上调用compareTo
//最后示例程序打印结果,这个println方法调用代理对象的toString,这个调用也会被重定向到调用处理器
/*代理类的特性*/
//需要记住,代理类是在程序运行过程中动态创建的,然而一旦被创建,它们就变成了常规类,与虚拟机中的任何其他类没有什么区别
//所有的代理类都扩展Proxy类,一个代理类只有一个实例字段——即调用处理器,它在Proxy超类中定义
//完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中
//例如,上例代理Comparable对象时,TraceHandler就包装了实际的对象
//所有的代理类都要覆盖Object类的toString、equals、hashCode方法
//如同所有代理方法一样,这些方法只是在调用处理器上调用invoke
//Object类中的其他方法(如clone和getClass)没有重新定义
//没有定义代理类的名字,Oracle虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名
//对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类
//也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法,将得到同一个类的两个对象
//也可以利用getProxyClass方法获得这个类
// Class proxyClass = Proxy.getProxyClass(null, interfaces);
//代理类总是public和final,如果代理类实现的所有接口都是public,这个代理类就不属于任何特定的包
// 否则,所有非公共的接口都必须属于同一个包,同时,代理类也属于这个包
//可以调用Proxy类的isProxyClass方法检测一个特定的Class对象是否表示一个代理类
}
}
TimePrinter.java
package timer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Instant;
public class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen()));
Toolkit.getDefaultToolkit().beep();
}
}
TimerTest.java
package timer;
import javax.swing.*;
public class TimerTest {
public static void main(String[] args) {
var listener = new TimePrinter();
//构造一个调用listener的timer
var timer = new Timer(1000, listener);
timer.start();
//保持程序运行直到用户点击确定
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
Employee.java
package interfaces;
public class Employee implements Cloneable, Comparable<Employee> {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
@Override
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
@Override
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}
EmployeeSortTest.java
package interfaces;
import java.util.Arrays;
public class EmployeeSortTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Hary", 35000);
staff[1] = new Employee("Carl", 75000);
staff[2] = new Employee("Tony", 38000);
Arrays.sort(staff);
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
LengthComparator.java
package interfaces;
import java.util.Comparator;
public class LengthComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
LambdaTest.java
package lambda;
import javax.swing.*;
import java.util.Arrays;
import java.util.Date;
public class LambdaTest {
public static void main(String[] args) {
var planets = new String[]{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
System.out.println(Arrays.toString(planets));
System.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by length:");
Arrays.sort(planets, (f, s) -> f.length() - s.length());
System.out.println(Arrays.toString(planets));
var timer = new Timer(1000, event -> System.out.println("The time is " + new Date()));
timer.start();
JOptionPane.showMessageDialog(null, "Quit?");
System.exit(0);
}
}
Cipher.java
package serviceLoader;
public interface Cipher {
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}
CaesarCipher.java
package serviceLoader.impl;
import serviceLoader.Cipher;
public class CaesarCipher implements Cipher {
@Override
public byte[] encrypt(byte[] source, byte[] key) {
var result = new byte[source.length];
for (int i = 0; i < source.length; i++) {
result[i] = (byte) (source[i] + key[0]);
}
return result;
}
@Override
public byte[] decrypt(byte[] source, byte[] key) {
return encrypt(source, new byte[]{(byte) -key[0]});
}
@Override
public int strength() {
return 1;
}
}
异常、断言和日志
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.Scanner;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Chapter7 {
public static void main(String[] args) throws EOFException, FileFormatException {
//对于异常情况,Java使用了一种称为异常处理(exception handing)的错误捕获机制
//Java中的异常处理与C++中的十分类似
//在测试期间需要运行大量检查以确保程序操作的正确性,在测试结束后没有必要保留,可以简单删除检查,需要另做测试时在粘贴回来,但这样会很繁琐
//使用断言可以有选择地启用检查
//程序出错时,我们可能希望记录出现的问题,以备日后分析
//可以使用标准Java日志框架
errorHanding();
catchException();
howToUseException();
useAssertion();
log();
howToDebug();
}
public static void errorHanding() throws EOFException {
/*【处理错误】P279*/
//用户期望在出现错误时,程序能够采取合理的行为
//如果由于错误某些操作没有完成,程序应该:
// · 返回到一种安全状态,并能够让用户执行其他的命令;或者
// · 允许用户保存所有工作的结果,并以妥善的方式终止程序
//异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器
//为了处理程序中的异常情况,必须考虑到程序中可能会出现的错误和问题,例如:
// · 用户输入错误、设备错误、物理限制(磁盘已满)、代码错误
//对于方法中的错误,传统的做法是返回一个特殊的错误码,由调用方法分析,例如返回-1或null
//但并不是任何情况下都能够返回一个错误码,有可能无法明确将有效数据与无效数据加以区分
//在Java中,如果某个方法不能采用正常的途径完成它的任务,可以通过另一个路径退出方法
//在这种情况下,方法会立刻退出,不返回任何值,而是抛出(throw)一个封装了错误信息的对象
//此外,也不会从调用这个方法的代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)
//异常有自己的语法和特定的继承层次结构,下面首先介绍语法,然后再给出有效使用这种语言特性的技巧
/*异常分类*/
//Java中所有的异常对象都是派生于 Throwable 类的一个类实例,如果内置的异常类不能满足需求,用户还可以创建自己的异常类
//需要注意,所有的异常都继承自 Throwable,但在下一层立即分解为两个分支:Error 和 Exception
//Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误
//[你的应用程序不应该抛出这种类型的对象]
//如果出现了这样的内部错误,除了通知用户并尽力妥善终止程序之外,你几乎无能为力,这种情况很少出现
//在设计Java程序时,要重点关注 Exception 层次结构
//这个层次结构又分解为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常
//一般规则是:[由编程错误导致的异常属于 RuntimeException];如果程序本身没问题,但由于像IO错误这类问题导致的异常属于其他异常
//派生于 RuntimeException 的异常包含以下问题:
// · 错误的强制类型转换、数组访问越界、访问null指针
//其他异常包括:
// · 试图超越文件末尾继续读取数据、试图打开一个不存在的文件、试图根据给定字符串查找Class对象,而这个字符串表示的类并不存在
//「如果出现 RuntimeException 异常,那么就一定是你的问题」
//这个规则很有道理,应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常
// 应该在使用变量前检测它是否为null来杜绝NullPointerException异常的发生
//Java语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非检查型(unchecked)异常,所有其他的异常称为检查型(checked)异常
//[编译器将检查你是否为所有的检查型异常提供了异常处理器]
/*声明检查型异常*/
//方法不仅要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误
//例如,试图处理文件的代码就需要通知编译器可能抛出IOException类的异常
//要在方法的首部指出这个方法可能抛出一个异常
// public FileInputStream(String name) throws FileNotFoundException
//这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也可能出错而抛出一个FileNotFoundException异常
//如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索知道如何处理FileNotFoundException对象的异常处理器
//自己编写方法时,不必声明这个方法可能抛出的所有异常
//记住在遇到下面4种情况时会抛出异常:
// · 调用了一个抛出检查型异常的方法
// · 检测到一个错误,并且利用throw语句抛出一个检查型异常
// · 程序出现错误、Java虚拟机或运行时库出现内部错误
//[如果出现前2种情况,则必须告诉调用这个方法的程序员有可能抛出异常]
//有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范(exception specification)声明这个方法可能抛出异常
// public Image loadImage(String s) throws IOException
//如果一个方法有可能抛出多个检查型异常类型,就必须在方法首部列出所有的异常类,每个异常类间用逗号隔开
// public Image loadImage(String s) throws FileNotFoundException, EOFException
//但是,[不需要声明Java的内部错误,即从 Error 继承的异常];任何程序代码都有可能抛出那些异常,而我们对此完全无法控制
//同样,[也不应该声明从 RuntimeException 继承的那些非检查型异常]
// 这些运行时错误完全在我们的控制之中,如果特别担心数组下标错误,就应该多花时间修正,而不只是声明这些错误有可能发生
//总之,一个方法[必须声明所有可能抛出的检查型异常]
//而非检查型异常要么在你的控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)
//如果你的方法没有声明所有可能发生的检查型异常,编译器就会发出一个错误信息
//当然,不只是声明异常,你还可以捕获异常,这样就不会从这个方法抛出这个异常,也没有必要使用 throws
//警告:重写的子类方法中声明的检查型异常不能比超类方法中声明的异常更通用(子类方法可以抛出更特定的异常,或不抛出任何异常)
//如果超类方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常
//如果一个方法声明会抛出一个异常,这个异常是特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类
//例如声明会抛出IOException,你并不知道具体是哪种IOException异常,它既可能是IOException,也可能是其某个子类的对象,例如FileNotFoundException
//在C++中,throw说明符在运行时执行而不是在编译时执行,也就是说,C++编译器将不处理任何异常规范
//但如果函数抛出的异常没有出现在throw列表中,就会调用unexpected函数,默认情况下程序会终止
//另外,在C++中,如果没有给出throw说明,函数可能抛出任何异常,而在Java中,没有throws说明符的方法将根本不能抛出任何检查型异常
/*如何抛出异常*/
//假设一个名为readData的方法正在读取一个文件,文件首部包含的信息承诺文件长度为1024个字符,然而读到一半文件就结束了
//你可能认为这是一种不正常的情况,希望抛出一个异常
//[首先要决定应该抛出什么类型的异常]
//可能某种IOException是个不错的选择,阅读Java API文档后会发现,EOFException异常的描述是「指示输入过程中意外遇到了EOF」,这正是我们要的
//下面是抛出这个异常的语句
if (false) throw new EOFException();
//或者也可以是
var e = new EOFException();
if (false) throw e;
//EOFException 类还有一个带一个字符串参数的构造器,你可以利用它更细致地描述异常
String gripe = "Content-length: " + "****" + ", Received: " + "***";
if (false) throw new EOFException(gripe);
//在Java中只能抛出 Throwable 子类的对象,而在C++中,可以抛出任何类型的值
/*创建异常类*/
//如果你遇到了任何标准异常类都无法描述清楚的问题,就可以创建自己的异常类了
//定义一个派生于 Exception 的类,或者派生于Exception的某个子类
//习惯做法是:自定义的这个类应该包含两个构造器,一个是默认的,另一个包含详细描述信息的
// (超类 Throwable 的 toString 方法会返回一个字符串,其中包含这个详细信息,这在调试中非常有用)
try {
throw new FileFormatException();
} catch (FileFormatException ignored) {
}
}
public static void catchException() throws FileFormatException {
/*【捕获异常】P286*/
//如果try语句块中的任何代码抛出了catch子句中指定的一个异常类
// · 程序将跳过try语句块的其余代码、程序将执行catch字句中的处理器代码
//如果方法中的任何代码抛出了catch字句中没有声明的一个异常类型,那么这个方法就会立即退出
//编译器严格地执行throws说明符,如果调用了一个抛出检查型异常的方法,就必须处理这个异常,或者继续传递这个异常
//一般经验是,要捕获那些你知道如何处理的异常,继续传递那些你不知道怎样处理的异常
// 这也有一个例外,前面提到,如果覆盖超类的方法,而这个超类方法没有抛出异常,你就必须捕获你的方法代码中出现的每一个检查型异常
/*捕获多个异常*/
//可以为每个异常类型使用一个单独的catch子句
//如果想获得异常对象中包含的更多信息,可以尝试使用 e.getMessage() 或者使用 e.getClass().getName() 得到异常对象的实际类型
try {
//Java7中,同一个catch字句可以捕获多个异常类型,用 | 分隔
//catch (FileNotFoundException | FileFormatException e) {
} catch (Exception e) {
e.printStackTrace();
String message = e.getMessage();
String className = e.getClass().getName();
}
// [捕获多个异常时,异常变量隐含为 final 变量,在有|分隔的catch中不能为e赋不同的值]
//捕获多个异常不仅让你的代码看起来更简单,生成的字节码还会更高效
/*再次抛出异常与异常链*/
//可以在catch子句中抛出一个异常,通常,希望改变异常的类型时会这样做
try {
// [可以把原始异常设置为新异常的原因]
} catch (Exception original) {
var e = new FileFormatException("xxx error");
e.initCause(original);
throw e;
//捕获到这个异常时,可以使用下面这条语句获取原始异常
// Throwable original = caughtException.getCause();
}
//提示:如果在一个方法中发生了一个检查型异常,但这个方法不允许抛出检查型异常,那么包装技术也很有用
// 我们可以捕获这个检查型异常,并将它包装成一个运行时异常
try {
//有时你可能只想记录一个异常,再将它重新抛出,而不做任何改变
} catch (Exception e) {
//logger.log(level, message, e);
throw e;
}
/* finally 子句*/
//代码抛出一个异常时,如果这个方法已经获得了只有它自己知道的一些本地资源,而且这些资源必须清理,就会有问题
//如果捕获所有异常,完成资源的清理再抛出,这就需要在两个地方编写清理资源代码
//finally子句可以解决这个问题
//Java7之后,还有一种更精巧的解决方案,即 try-with-resources 语句,在实际中,这可能比finally子句更常用
// [不管是否有异常被捕获,finally子句中的代码都会执行]
//如果 catch 子句抛出一个异常,finally子句也会被执行
//[try语句可以只有finally子句],而没有catch子句,这种情况下,无论是否遇到异常,finally子句都会被执行,当然,如果遇到异常,这个异常会被重新抛出
try {
try {
// 这种双层解决方案不仅清楚,而且功能更强:将会报告finally子句中出现的错误
} finally {
//in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
//警告:当 finally 子句包含 return 语句时,可能产生意想不到的结果
// 如果利用return从try语句块中退出,在方法返回前会执行finally子句块,如果finally中也有一个return语句,[这个返回值会遮蔽原来的返回值]
System.out.println(parseInt("18"));
//更糟糕的是,如果Integer.parseInt(s)抛出一个异常,然后执行finally子句,return语句甚至会「吞掉」这个异常!
System.out.println(parseInt("BadInput"));
// [finally 子句的体要用于清理资源,不要把改变控制流的语句(return、throw、break、continue)放在 finally 子句中]
/*try-with-Resources 语句*/
//假设资源属于一个实现了 AutoCloseable 接口的类,Java7提供了一个很有用的快捷方式
//AutoCloseable 接口有一个方法 void close() throws Exception
// 注:还有一个 Closeable 接口,是 AutoCloseable 的子接口,也只包含一个close方法,不过这个方法声明为抛出一个IOException
//try-with-resources语句(带资源的try语句)的最简形式为:
// try(Resource res = ...) { ... }
// [try块退出时,会自动调用 res.close()]
//例如
try (var in = new Scanner(new FileInputStream("/usr/words"), StandardCharsets.UTF_8);
var out = new PrintWriter("out.txt", StandardCharsets.UTF_8)) {
while (in.hasNext()) System.out.println(in.next());
} catch (Exception ignored) {
}
//这个块正常退出时,或存在一个异常时,都会调用 in.close() 方法,就好像使用了 finally 块一样
//还可以指定多个资源,以分号分隔,无论这个块如何退出,in 和 out 都会关闭 (如果用常规方法,需要两个嵌套的try/finally语句)
//在Java9中,可以在try首部中提供之前声明的事实最终变量 try(out) { .... }
//如果try块抛出一个异常,而且close方法也抛出一个异常,就会带来一个难题
//try-with-resources语句可以很好地处理这种情况:
// 原来的异常会重新抛出,而close方法抛出的异常会「被抑制」;这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常
//如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会生成从 close 方法抛出并被抑制的异常数组
//只要需要关闭资源,就要尽可能使用 try-with-resources 语句
// 注:try-with-resources语句自身也可以有catch子句,甚至还可以有一个finally子句,这些子句会在关闭资源之后执行
/*分析堆栈轨迹元素 P294*/
// todo 回看
//堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表
//Java程序因为一个未捕获的异常而终止时,就会显示堆栈轨迹
//可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
System.out.println(description);
//一种更灵活的方法是使用 StackWalker 类,它会生成一个 StackWalker.StackFrame 实例流,其中每个实例分别描述一个栈帧(stack frame)
//可以利用以下调用迭代处理这些栈帧
StackWalker walker = StackWalker.getInstance();
// walker.forEach(stackFrame -> /*analyze frame*/);
//如果想要以懒方式处理 Stream<StackWalker.StackFrame> 可以调用
// walker.walk(stream -> /*process stream*/);
//利用 StackWalker.StackFrame 类的一些方法可以得到所执行代码行的文件名和行号,以及类对象和方法名
//toString 方法会生成一个格式化字符串,其中包含所有这些信息
//注:Java9之前,Throwable.getStackTrace 方法会生成一个 StackTraceElement[] 数组,其中包含与 StackWalker.StackFrame 实例流类似的信息
//不过这个调用的效率不高,因为它要得到整个堆栈,即使调用者可能只需要几个栈帧;另外它只允许访问挂起方法的类名,而不能访问类对象
//例程:StackTraceTest.java
}
private static int parseInt(String s) {
// BAD method
try {
return Integer.parseInt(s);
} finally {
return 0;
}
}
public static void howToUseException() {
/*【使用异常的技巧】P297*/
// 1. 异常处理不能代替简单的测试
//与完成简单的测试相比,捕获异常所花费的时间大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常
// 2. 不要过分地细化异常
//将每一条语句都分装在一个独立的try语句块中将导致代码量的急剧膨胀;考虑将整个任务包在一个try中
// 3. 充分利用异常层次结构
//不要只抛出RuntimeException,应该寻找一个适合的子类或创建自己的异常类
//不要只捕获Throwable异常,这会使你的代码更难读、更难维护
//考虑检查型异常与非检查型异常的区别;检查型异常本来就很庞大,不要为逻辑错误抛出这些异常
//如果能将一种异常转换成另一种更加适合的异常,那么不要犹豫
// 4. 不要压制异常
//一旦出现异常,这个异常会被悄无声息地忽略,如果你认为异常都非常重要,就应该适当地进行处理
// 5. 在检测错误时,「苛刻」要比放任更好
//当检测到错误时,有些程序员对抛出异常很担心
//最好在出错的地方抛出一个合适的异常,这好过以后抛出另一个异常
// 6. 不要羞于传递异常
//很多程序员都感觉应该捕获抛出的全部异常,其实,可能更好是继续传递这个异常
//更高层的方法通常可以更好地通知用户发生了错误,或者放弃不成功的命令
}
public static void useAssertion() {
/*【使用断言】P300*/
//在一个具有自我保护能力的程序中,断言很常用
/*断言的概念*/
//假设确信某个属性符合要求,并且代码的执行依赖于这个属性
int x = 2;
double y = Math.sqrt(x);
//你确信这里的x是一个非负数,你可能还是想再做一次检查,不希望计算结果中潜入让人困惑的「不是一个数 NaN」;当然,也可以抛出一个异常
if (x < 0) throw new IllegalArgumentException("x < 0");
//但如果程序中含有大量这种检查,程序运行起来会慢很多
//[断言机制允许在测试期间向代码插入一些检查,而在生产代码中会自动删除这些检查]
//Java引入了关键字 assert 它有两种形式
// assert condition; 和 assert condition : expression;
//这两个语句都会计算条件,如果结果为false,则抛出一个 AssertionError 异常
//在第二个语句中,表达式将传入 AssertionError 对象的构造器,并转换成一个消息字符串
//注:expression表达式部分的唯一目的是产生一个消息字符串;AssertionError 对象并不存储具体的表达式值
// 正如JDK文档所描述的那样,如果使用表达式的值,就会鼓励程序员尝试从断言失败恢复程序的运行,这不符合断言机制的初衷
//要想断言x是一个非负数,只需使用
assert x >= 0;
//或者将x的实际值传递给 AssertionError 对象,以便以后显示
assert x >= 0 : x;
//注:C语言中的assert宏将断言中的条件转换成一个字符串,当断言失败时就会打印这个字符串:assert(x>=0) 打印 "x>=0"
// 在Java中,条件并不会自动成为错误报告的一部分,如果希望看到这个条件,就必须将它以字符串的的形式传递给 AssertionError 对象
assert x >= 0 : "x>=0";
/*启用和禁用断言*/
//断言默认是禁用的,可以在运行程序时用 -enableassertions 或 -ea 选项启用断言
//[不必重新编译程序来启用或禁用断言,启用或禁用断言时类加载器(class loader)的功能]
//禁用断言时,类加载器会去除断言代码,因此不会降低程序运行的速度
//也可以在某个包或整个包中启用断言
// java -ea:MyClass -ea:com.mycompany.mylib MyApp
//这条命令将为MyClass类以及com.mycompany.mylib包[和它的子包]中的所有类打开断言,选项-ea将打开无名包中所有类的断言
//也可以用 -disableassertions 或 -da 在某个特定类和包中禁用断言
//有些类不是由类加载器加载,而是直接由虚拟机加载的。可以使用这些开关有选择地启用或禁用那些类中的断言
//不过启用和禁用所有断言的-ea和-da开关不能应用到那些没有类加载器的「系统类」上;对于这些系统类,需要使用 -enablesystemassertions/-esa 开关启用断言
//也可以通过编程控制类加载器的断言状态
/*使用断言完成参数检查*/
//Java中,给出了3种处理系统错误的机制
// · 抛出一个异常
// · 日志
// · 使用断言
//什么时候应该选择使用断言呢?记住下面几点
// · 断言失败是致命的、不可恢复的错误
// · 断言检查只是在开发和测试阶段打开
//因此,不应该使用断言向程序的其他部分通知发生了可恢复性的错误,或者,不应该利用断言与程序用户沟通问题
//[断言只应该用于在测试阶段确定程序内部错误的位置]
//如果在方法的文档中约定,一个参数a必须不为null,就可以在这个方法的开头使用断言 assert a != null;
//计算机科学家将这种约定称为前置条件(Precondition)
//如果没有承诺在任何情况下都有正确的行为,即有一个前置条件a非null;如果调用者在调用这个方法时没有满足这个前置条件,断言会失败
//事实上,由于有这个断言,当方法被非法调用时,它的行为将是难以预料的:有时候会抛出一个断言错误,有时候会产生一个null指针异常,这完全取决于类加载器的配置
/*使用断言提供底层假设的文档*/
//很多程序员使用注释来提供底层假设的文档
/*
if (i % 3 == 0)
...
else if (i % 3 == 1)
...
else // (i % 3 == 2)
...
*/
//在这个示例中,使用断言会更好
/*
if (i % 3 == 0)
...
else if (i % 3 == 1)
...
else {
assert i % 3 == 2;
...
}
*/
//当然,如果仔细考虑这个问题,如果i是正值,那么余数肯定是0 1 2,如果i是负值,余数可以是-1和-2,因此最好是在if语句之前使用断言 assert i >= 0;
//无论如何,[这个示例说明了程序员应该如何使用断言来进行自我检查]
//断言是一种测试和调试阶段使用的战术性工具;与之不同,日志是一种在程序整个生命周期都可以使用的战略性工具
}
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
public static void log() {
/*【日志】P304*/
//日志API可以解决使用print方法观察程序行为的不便
//日志API的主要优点有:
// · 可以很容易地取消全部日志记录,或者仅仅取消某个级别以下的日志,而且可以很容易地再次打开日志开关
// · 可以很简单地禁止日志记录,因此,将这些日志代码留在程序中的开销很小
// · 日志记录可以被定向到不同的处理器,如在控制台显示、写至文件等
// · 日志记录器和处理器都可以对记录进行过滤;过滤器可以根据过滤器实现器指定的标准丢弃那些无用的记录项
// · 日志记录可以采用不同的方式格式化,例如,纯文本或XML
// · 应用程序可以使用多个日志记录器,它们使用与包名类似的有层次结构的名字,例如 com.mycompany.myapp
// · 日志系统的配置由配置文件控制
//注:很多应用会使用其他日志框架,如Log4J和Logback,它们能提供比标准Java日志框架更高的性能
//这些框架的API稍有区别;SLF4J和Commons Logging等日志门面(Logging facades)提供了一个统一的API,利用这个API,你无需重写应用就可以替换日志框架
//注:在Java9中,Java平台有一个单独的轻量级日志系统,它不依赖于java.logging模块(包含标准Java日志框架)
//这个系统只用于JavaAPI;如果有java.logging模块,日志消息会自动地转发给它;第三方日志框架可以提供适配器来接收平台日志消息
//开发应用程序的程序员不太会用到平台日志
/*基本日志*/
//要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法
Logger.getGlobal().info("File->Open menu item selected");
//但是如果在适当的地方(如main的最前面)调用 Logger.getGlobal().setLevel(Level.OFF); 将会取消所有日志
/*高级日志*/
//你可以定义自己的日志记录器,可以调用 getLogger 方法创建或获取日志记录器
//private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
//未被任何变量引用的日志记录器可能会被垃圾回收,为了防止这种情况,要用一个静态变量存储日志记录器的引用
//日志记录器名也具有层次结构,与包相比,日志记录器的层次性更强
//日志记录器的父与子之间将共享某些属性;如果对日志记录器com.mycompany设置了日志级别,它的子日志记录器也会继承这个级别
//通常有以下7个日志级别
// · SEVERE WARNING INFO CONFIG FINE FINER FINEST
//在默认情况下,实际上只记录前3个级别
//也可以设置一个不同的级别
myLogger.setLevel(Level.FINE);
//现在,FINE以及更高级别的日志都会记录
//还可以使用Level.ALL开启所有级别的日志记录,或者用Level.OFF关闭所有级别记录
myLogger.setLevel(Level.ALL);
//所有级别都有日志记录方法
myLogger.warning("message");
myLogger.fine("message");
//也可以用log方法并指定级别
myLogger.log(Level.CONFIG, "message");
//提示:默认的日志配置会记录INFO或更高级别的所有日志
//因此,应该使用CONFIG、FINE、FINER、FINEST级别来记录那些有助于诊断但对用户意义不大的调试信息
//警告:如果将记录级别设置为比INFO更低的级别,还需要修改日志处理器的配置;默认的日志处理器会抑制低于INFO级别的消息
//默认的日志记录将显示根据调用堆栈得出的包含日志调用的类名和方法名
//但如果虚拟机对执行过程进行了优化,就得不到准确的调用信息
//此时,可以用logp方法获得调用类和方法的确切位置
myLogger.logp(Level.CONFIG, Chapter7.class.getName(), "Method Name", "message");
//有一些用来跟踪执行流的便利方法
myLogger.entering("Class Name", "Method Name", new Object[]{});
//...
myLogger.exiting("Class Name", "Method Name", "result");
//这些调用将生成FINER级别且以字符串ENTRY和RETURN开头的日志记录
//将来,带Object[]参数的日志记录方法可能会被重写,以支持可变参数列表
//记录日志的常见用途是记录那些预料之外的异常,可以用下面两个便利方法在日志记录中包含异常的描述
try {
} catch (Exception e) {
myLogger.throwing("Class Name", "Method Name", e);
throw e;
}
try {
} catch (Exception e) {
myLogger.log(Level.WARNING, "Reading file", e);
}
//throwing 调用可以记录一条FINER级别的日志记录和一条以THROW开始的消息
/*修改日志管理器配置*/
//todo 回看
//可以通过编辑配置文件来修改日志系统的各个属性
//配置文件默认位于 conf/logging.properties (或Java9之前,jre/lib/logging.properties)
//要想使用另一个配置文件,就要修改java.util.logging.config.file属性,为此要用以下命令启动应用程序
// java -Djava.util.logging.config.file=configFile MainClass
//要修改默认的日志级别,需要编辑配置文件修改以下命令行 .level=INFO
//可以通过添加 com.mycompany.myapp.level=FINE 指定自定义日志记录器的日志级别
//日志记录器并不将消息发送到控制台,那是处理器的任务;处理器也有级别,要想在控制台上看到FINE级别的消息,就要进行以下设置
// java.util.logging.ConsoleHandler.level=FINE
//日志管理器配置中的属性设置不是系统属性,用-Dcom.mycompany.myapp.level=FINE 启动程序不会对日志记录器产生任何影响
//日志管理器在虚拟机启动时初始化,也就是在main方法执行前
//可以在程序中调用
// System.setProperty("java.util.logging.config.file", file);
//你还必须重新初始化日志管理器
// LogManager.getLogManager().readConfiguration();
//在Java9中,可以调用以下方法更新日志配置
// LogManager.getLogManager().updateConfiguration(mapper);
//这样就会从java.util.logging.config.file系统属性指定的位置读取一个新配置;然后应用这个映射器来解析老配置或新配置中所有键的值
//todo
/*本地化*/
//todo P308
//请求一个日志记录器时,可以指定一个资源包;然后为日志消息指定资源包的键,而不是实际的日志消息字符串
/*处理器*/
//默认情况下,日志记录器将记录发送到ConsoleHandler,并由它输出到 System.err 流
//具体地,日志记录器会把记录发送到父处理器,而最终的祖先处理器(名为"")有一个 ConsoleHandler
//与日志记录器一样,处理器也有日志级别,一个要记录的日志记录,它的日志级别必须高于日志记录器和处理器两者的阈值
//日志管理器配置文件将默认的控制台处理器的日志级别设置为 java.util.logging.ConsoleHandler.level=INFO
//另外,还可以绕过配置文件,安装你自己的处理器
Logger logger = Logger.getLogger("com.mycompany.myapp2");
logger.setLevel(Level.FINE);
logger.setUseParentHandlers(false);
var handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);
//默认情况下,日志记录器将记录发送到自己的处理器和父日志记录器的处理器
//我们的日志记录器是祖先日志记录器(名为"")的子类,而这个祖先日志记录器会把所有等于或高于INFO级别的记录发送到控制台
//为了不两次看到记录,应该将useParentHandlers属性设置为false
//日志API提供两个很有用的处理器
//SocketHandler 将日志记录发送到指定的主机和端口
//FileHandler 将记录收集到文件中
if (false) {
try {
var fileHandler = new FileHandler();
logger.addHandler(fileHandler);
} catch (Exception ignored) {
}
}
//new FileHandler(); 默认文件处理器将记录发送到用户主目录的javan.log文件,n是保证文件唯一的一个编号
//如果用户系统没有主目录的概念,文件就存储在一个默认位置(如 C:\Windows)
//默认情况下,记录会格式化为XML
//todo 文件处理器配置参数 P310
/*过滤器*/
//要定义一个过滤器,需要实现 Filter 接口
//要将一个过滤器安装到一个日志记录器或处理器中,只需要调用 setFilter
//todo P312
//注意,同一时刻最多只能有一个过滤器
/*格式化器*/
//自定义格式需要扩展 Formatter 类并覆盖 String format(LogRecord record)
//调用 setFormatter 将格式化器安装到处理器中
//todo P313
/*日志技巧*/
//下面总结了一些最常用的操作
// 1. 对一个简单的应用,选择一个日志记录器;可以把日志记录器命名为与主应用包一样的名字
// 总是可以通过调用以下方法得到日志记录器
Logger logger1 = Logger.getLogger("com.company.program");
// 为方便起见,你可能希望为有大量日志记录活动的类增加静态字段
// private static final Logger logger = Logger.getLogger("com.company.program");
// 2. 默认的日志配置会把级别等于或高于INFO的所有消息记录到控制台
// 用户可以覆盖这个默认配置,但改变配置的过程有些复杂,因此,最好在你的应用中安装一个更合适的默认配置
// 以下代码确保将所有消息记录到应用特定的一个文件中,可以将这段代码放置在应用的main方法中
if (System.getProperty("java.util.logging.config.class") == null
&& System.getProperty("java.util.logging.config.file") == null) {
try {
Logger.getLogger("").setLevel(Level.ALL);
final int LOG_ROTATION_COUNT = 10;
//var myHandler = new FileHandler("%h/myApp.log", 0, LOG_ROTATION_COUNT);
//Logger.getLogger("").addHandler(myHandler);
} catch (Exception e) {
logger.log(Level.SEVERE, "Can't create log file handler", e);
}
}
// 3. 现在可以记录自己想要的内容了;但要牢记,所有级别为INFO、WARNING、SEVERE的消息都将显示到控制台
// 因此,最好只将对用户有意义的消息设置为这几个级别,将程序员想要的日志设置为FINE级别是一个很好的选择
// 想要调用 System.out.print 时,可以换成发出以下的日志消息
logger.fine("File open dialog canceled");
// 记录那些预料之外的异常也是一个不错的想法
try {
} catch (Exception e) {
logger.log(Level.FINE, "explanation", e);
}
//todo P314
}
public static void howToDebug() {
/*【调试技巧】P321*/
//1.打印或记录变量的值
int x = 1;
System.out.println("x=" + x);
Logger.getGlobal().info("x=" + x);
//要获得隐式参数的状态,也可以打印this对象的状态
// Logger.getGlobal().info("this=" + this);
//2.可以在每一个类中放置一个单独的main方法,这样就可以提供一个单元测试桩(stub),能够独立地测试类
// 可以建立一些对象,调用所有的方法,检查每个方法能否正确地完成工作
//3.可以使用 JUnit,这是一个流行的单元测试框架,利用它可以很容易地组织测试用例套件
// 只要对类做了修改,就需要运行测试,一旦发现bug,还要再补充另一个测试用例
//4.日志代理(logging proxy)是一个子类的对象,它可以截获方法调用,记录日志,然后调用超类中的方法
//例如,如果在调用Random类的nextDouble方法时出了问题,就可以如下创建一个代理对象,这是一个匿名子类的实例
var generator = new Random() {
public double nextDouble() {
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble: " + result);
return result;
}
};
//要想知道谁调用了这个方法,可以生成一个堆栈轨迹
//5.利用Throwable类的printStackTrace方法,可以从任意的异常对象获得堆栈轨迹
try {
} catch (Throwable t) {
t.printStackTrace();
throw t;
}
//不一定要通过异常来生成堆栈轨迹,只要在代码的某个位置插入下面的语句就可以获得堆栈轨迹
Thread.dumpStack();
//6.一般来说,堆栈轨迹显示在 System.err 上,可以如下将它捕获到一个字符串中
var out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String des = out.toString();
//7.将程序错误记入一个文件会很有用,但由于错误不是发到System.out,所以不能用 java MyProgram > error.txt
//而应如下捕获错误流 java MyProgram 2> error.txt
//要想在同一个文件中同时捕获 System.err 和 System.out,需要使用
// java MyProgram 1> error.txt 2>&1
//这条命令在bash和Windows shell中都有效
//8.在System.err中显示未捕获的异常堆栈轨迹不是一个理想的方法,如果最终用户碰巧看到这些信息,就会很慌乱
//更好的方法是将这些消息记录到一个文件中,可以用静态方法 Thread.setDefaultUncaughtExceptionHandler 改变未捕获异常的处理器
Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
//save information in log file
}
}
);
//9.要想观察类的加载过程,启动Java虚拟机时可以使用 -verbose 标志
//10.-Xlint选项告诉编译器找出常见的代码问题 javac -Xlint sourceFiles
//11.JDK提供一个名为jconsole的图形工具,可以显示有关虚拟机性能的统计结果
//12.Java任务管理器(Java Mission Control)是一个专业级性能分析和诊断工具,包含在Oracle JDK中
//类似jconsole,Java Mission Control可以关联到正在运行的虚拟机
// 它还能分析Java飞行记录器(Java Flight Recorder)的输出,这个工具可以从一个正在运行的Java应用程序收集诊断和性能分析数据
}
}
class FileFormatException extends IOException {
public FileFormatException() {
}
public FileFormatException(String message) {
super(message);
}
}
StackTraceTest
import java.util.Scanner;
public class StackTraceTest {
public static void main(String[] args) {
try (var in = new Scanner(System.in)) {
System.out.print("Enter n: ");
int n = in.nextInt();
factorial(n);
}
}
public static int factorial(int n) {
System.out.println("factorial(" + n + "):");
var walker = StackWalker.getInstance();
walker.forEach(System.out::println);
int r;
if (n <= 1) r = 1;
else r = n * factorial(n - 1);
System.out.println("return" + r);
return r;
}
}
泛型程序设计
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Predicate;
public class Chapter8 {
public static void main(String[] args) {
//在有泛型类之前,程序员必须使用Object编写适用于多种类型的代码,这很烦琐,也很不安全
//泛型的目标是提供让其他程序员可以轻松使用的类和方法而不会出现意外
whyGeneric();
simpleGenericClass();
genericMethod();
typeParameter();
genericAndVM();
limit();
genericClassExtendsRule();
wildcardType();
reflectionAndGeneric();
}
public static void whyGeneric() {
/*【为什么要使用泛型程序设计】P326*/
//泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用
//一个ArrayList类就可以收集任何类的对象,这就是泛型程序设计的一个例子
/*类型参数的好处*/
//Java增加泛型类之前,泛型程序设计是用继承实现的;ArrayList类只维护一个Object引用的数组
//这种方法存在两个问题。获取一个值时必须进行强制类型转换。没有错误检查,可以向数组列表中添加任何类的值。
//泛型提供了一个更好的解决方案:类型参数(type parameter)
var files1 = new ArrayList<String>();
//如果用一个明确的类型而不是var声明一个变量,则可以通过使用「菱形」语法省略构造器中的类型参数
ArrayList<String> files2 = new ArrayList<>();
//Java9扩展了菱形语法的使用范围,例如现在可以对匿名子类使用菱形语法
ArrayList<String> passwords = new ArrayList<>() {
public String get(int n) {
return super.get(n).replace(".", "*");
}
};
//编译器也可以充分利用这个类型信息;调用get时,不需要进行强制类型转换,编译器知道返回值类型为String,而不是Object
// String filename = files1.get(0);
//编译器还知道add方法有一个类型为String的参数,可以检查防止你插入错误类型的对象
/*谁想成为泛型程序员*/
//实现一个泛型类并不容易,你的任务是要预计到你的泛型类所有可能的用法
//例如ArrayList有一个方法addAll,用来添加另一个集合的全部元素
// 一个程序员可能想将一个ArrayList<Manager>中的所有元素添加到一个ArrayList<Employee>中
// 如何允许前一个调用,而不允许后一个调用呢?Java设计者发明了一个新概念解决这个问题,即通配符类型(wildcard type)
//应用程序员很可能不会编写太多的泛型代码,JDK开发人员已经做了很大努力,为所有的集合类提供了类型参数
//凭经验说,如果代码中原本涉及大量通用类型的强制类型转换,只有这些代码才会因使用类型参数而受益
}
public static void simpleGenericClass() {
/*【定义简单泛型类】P328*/
//Pair类引入了一个类型变量T,用尖括号括起来,放在类名后面
//泛型类可以有多个类型变量,例如 class Pair<T, U> {...}
//类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型
//注释:常见的做法是类型变量使用大写字母,而且很简短
// Java库使用变量E表示集合的元素类型,K和V分别表示表的键和值的类型
// T(必要时还可以用相邻的字母U和S)表示「任意类型」
//可以用具体的类型替换类型变量来实例化(instantiate)泛型类型,例如Pair<String>
// 可以把结果想象成一个普通类,它有构造器Pair<String>(String, String)
// 以及方法 String getFirst() 和 void setFirst(String)
// 换句话说,泛型类相当于普通类的工厂
}
public static void genericMethod() {
/*【泛型方法】P330*/
//还可以定义一个带有类型参数的方法
/*
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
*/
//泛型方法可以在普通类中定义,也可以在泛型类中定义
//注意,类型变量放在修饰符的后面,并在返回类型的前面
//当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面
String middle = Chapter8.<String>getMiddle("JoJo", "Set", "Public");
//大多数情况下,方法调用中可以省略类型参数,编译器有足够的信息推断出你想要的方法
//几乎所有情况下,泛型方法的类型推断都能正常工作,偶尔,编译器也会提示错误,比如
// double dMiddle = getMiddle(3.14, 1729, 0);
//编译器将把参数自动装箱为1个Double和2个Integer对象,然后寻找它们的共同超类
// 事实上,它找到了2个超类,Number和Comparable接口,Comparable接口本身也是一个泛型类型
//如果想知道编译器对一个泛型方法调用最终推断出哪种类型,可以故意引入一个错误,研究错误信息
//C++中要将类型参数放在方法名后,这有可能导致解析的二义性
// 例如,g(f<a, b>(c)) 可以理解为 用f<a, b>(c)的结果调用g,或者 用两个布尔值调用g
}
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
public static void typeParameter() {
/*【类型变量的限定】P331*/
//有时,类或方法需要对类型变量加以约束
//例如,计算数组中的最小元素,你可能需要限制T只能是实现了Comparable接口的类
//可以通过对类型变量T设置一个限定(bound)实现
// public static <T extends Comparable> T min(T[] a)
//实际上Comparable接口本身就是一个泛型类型,这里我们暂时忽略其复杂性以及编译器产生的警告
//在C++中,不能对模板参数加以限制,如果程序员用一个不适当的类型实例化一个模板,将会在模板代码中报告一个错误
// <T extends BoundingType>
//表示T应该是限定类型(bounding type)的子类型(subtype)
//T和限定类型可以是类,也可以是接口
//选择关键字extends的原因是它更接近子类型的概念
//一个类型变量或通配符可以有多个限定
// T extends Comparable & Serializable
//限定类型用&分隔,而逗号用来分隔类型变量
//在Java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类
//如果有一个类作为限定,它必须是限定列表中的第一个限定
}
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 1; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
@SuppressWarnings("unchecked")
public static void genericAndVM() {
/*【泛型代码和虚拟机】P333*/
//虚拟机没有泛型类型对象——所有对象都属于普通类
//下面介绍编译器如何「擦除」类型参数,以及这个过程对Java程序员有什么影响
/*类型擦除*/
//无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)
//这个原始类型的名字就是去掉类型参数后的泛型类型名
//类型变量会被擦除(erased),并替换为其限定类型(对于无限定的变量则替换为Object)
//例如,Pair<T>的原始类型如下
/*
public class Pair {
private Object first;
...
public Object getFirst() { return first; }
...
}
*/
//在程序中可以包含不同类型的Pair,例如Pair<String>或Pair<LocalDate>,但擦除类型后,它们都会变成原始的Pair类型
//C++会为每个模板的实例化产生不同的类型,这一现象称为「模板代码膨胀」,Java不存在这个问题的困扰
//[原始类型用第一个限定来替换类型变量],或者如果没有限定,就替换为Object
//编译器在必要时要插入强制类型转换
//为了提高效率,应该将标签(tagging)接口,即没有方法的接口,放在限定列表的末尾
/*转换泛型表达式*/
//编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换
//当访问一个泛型字段时也会插入强制类型转换
/*转换泛型方法*/
//类型擦除也会出现在泛型方法中,类型参数T被擦除,只留下限定类型
//方法的擦除带来了两个复杂问题,看一下示例:class DataInterval extends Pair<LocalDate>
//我们覆盖了setSecond方法来确保第二个值永远不小于第一个值,这个类擦除后变成
// class DataInterval extends Pair { public void setSecond(LocalDate second)... }
//令人感到奇怪的是,还有另一个从Pair继承的setSecond方法
// public void setSecond(Object second)
var interval = new DataInterval();
Pair<LocalDate> pair = interval;
//pair.setSecond(LocalDate.now());
//这里我们希望setSecond调用具有多态性,会调用最合适的那个方法
//由于pair引用一个DataInterval对象,所以应该调用DataInterval.setSecond
//问题在于类型擦除与多态发生了冲突,为了解决这个问题,编译器在DataInterval类中生成一个桥方法(bridge method)
// public void setSecond(Object second) { setSecond((LocalDate) second); }
//桥方法可能会变得很奇怪。假设DataInterval类也覆盖了getSecond方法
//现在在DataInterval类中,有两个getSecond方法
// LocalDate getSecond()
// Object getSecond()
//不能这样编写Java代码(两个同名方法有相同的参数类型是不合法的)
//但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法,因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确处理这种情况
//注:桥方法不仅用于泛型类型
//前面提到过,一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,例如
// public class Employee implements Cloneable { public Employee clone() ... }
//Object.clone 和 Employee.clone 方法被称为「有协变的返回类型(covariant return type)」
//实际上,Employee类有两个克隆方法
// Employee clone()
// Object clone()
//合成的桥方法会调用新定义的方法
//总之,对于Java泛型的转换,需要记住以下几个事实
// · 虚拟机中没有泛型,只有普通的类和方法
// · 所有的类型参数都会替换为它们的限定类型
// · 会合成桥方法来保持多态
// · 为保持类型安全性,必要时会插入强制类型转换
/*调用遗留代码*/
//涉及Java泛型时,主要目标是允许泛型代码和遗留代码之间能够互操作
//将一个Dictionary<Integer, Component> 对象传递给 setLabelTable(Dictionary table) 时,编译器会发出一个警告
// (没有类型参数的Dictionary时一个原始类型,这里就存在兼容性问题)
//因为编译器无法确定setLabelTable可能会对Dictionary对象做什么操作,这个方法可能会打破键类型必须为Integer的承诺,未来的操作有可能导致糟糕的强制类型转换
//要仔细考虑这个问题,如果只读取这个信息,就可以忽略这个警告
//相反,由一个遗留类得到一个原始类型的对象,可以将它赋给一个类型使用了泛型的变量,这也会看到一个警告
// Dictionary<Integer, Component> labelTable = slider.getLabelTable();
//恶意的程序员可能会安装一个不同的Dictionary,但这种情况并不会比有泛型前的情况更糟糕,最差的情况也就是程序抛出一个异常
//考虑了警告之后,可以使用注解(annotation)使之消失,可以对一个局部变量加注解
// @SuppressWarnings("unchecked")
// Dictionary<Integer, Component> labelTable = slider.getLabelTable();
//或者可以对整个方法加注解
// @SuppressWarnings("unchecked")
// public void configureSlider() { ... }
//这个注解会关闭对方法中所有代码的检查
}
static class DataInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
public LocalDate getSecond() {
return (LocalDate) super.getSecond();
}
}
public static void limit() {
/*【限制与局限性】P338*/
//使用Java泛型时需要考虑一些限制,大多数限制都是由类型擦除引起的
/*不能用基本类型实例化类型参数*/
//不能用基本类型代替类型参数,因此没有Pair<double>,只有Pair<Double>
//原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值
//这并不是一个致命的缺陷,只有8种基本类型,而且即使不能接受包装器类型(wrapper type),也可以使用单独的类和方法来处理
/*运行时类型查询只适用于原始类型*/
//虚拟机中的对象总有一个特定的非泛型类型,因此所有的类型查询只产生原始类型
// if (a instanceof Pair<String>) //error
// if (a instanceof Pair<T>) //error
// Pair<String> p = (Pair<String>) obj; //warning: can only test that obj is a Pair
//为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,你会得到一个编译器错误(使用instanceof时),或者得到一个警告(使用强制类型转换时)
//同理,getClass方法总是返回原始类型
Pair<String> stringPair = new Pair<>();
Pair<Double> doublePair = new Pair<>();
if (stringPair.getClass() == doublePair.getClass()) { //始终为真
System.out.println("stringPair.getClass() == doublePair.getClass()");
}
//两次getClass调用都返回Pair.class
/*不能创建参数化类型的数组*/
// var table = new Pair<String>[10]; //error
//擦除之后,table的类型是Pair[],可以把它转换为Object[]
//数组会记住它的元素类型,如果试图存储其他类型的元素,会抛出一个ArrayStoreException
//不过对于泛型类型,擦除会使这种机制无效。出于这个原因,不允许创建参数化类型的数组
//注意,只是不允许创建这些数组,而声明变量仍然是合法的
Pair<String>[] pairs;
//不过不能用new Pair<String>[10]初始化这个变量
//注:可以声明通配类型的数组,然后进行强制类型转换
var table = (Pair<String>[]) new Pair<?>[10];
//结果将是不安全的
//如果在table[0]中存储一个Pair<Employee>,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCastException
//提示:如果需要收集参数化类型对象,简单地使用 ArrayList<Pair<String>> 更安全、有效
/*Varargs警告*/
//向参数个数可变的方法传递一个泛型类型的实例,考虑 public static <T> void addAll(Collection<T> coll, T... ts) { for(T t : ts) coll.add(t); }
//回忆一下,实际上参数ts是一个数组,包含提供的所有实参
//考虑以下调用
/*
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
*/
//为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这就违反了前面的规则
//不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误
//可以采用两种方法来抑制这个警告,一是为包含addAll调用的方法增加注解 @SuppressWarnings("unchecked")
//或者在Java7中,还可以用@SafeVarargs直接注解addAll方法
//对于任何只需要读取参数数组元素的方法,都可以使用这个注解
//@SafeVarargs只能用于声明为static、final或(Java9中)private的构造器和方法
//所有其他方法都可能被覆盖,使得这个注解没有什么意义
//注意:可以使用@SafeVarargs注解来消除创建泛型数组的有关限制
// @SafeVarargs static <E> E[] array(E... array) { return array; }
//现在可以调用
Pair<String> pair1 = new Pair<>();
Pair<String> pair2 = new Pair<>();
Pair<String>[] stringPairs = array(pair1, pair2);
//这看起来很方便,不过隐藏着危险
Object[] objects = table;
objects[0] = new Pair<Double>();
//以上代码能顺利运行而不会出现ArrayStoreException (因为数组存储只会检查擦除的类型)
//但在处理table[0]时,你会在别处得到一个异常
/*不能实例化类型变量*/
//不能在类似new T(...)的表达式中使用类型变量,例如下面的Pair<T>构造器就是非法的
// public Pair() { first = new T(); second = new T(); } //error
//类型擦除将T变成Object,而你肯定不希望调用new Object()
//在Java8之后,最好的办法是让调用者提供一个构造器表达式
Pair<String> p1 = Pair.makePair(String::new);
//makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数且返回类型为T的函数
//比较传统的解决方案是通过反射调用 Constructor.newInstance 方法来构造泛型对象
//必须适当地设计API以便得到一个Class对象
Pair<String> p2 = Pair.makePair(String.class);
//注意:Class类本身是泛型的
//例如,String.class是一个Class<String>的实例(事实上,它是唯一的实例)
//因此,makePair能够推断出所建立的pair的类型
/*不能构造泛型数组*/
//就像不能实例化泛型实例一样,也不能实例化数组
/*
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = new T[2]; //error
}
*/
//类型擦除会让这个方法总是构建 Comparable[2] 数组
//如果数组仅仅作为一个类的私有实例字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换
/*
private Object[] elements;
@SuppressWarnings("unchecked")
public E get(int n) { return (E) elements[n]; }
public void set(int n, E e) { elements[n] = e; }
*/
/*
private E[] elements;
public ArrayList() { elements = (E[]) new Object[10]; }
*/
//这里,强制类型转换 E[] 是一个假象,而类型擦除使其无法察觉
//这个技术并不适用于minmax方法,假设编写
/*
public static <T extends Comparable> T[] minmax(T... a) {
var result = new Comparable[2];
...
return (T[]) result;
}
*/
//以下调用
// String[] name = minmax("Tom", "Dick", "Harry");
//编译时不会有任何警告,但当方法返回Comparable[]引用强制转换为String[]时,会出现ClassCastException
//在这种情况下,最好让用户提供一个数组构造器表达式
// String[] name = minmax(String::new, "Tom", "Dick", "Harry");
//minmax方法使用这个参数生成一个有正确类型的数组
/*
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] result = constr.apply(2);
...
}
*/
//比较老式的方法是利用反射,并调用Array.newInstance
/*
public static <T extends Comparable> T[] minmax(T... a) {
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
...
}
*/
//ArrayList类的toArray方法就没有这么幸运;它需要生成一个T[]数组,但没有元素类型,因此有下面两种不同的形式
// Object[] toArray()
// T[] toArray(T[] result)
//第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用result的元素类型构造一个足够大的新数组
/*泛型类的静态上下文中类型变量无效*/
//不能在静态字段或方法中引用类型变量,例如下面的方法行不通
/*
public class Singleton<T> {
private static T singleInstance; //error
public static T getSingleInstance() { //error
if(singleInstance == null) ...
return singleInstance;
}
}
*/
//如果这样可行,就可以声明一个Singleton<Random>共享一个随机数生成器,另外声明一个Singleton<JFileChooser>共享一个文件选择对话框
//但类型擦除后,只剩下Singleton类,它只包含一个singleInstance字段
//因此,禁止使用带有类型变量的静态字段和方法
/*不能抛出或捕获泛型类的实例*/
//既不能抛出也不能捕获泛型类的对象
//实际上,泛型类扩展Throwable都是不合法的
//catch子句中不能使用类型变量
/*可以取消对检查型异常的检查*/
//Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器
//不过可以利用泛型取消这个机制
/*
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
*/
//假设这个方法包含在接口Task中,如果有一个检查型异常e,并调用
// Task.<RuntimeException>throwAs(e);
//编译器就会认为e是一个非检查型异常
//以下代码会把所有异常都转换为编译器所认为的非检查型异常
/*
try {
} catch (Throwable t) {
Task.<RuntimeException>throwAs(t);
}
*/
//下面使用这个技术解决一个棘手的问题
//要在一个线程中运行代码,需要把代码放在一个实现了Runnable接口的类的run方法中
//不过这个方法不允许抛出检查型异常
// Task.java
var thread = new Thread(Task.asRunnable(() -> {
Thread.sleep(1000);
System.out.println("Hello, World!");
throw new Exception("Check this out!");
}));
thread.start();
//Thread.sleep方法声明为抛出一个InterruptedException,我们不再需要捕获这个异常
//由于我们没有中断这个线程,InterruptedException不会被抛出
//不过,程序会抛出一个检查型异常;运行程序时,你会得到一个堆栈轨迹
//这有什么意义呢?正常情况下,你必须捕获一个Runnable的run方法中的所有检查型异常
// 把它们「包装」到非检查型异常中,因为run方法声明为不抛出任何检查型异常
//这里我们只是抛出异常,并哄骗编译器。让它相信这不是一个检查型异常
//通过使用泛型类、擦除和@SuppressWarnings注解,我们就能消除Java类型系统的部分基本限制
/*注意擦除后的冲突*/
//当泛型类型被擦除后,不允许创建引发冲突的条件
//假设Pair类有一个equals方法 public boolean equals(T value)
//考虑创建一个Pair<String>,从概念上讲,它有两个equals方法 equals(String) equals(Object)
//但是,boolean equals(T) 擦除后就是 boolean equals(Object),这会与Object.equals方法发生冲突
//如果两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类
class Employee implements Comparable<Employee> {
public int compareTo(Employee o) {
return 0;
}
}
//class Manager extends Employee implements Comparable<Manager> { } //ERROR
//Manager会实现Comparable<Employee>和Comparable<Manager>,这是同一接口的不同参数化
//其原因非常微妙,有可能与合成的桥方法产生冲突
//实现了 Comparable<X> 的类会获得一个桥方法 public int compareTo(Object other) { return compareTo((X) other); }
//不能对不同的类型X有两个这样的方法
}
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t : ts) coll.add(t);
}
@SafeVarargs
static <E> E[] array(E... array) {
return array;
//这看起来很方便,不过隐藏着危险
}
public static void genericClassExtendsRule() {
/*【泛型类型的继承规则】P346*/
//无论S与T有什么关系,通常,Pair<S>与Pair<T>都没有任何关系
//这对于类型安全非常必要,假设允许将Pair<Manager>转换为Pair<Employee>,就可能将一个Manager和一个Employee组成一组,这对于Pair<Manager>来说应该是不可能的
//这是泛型与数组间的一个重要区别,不过,数组有特别的保护,如果试图存储一个低级别的员工,虚拟机会抛出ArrayStoreException
//总是可以将参数化类型转换为一个原始类型;例如Pair<Manager>是原始类型Pair的一个子类型
//泛型类可以扩展或实现其他的泛型类,这一点它们与普通的类没有什么区别
//例如,ArrayList<T>类实现了List<T>接口,这意味着一个ArrayList<Manager>可以转换为一个List<Manager>
// 但如前面所见,ArrayList<Manager>不是一个ArrayList<Employee>或List<Employee>
}
public static void wildcardType() {
/*【通配符类型】P348*/
/*通配符概念*/
//在通配符类型中,允许类型参数发生变化;例如通配符类型 Pair<? extends Employee>
// 表示任何泛型Pair类型,它的类型参数是Employee类的子类
//假设编写一个打印员工对的方法
// public static void printBuddies(Pair<? extends Employee> p)
//使用通配符会通过Pair<? extends Employee>的引用破坏Pair<Manager>吗?
// 如果将一个Pair<Manager>转换为Pair<? extends Employee>,调用setFirst方法试图存储一个Employee对象,会产生一个编译时错误
var managerBuddies = new Pair<Manager>(new Manager(), new Manager());
Pair<? extends Employee> wildcardBuddies = managerBuddies;
//wildcardBuddies.setFirst(new Employee());
//Pair<? extends Employee> 的方法如下 void setFirst(? extends Employee)
//这样不可能调用setFirst方法,编译器只知道需要Employee的某个子类型,但不知道具体是什么类型
//它拒绝传递任何特定的类型
//使用getFirst就不存在这个问题
//这就是引入了有限定的通配符的关键之处,现在已经有办法区分安全的访问器方法和不安全的更改器方法了
/*通配符的超类型限定*/
//可以指定一个超类型限定(supertype bound)
// ? super Manager
//这个通配符限制为Manager的所有超类型
//[带有超类型限定的通配符与上面介绍的相反,可以为方法提供参数,但不能使用返回值]
//编译器无法知道setFirst方法的具体类型,因此不能接受Employee或Object,只能传递Manager类型的对象
//如果调用getFirst,不能保证返回对象的类型,只能把它赋给一个Object
//直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象
//由于Comparable是一个泛型类型,也许可以把ArrayAlg类的min方法做得更好一些,可以这样声明
// public static <T extends Comparable<T>> T min(T[] a)
//对许多类来说,这样工作得更好;例如计算一个String数组的最小值,T就是String,而String是Comparable<String>的一个子类型
//[但是],在处理一个LocalDate对象的数组时,我们会遇到一个问题,LocalDate实现了ChronoLocalDate
// 而ChronoLocalDate扩展了Comparable<ChronoLocalDate>,因此LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>
//[在这种情况下,可以利用超类型来解决]
// public static <T extends Comparable<? super T>> T min(T[] a)
//现在,compareTo方法写成 int compareTo(? super T)
//注:子类型限定的另一个常见用法是作为一个函数式接口的参数类型
//例如Collection接口有一个方法 default boolean removeIf(Predicate<? super E> filter)
//这个方法会删除所有满足给定谓词条件的元素,例如,如果你不喜欢有奇怪散列码的员工,可以如下将他们删除
ArrayList<Employee> staff = new ArrayList<>();
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);
//你希望传入一个Predicate<Object>,而不只是Predicate<Employee>,super通配符可以使这个愿望成真
/*无限定通配符*/
//Pair<?> 初看起来,这好像与原始的Pair一样,实际上,这两种类型有很大的不同
//类型Pair<?>有以下方法
// ? getFirst()
// void setFirst(?)
//getFirst的返回值只能赋给一个Object
//setFirst方法不能被调用,甚至不能用Object调用 (可以调用setFirst(null))
//Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setFirst方法
//这个类型对很多简单操作非常有用,例如测试一个Pair是否包含一个null引用,它不需要实际的类型
/*通配符捕获*/
//编写一个swap方法来交换Pair的元素
//通配符不是类型变量,因此不能在编写代码中使用?作为一种类型
//这是一个问题,因为在交换的时候必须临时保存第一个元素
//这个问题有一个有趣的解决方案,我们可以写一个辅助方法swapHelper
//注意,swapHelper是一个泛型方法,而swap不是,它有一个固定的Pair<?>类型的参数
//在这种情况下,[swapHelper方法的参数T捕获通配符]
//它不知道通配符指示哪种类型,但这是一个明确的类型,并且从<T>swapHelper的定义可以清楚地看到T指示那个类型
//虽然我们页可以直接把swap实现为一个没有通配符的泛型方法,但在下面这个例子中,通配符捕获机制是不可避免的
// public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
// minmaxBonus(a, result);
// swapHelper(result);
// }
//通配符捕获只有在非常限定的情况下才是合法的,编译器必须能够保证通配符表示单个确定的类型
//例如,ArrayList<Pair<T>>中的T永远不能捕获ArrayList<Pair<?>>中的通配符,数组列表可以保存两个Pair<?>,其中?分别有不同的类型
}
public static void printBuddies(Pair<? extends Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.toString() + second.toString());
}
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
//get min and max bonus
result.setFirst(min);
result.setSecond(max);
}
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);
}
public static void reflectionAndGeneric() {
/*【反射和泛型】P354*/
//反射允许你在运行时分析任意对象,如果对象是泛型类的实例,关于泛型类型参数你将得不到太多信息,因为它们已经被擦除了
/*泛型Class类*/
//Class类是泛型的,例如String.class实际上是Class<String>类的唯一对象
//newInstance方法返回这个类的一个实例,由无参数构造器获得;它的返回类型现在被声明为T,与Class<T>描述的类相同,这样就免除了类型转换
//如果给定对象的类型实际上是T的一个子类型,cast方法就会返回这个给定对象(现在声明为类型T),否则会抛出一个BadCastException
//如果这个类不是enum类或T类型枚举值的数组,getEnumConstants方法将返回null
//最后,getConstructor与getDeclaredConstructor方法返回一个Constructor<T>对象,Constructor类也已经变成泛型,从而使newInstance方法有一个正确的返回类型
/*使用Class<T>参数进行类型匹配*/
//匹配泛型方法中Class<T>参数的类型变量有时会很有用,例如下面的makePair
//调用makePair(Employee.class),T同Employee匹配,编译器可以推断出这个方法将返回一个Pair<Employee>
/*虚拟机中的泛型类型信息*/
//Java泛型的突出特征之一是在虚拟机中擦除泛型类型
//但擦除的类仍然保留原先泛型的微弱记忆
//例如,原始的Pair类知道它源于泛型类Pair<T>,尽管一个Pair类型的对象无法区分它是构造为Pair<String>还是Pair<Employee>
//考虑以下方法
// 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[])
//todo 示例代码 P357
/*类型字面量*/
//有时,你会希望由值的类型决定程序的行为。通常的实现方法是将Class对象与一个动作关联。
//不过,如果有泛型类,擦除会带来问题
//这里有一个技巧,在某些情况下可以解决这个问题;可以捕获Type接口的一个实例,然后构造一个匿名子类
//todo P359
}
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException {
return new Pair<>(c.newInstance(), c.newInstance());
}
}
Pair.java
import java.util.function.Supplier;
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 static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance());
} catch (Exception e) {
return null;
}
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Task.java
public interface Task {
void run() throws Exception;
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
static Runnable asRunnable(Task task) {
return () -> {
try {
task.run();
} catch (Exception e) {
Task.<RuntimeException>throwAs(e);
}
};
}
}
Collection
import java.io.*;
import java.time.LocalDate;
import java.util.*;
import java.util.logging.LogManager;
public class Chapter9 {
public static void main(String[] args) {
javaCollectionFrame();
collectionFrameInterface();
specificCollection();
map();
viewAndWrapper();
algorithm();
leftCollection();
}
public static void javaCollectionFrame() {
/*【Java集合框架】P365*/
/*集合接口与实现分离*/
//与现代的数据结构类库的常见做法一样,Java集合类库也将接口(interface)与实现(implementation)分离
//例如队列(queue)接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数
//队列接口没有说明队列是如何实现的;队列通常有两种实现方式:循环数组、链表;每一个实现都可以用一个实现了Queue接口的类表示
//使用队列时,一旦已经构造了集合,就不需要知道究竟使用了哪种实现
//只有在构造集合对象时,才会使用具体的类;可以使用接口类型存放集合引用
//利用这种方法,一旦改变了想法,就可以轻松使用另外一种不同的实现;只需要对程序调用构造器的地方进行修改
// 循环数组比链表更高效,因此多数人优先选择循环数组,不过,循环数组是一个有界集合,即容量有限,如果程序要收集的对象数量没有上限,就最好使用链表来实现
//API文档中还有另外一组以Abstract开头的类,例如 AbstractQueue,这些类是为类库实现者而设计的
//如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多
/*Collection接口*/
//Java类库中,集合类的基本接口是Collection接口,它有几个基本方法
//add方法用于向集合中添加元素,如果添加集合确实改变了集合就放回true,如果集合没有发生变化就返回false
// 例如:如果向集(set)中添加一个对象,而这个对象在集合中已经存在,这个add请求就没有实效,因为集合中不允许有重复的对象
//iterator方法返回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素
/*迭代器*/
//Iterator接口包含4个方法
//通过反复调用next方法,可以逐个访问集合中的每个元素;但如果到达了集合的末尾,next方法将抛出一个NoSuchElementException
//因此,需要在调用next之前调用hasNext方法,如果迭代器对象还有多个可以访问的元素,这个方法就返回true
//如果想要查看集合中的所有元素,就请求一个迭代器,当hasNext返回true时就反复调用next方法
Collection<String> c = new ArrayList<>();
Iterator<String> iterator = c.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
//do something
}
//用foreach循环可以更加简练地表示同样的循环操作
for (String element : c) {
//do something
}
//编译器简单地将foreach循环转换为带有迭代器的循环
//[foreach循环可以处理任何实现了Iterable接口的对象,这个接口只包含一个抽象方法 Iterator<E> iterator();]
//Collection接口扩展了Iterable接口,因此,[标准类库中的任何集合都可以使用foreach循环]
//也可以不写循环,而是调用forEachRemaining方法并提供一个lambda表达式(它会处理一个元素);将对迭代器的每一个元素调用这个lambda表达式,直到再没有元素为止
iterator.forEachRemaining(element -> element = null);
//访问元素的顺序取决于集合类型;如果迭代处理一个ArrayList,迭代器将从索引0开始,每迭代一次,索引值加1
//不过,如果访问HashSet中的元素,会按照一种基本上随机的顺序获得元素;虽然可以确保在迭代过程中能够遍历到集合中的所有元素,但是无法预知访问各元素的顺序
// 这通常不是什么问题,因为对于计算总和或统计匹配之类的计算,顺序并不重要
//注释:Iterator接口的next和hasNext方法与Enumeration接口的nextElement和hasMoreElements方法的作用一样
// Java集合类库的设计者本来可以选择使用它,但他们不喜欢这个接口累赘的方法名,于是引入了具有较短方法名的新接口
//Java集合类库中的迭代器与其他类库中的迭代器在概念上有着重要的区别
//在传统的集合类库中,例如C++的标准模板库,迭代器是根据数组索引建模的;如果给定这样一个迭代器,可以查找存储在指定位置上的元素,就像知道数组索引i就可以查找数组元素a[i]
// 不需要查找元素,也可以将迭代器向前移动一个位置,这与不需要执行查找操作而通过调用i++将数组索引向前移动一样
//但Java迭代器并不是这样处理的,查找操作与位置变得更紧密耦合
// 查找一个元素的唯一方法式调用next,而在执行查找操作的同时,迭代器的位置就会随之向前移动。
//因此,可以认为Java迭代器位于两个元素之间,当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用
//注释:可以将Iterator.next与InputStream.read看成等效的,从数据流中读取一个字节,就会自动地「消耗掉」这个字节,下一次调用read将会消耗并返回输入的下一个字节
//Iterator接口的remove方法将会删除上次调用next方法时返回的元素;大多数情况下这是有道理的,在决定删除某个元素之前应该先看一下这个元素
//如果想要删除指定位置上的元素,则仍然需要越过这个元素
//[更重要的是,next方法和remove方法调用之间存在依赖性;如果调用remove之前没有调用next,将是不合法的,如果这样做,将会抛出一个IllegalStateException]
//[如果想删除两个相邻的元素,不能连续调用remove,必须先调用next越过将要删除的元素]
/*泛型实用方法*/
//由于Collection与Iterator都是泛型接口,这意味着你可以编写处理任何集合类型的实用方法
//下面是一个检测任意集合是否包括指定元素的泛型方法
contains(c, null);
//Java类库的设计者认为:这些实用方法中有一些非常有用,应该将它们提供给用户使用;这样类库的使用者就不必自己重新构建这些方法了
// contains就是这样一个实用方法
//事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法
c.contains(null);
//当然,如果实现Collection接口的每一个类都要提供如此多的例行方法,这将是一件很烦人的事情
//为了能够让实现者更容易地实现这个接口,Java类库提供了一个类 AbstractCollection,它保持基础方法size和iterator仍为抽象方法,但是为实现者实现了其他例行方法
//这样一来,具体集合类可以扩展AbstractCollection;不过,如果子类有更高效的方式实现contains方法,也完全可以由子类提供contains方法
//这种做法有些过时了;这些方法最好是Collection接口的默认方法;不过,确实已经增加了很多默认方法,其中大部分都与流的处理有关
//另外,还有一个很有用的方法,这个方法用于删除满足某个条件的元素
// default boolean removeIf(Predicate<? super E> filter)
//这个方法用于删除满足某个条件的元素
}
public static <E> boolean contains(Collection<E> c, Object obj) {
for (E element : c) {
if (element.equals(obj)) return true;
}
return false;
}
public static void collectionFrameInterface() {
/*【集合框架中的接口】P373*/
//集合中有两个基本接口:Collection 和 Map
//可以用add方法在集合中插入元素,不过,由于映射包含键值对,所以要用put方法来插入
// V put(K key, V value)
//要从集合读取元素,可以用迭代器访问元素,不过,从映射中读取值则要使用get方法
// V get(K key)
//[List是一个有序集合(ordered collection),元素会增加到容器中的特定位置]
//可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问
//后面这种方法称为随机访问(random access),可以按任意顺序访问元素,而使用迭代器访问时必须按顺序访问元素
//List接口定义了多个用于随机访问的方法:
// void add(int index, E element)
// void remove(int index)
// E get(int index)
// E set(int index, E element)
//ListIterator 是 Iterator 的一个子接口,它定义了一个方法用于在迭代器位置前面增加一个元素
// void add(E element)
//[实际上有两种有序集合,其性能开销有很大差异]:
//由数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问
//与之不同,链表尽管也是有序的,但是随机访问很慢,所以最好使用迭代器来遍历
//注:为了避免对链表进行随机访问操作,Java1.4引入了一个标记接口 RandomAccess
//这个接口不包含任何方法,不过可以用它来测试一个特定的集合是否支持高效的随机访问
List<String> list = new LinkedList<>();
if (list instanceof RandomAccess) {
System.out.println("TRUE");
} else {
System.out.println("FALSE");
}
//Set接口是Collection的一个子接口,集(set)的add方法不允许增加重复的元素
//要适当地定义集的equals方法:只要两个集包含同样的元素就认为它们是相等的,而不要求这些元素有同样的顺序
//hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码
//SortedSet和SortedMap接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法
//最后,Java6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法
// (理想情况下,这些方法本应直接包含在SortedSet和SortedMap接口中)
// TreeSet和TreeMap类实现了这些接口
}
public static void specificCollection() {
/*【具体集合】P375*/
//以下类除了以Map结尾的类之外,都实现了Collection接口,而以Map结尾的类实现了Map接口
ArrayList arrayList; //可以动态增长和缩减的一个索引序列
LinkedList linkedList; //可以在任何位置高效插入和删除的一个有序序列
ArrayDeque arrayDeque; //实现为循环数组的一个双端队列
HashSet hashSet; //没有重复元素的一个无序集合
TreeSet treeSet; //一个有序集
EnumSet enumSet; //一个包含枚举类型值的集
LinkedHashSet linkedHashSet; //一个可以记住元素插入次序的集
PriorityQueue priorityQueue; //允许高效删除最小元素的一个集合
HashMap hashMap; //存储键/值关联的一个数据结构
TreeMap treeMap; //键有序的一个映射
EnumMap enumMap; //键属于枚举类型的一个映射
LinkedHashMap linkedHashMap; //可以记住键/值项添加次序的一个映射
WeakHashMap weakHashMap; //值不会在别处使用时就可以被垃圾回收的一个映射
IdentityHashMap identityHashMap;//用==而不是用equals比较键的一个映射
/*链表*/
//在Java中,[所有链表实际上都是双向链接的(doubly linked)——即每个链接还存放着其前驱的引用]
var staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator<String> iterator = staff.iterator();
String first = iterator.next();
String second = iterator.next();
iterator.remove(); //删除第二个元素
//链表是一个有序集合(ordered collection),LinkedList.add方法将对象添加到链表的尾部
//但常常需要将元素添加到链表的中间,由于迭代器描述了集合中的位置,所以这种依赖于位置的add方法由迭代器负责
//只有对自然有序的集合使用迭代器添加元素才有实际意义;例如在集(set)中,元素是完全无序的,因此Iterator接口中没有add方法
//实际上,集合类库提供了一个子接口ListIterator,其中包含add方法
//与Collection.add不同,这个方法不返回boolean值,它假定add操作总会改变链表
//另外,ListIterator接口有两个方法,可以用来反向遍历链表
// E previous()
// boolean hasPrevious()
//与next一样,previous返回越过的对象
//LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象
//add方法在迭代器位置[之前]添加一个新对象,如果多次调用add方法,将按照提供的次序把元素添加到链表中
ListIterator<String> listIterator = staff.listIterator();
listIterator.next();
listIterator.previous();
listIterator.add("");
listIterator.add("");
//注:不能连续两次调用remove,add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态
//set方法用一个新元素替换调用next或previous方法返回的上一个元素
//如果一个迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个ConcurrentModificationException
//为了避免发生并发修改异常,请遵循这样一个简单的规则:
// 可以根据需要为一个集合关联多个迭代器,前提是这些迭代器只能读取集合;或者可以再关联一个能同时读写的迭代器
//集合可以跟踪更改操作的次数,每个迭代器都会为它负责的更改操作维护一个单独的更改操作数
// 在每个迭代器方法的开始处,迭代器会检查它自己的更改操作数是否与集合的更改操作数相等,如果不一致,就抛出ConcurrentModificationException
//注:链表只跟踪对列表的结构性修改,例如添加和删除链接,set方法不被视为结构性更改
// 可以为一个链表关联多个迭代器,所有的迭代器都可以调用set方法修改现有链接的内容
//Collection接口还声明了操作链表的很多其他有用的方法,其中大部分方法都是在LinkedList类的超类AbstractCollection中实现的
//例如toString方法调用所有元素的toString方法产生一个[A,B,C]格式的长字符串
//使用contains方法检测某个元素是否出现在链表中
//Java类库中还提供了许多在理论上存在一定争议的方法,链表不支持快速随机访问,但LinkedList类还是提供了一个用来访问某个特定元素的get方法
// LinkedList<String> stringLinkedList = ...;
// String obj = stringLinkedList.get(n);
//当然,这个方法效率并不高,如果发现自己正在使用这个方法,说明对于所要解决的问题,你可能使用了错误的数据结构
// get方法做了一个微小的优化:如果索引大于等于size()/2,就从列表尾部开始搜索元素
//列表迭代器接口还有一个方法,可以告诉你当前位置的索引
//从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以有两个索引
listIterator.nextIndex();
listIterator.previousIndex();
//这两个方法的效率非常高,因为有一个迭代器保持着当前位置的计数值
//如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置
/*数组列表*/
//上一节介绍了List接口和实现了这个接口的LinkedList类
//List接口用于描述一个有序集合,并且集合中每个元素的位置很重要
//ArrayList类也实现了List接口,它封装了一个动态再分配的对象数组
//注:Vector类的所有方法都是同步的,可以安全地从两个线程访问一个Vector对象
// 但如果只从一个线程访问,代码就会在同步操作上白白浪费大量的时间
// ArrayList方法不是同步的,因此,建议在不需要同步时使用ArrayList
/*散列集*/
//如果不在意元素的顺序,有几种能快速查找元素的数据结构,其缺点是无法控制元素出现的次序,这些数据结构将按照对自己最方便的方式组织元素
//有一种众所周知的数据结构,可以用于快速查找对象,这就是散列表(hash table)
//散列表为每个对象计算一个散列码(hash code),有不同数据的对象将产生不同的散列码
//如果定义你自己的类,你就要负责实现自己的hashCode方法
//注意,你的实现应该与equals方法兼容,即如果a.equals(b)为true,a与b必须有相同的散列码
//在Java中,散列表用链表数组实现,每个列表被称为桶(bucket)
//要查找表中对象的位置,就要[先计算它的散列码,然后与桶的总数取余],所得结果就是保存这个元素的桶的索引
//要插入一个元素到桶中,有时候会遇到桶已经被填充的情况,这被称为散列冲突(hash collision)
//这时,需要将新对象与桶中所有对象进行比较,查看这个对象是否已经存在
// 如果散列码合理地随机分布,桶的数目也足够大,需要比较的次数就会很少
//如果想更多地控制散列表的性能,可以指定一个初始的桶数;桶数是指用于收集有相同散列值的桶的数目
//如果要插入到散列表中的元素太多,就会增加冲突数量,降低检索性能
//如果大致知道最终会有多少个元素要插入,就可以设置桶数
//[通常,将桶数设置为预计元素个数的75%~150%]
//有些研究人员认为,最好将桶数设置为一个素数,以防止键的聚集;不过,对此并没有确凿的证据
//[标准类库使用的桶数是2的幂,默认值为16,为表大小提供的任何值都将自动地转换为2的下一个幂值]
//如果散列表太满,就需要再散列(rehashed);这需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表
//装填因子(load factor)可以确定何时对散列表进行再散列;
// 例如,如果装填因子为0.75(默认值),说明表中已填满了75%以上,就会自动再散列,新表的桶数是原来的2倍
//Java集合类库的HashSet类中的contains方法已经被重新定义,它只查看一个桶中的元素,而不必查看集合中的所有元素
//散列表迭代器将依次访问所有的桶,由于散列将元素分散在表中,所以会以一种看起来随机的顺序访问元素
//在更改集中的元素时要格外小心,如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化
/*树集*/
//TreeSet类与散列集十分类似,但它比散列集有所改进
//树集是一个有序集合(sorted collection),可以任意顺序将元素插入,[遍历集合时,值将自动按排列后的顺序呈现]
//排列时用一个树数据结构完成的(当前实现使用红黑树 red-black tree)
//每次将一个元素添加到树中时,都会将其放置在正确的排列位置上;因此,迭代器总是以有序的顺序访问每个元素
//将一个元素添加到树中要比添加到散列表中慢,如果树中包含n个元素,查找新元素的位置平均需要log n次比较
//[要使用树集,必须能比较元素,元素必须实现Comparable接口,或者构造集时提供一个Comparator]
//树的排列顺序必须是全序,任意两个元素都必须是可比的,并且只有在两个元素相等时结果才为0
/*队列与双端队列*/
//双端队列(deque)允许在头部和尾部都高效地添加或删除元素
//ArrayDeque和LinkedList类都实现了Deque接口,这两个类都可以提供双端队列
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.offer(1);
queue.offer(1);
//如果队列已满,add会抛出异常,offer则返回false
queue.remove();
queue.poll();
//如果队列为空,remove抛出异常,poll则返回false
queue.element();
queue.peek();
//返回队头元素但不删除,如果队列空,element抛出异常,peek返回null
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(1);
deque.offerFirst(1);
deque.offerLast(1);
deque.removeFirst();
deque.pollLast();
deque.getFirst();
deque.peekLast();
/*优先队列*/
//priority queue中的元素可以按照任意的顺序插入,但会按有序的顺序进行检索
//无论何时调用remove方法,总会获得当前优先队列中最小的元素
//不过,[优先队列并没有对所有元素进行排序];如果迭代处理这些元素,并不需要对它们进行排序
//优先队列使用了一个数据结构,称为堆(heap)
//堆是一个可以自组织的二叉树,其add和remove操作可以让最小的元素移动到根,而不必花费时间对元素进行排序
//优先队列的典型用法时任务调度
//每个任务有一个优先级,任务以随机顺序添加到队列中,每启动一个新的任务,都将优先级最高的任务从队列中删除
//(由于习惯上将1设为「最高」优先级,所以remove操作会将最小的元素删除)
PriorityQueue<LocalDate> pq = new PriorityQueue<>();
pq.offer(LocalDate.of(1906, 12, 9));
pq.offer(LocalDate.of(1815, 12, 10));
pq.offer(LocalDate.of(1903, 12, 3));
for (LocalDate i : pq)
System.out.println(i);
System.out.println();
while (!pq.isEmpty())
System.out.println(pq.poll());
}
public static void map() {
/*【映射】P394*/
//集合允许你快速查找现有的元素,但首先需要有所要查找的元素的准确副本
//映射(map)用来存放键值对,如果提供了键,就能找到值
//Java类库为映射提供了两个通用的实现
HashMap<Integer, String> hashMap = new HashMap<>();
TreeMap<String, Integer> treeMap = new TreeMap<>();
//这两个类都实现了Map接口
//散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树
//散列或比较函数只应用于键,与键关联的值不进行散列或比较
//与Set一样,散列稍微快一点,如果不需要有序访问键,最好选择HashMap
hashMap.get(1);
//如果映射中没有存储与给定key对应的信息,get返回null
hashMap.getOrDefault(1, "defaultValue");
//getOrDefault方法可以在没有指定key的时候返回一个默认值
//key必须是唯一的,如果对同一个key调用两次put方法,第二个值会取代第一个值
hashMap.put(1, "Test");
System.out.println(hashMap.put(1, "Hi!"));
//put会返回与这个key关联的上一个值
hashMap.remove(1);
System.out.println(hashMap.size());
//要迭代处理映射的键和值,最容易的方法是使用forEach方法
//可以提供一个接收键和值的lambda表达式,映射中的每一项会依序调用这个表达式
hashMap.forEach((k, v) -> System.out.println("key=" + k + ", value=" + v));
/*更新映射条目*/
//处理映射的一个难点是更新映射条目
//正常情况下可以得到与一个key关联的原值,更新完再放回
//但必须考虑一个特殊情况:key第一次出现
//一种简单的补救是使用getOrDefault
treeMap.put("word", treeMap.getOrDefault("word", 0) + 1);
//另一种方法是首先调用putIfAbsent方法,只有当键不存在(或者映射到null)时才会放入一个值
treeMap.putIfAbsent("word", 0);
treeMap.put("word", treeMap.get("word") + 1);
//还可以做得更好,merge方法可以简化这个常见操作
treeMap.merge("word", 1, Integer::sum);
//如果key不存在,merge将把word与1关联,否则使用sum函数组合原值和1
/*映射视图*/
//集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键值对集合,或者是按键索引的值集合)
//不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象
//有3种视图:键集、值集合(不是Set)、键值对集
Set<String> keySet = treeMap.keySet();
Collection<Integer> values = treeMap.values();
Set<Map.Entry<String, Integer>> entrySet = treeMap.entrySet();
//映射条目集的元素是实现了Map.Entry接口的类的对象
//keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象
//Set接口扩展了Collection接口,因此,可以像使用任何集合一样使用keySet
//如果想同时查看键和值,可以通过枚举映射条目来避免查找值
//for (Map.Entry<String, Employee> entry : staff.entrySet())
//可以使用var声明避免笨拙的Map.Entry
//如今,只需要使用forEach方法
//如果在视图上调用迭代器的remove方法,会从映射中删除这个键值对
//不过,[不能向视图中添加元素],如果尝试调用add,会抛出异常
//可以在映射条目集视图上调用setValue方法,它将相关映射中的值改为新值,并返回原来的值
/*弱散列映射*/
//当对key的唯一引用来自散列表映射条目时,WeakHashMap将与垃圾回收器协同工作一起删除键值对
//WeakHashMap 使用 弱引用(weak references) 保存key
//WeakHashMap 对象将包括另一个对象的引用,在这里,就是一个散列表key
//对于这种类型的对象,垃圾回收器采用一种特有的方式进行处理
//[如果某个对象只能由WeakReference引用,垃圾回收器也会将其回收,但会将引用这个对象的弱引用放进一个队列]
//WeakHashMap将周期性地检查队列,以便找出新添加的弱引用
//一个弱引用进入队列意味着这个key不再被他人使用,并且已经回收,于是WeakHashMap将删除相关联的映射条目
/*链接散列集与映射*/
//LinkedHashSet 和 LinkedHashMap 类会记住插入元素项的顺序
//在表中插入元素项时,就会并入到双向链表中
//LinkedHashMap.keySet()/values().iterator() 以插入顺序枚举键值
//或者,LinkedHashMap可以使用访问顺序而不是插入顺序来迭代处理映射条目
//每次调用get或put时,受到影响的项将从当前的位置删除,并放到链表的尾部(只影响项在链表中的位置,散列表的桶不受影响)
//如下构造这样一个散列映射
new LinkedHashMap<Integer, Integer>(16, 0.75F, true);
/*枚举集与映射*/
//EnumSet是一个枚举类型元素集的高效实现
//由于枚举类型只有有限个实例,所以EnumSet内部用[位序列]实现,如果对应的值在集中,则相应的位被置为1
//EnumSet类没有公共的构造器,要使用静态工厂方法构造这个集
enum WeekDay {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}
EnumSet<WeekDay> always = EnumSet.allOf(WeekDay.class);
EnumSet<WeekDay> never = EnumSet.noneOf(WeekDay.class);
EnumSet<WeekDay> workDay = EnumSet.range(WeekDay.MONDAY, WeekDay.FRIDAY);
EnumSet<WeekDay> mwf = EnumSet.of(WeekDay.MONDAY, WeekDay.WEDNESDAY, WeekDay.FRIDAY);
//EnumMap是一个key类型为枚举类型的映射,它可以直接且高效地实现为一个值数组
//需要在构造器中指定key类型
var personInCharge = new EnumMap<WeekDay, String>(WeekDay.class);
//注:形如 E extends Enum<E> 的类型参数表示“E是一个枚举类型”,所有的枚举类型都扩展了泛型Enum类
/*标识散列映射*/
//类 IdentityHashMap 有特殊的用途
//在这个类中,key的散列值不是用hashCode函数计算的,而是用 System.identityHashCode 方法计算的
//这是Object.hashCode根据对象的内存地址计算散列码时所使用的方法
//而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不使用equals
//也就是说,不同的key对象即使内容相同,也被视为不同的对象
//在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪哪些对象已经遍历过
}
public static void viewAndWrapper() {
/*【视图与包装器】P403*/
//可以使用视图(view)获得其他实现了Collection接口或Map接口的对象,映射类的keySet方法返回的就是这样一个对象
//实际上,keySet方法返回一个实现了Set接口的类对象,由这个类的方法操纵原映射
//这种集合称为视图
/*小集合*/
//Java 9引入了一些静态方法,可以生成给定元素的集或列表,以及给定键值对的映射
List<String> list = List.of("1", "2", "3");
Set<Integer> set = Set.of(2, 3, 5);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
//元素、键或值不能为null
//List和Set接口有11个方法,分别有0到10个参数,另外还有一个参数个数可变的of方法,提供这种特定性是为了提高效率
//Map接口则无法提供一个参数可变的版本
// 不过它有一个静态方法ofEntries,能接受任意多个Map.Entry<K, V>对象(可以用静态方法entry创建)
Map<String, Integer> map1 = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2));
//of和ofEntries方法可以生成某些类的对象,这些类对于每个元素会有一个实例变量,或者有一个后备数组提供支持
//这些集合对象是不可修改的,如果试图改变它们的内容,会导致一个异常
// list.add("4"); //UnsupportedOperationException
//如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器
new ArrayList<>(List.of(1, 2, 3));
Collections.nCopies(100, "DEFAULT");
//这个调用会返回一个实现了List接口的不可变对象,给人一种错觉:就好像有n个元素,每个元素都是一个o对象
//这样存储开销很小,对象只存储一次
//注:of方法是Java 9新引入的,之前有一个静态方法Arrays.asList,它会返回一个可更改但是大小不可变的列表
//另外还有遗留的方法Collections.emptySet和Collections.singleton
//Collections类包含很多实用方法,它们的参数和返回值都是集合,不要将它与Collection接口混淆
//提示:Java没有Pair类,有些程序员会使用Map.Entry作为pair,但这种做法并不好
//Java 9之前,这会很麻烦,你必须如下构造对象
new AbstractMap.SimpleImmutableEntry<>("first", "second");
//不过现在可以简单如下调用
Map.entry("first", "second");
/*子范围*/
//可以为很多集合建立子范围(subrange)视图
//例如使用subList方法获得列表子范围的视图
ArrayList<String> arrayList = new ArrayList<>(list);
List<String> subList = arrayList.subList(0, 2); //索引左边包含右边不包含
//可以对子范围应用任何操作,而且操作会自动反映到整个列表,例如可以删除整个子范围
subList.clear();
//对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围,SortedSet接口声明了3个方法
SortedSet<Integer> sortedSet = new TreeSet<>(Set.of(3, 4, 2, 6, 1));
sortedSet.subSet(0, 2);
sortedSet.headSet(2);
sortedSet.tailSet(2);
//这些方法将返回大于等于from且小于to的所有元素构成的子集
//有序映射也有类似的方法
SortedMap<Integer, Integer> sortedMap = new TreeMap<>(Map.of(1, 1, 2, 2));
sortedMap.subMap(0, 1);
sortedMap.headMap(2);
sortedMap.tailMap(2);
//这些方法会返回映射视图,该映射包含key落在指定范围的所有元素
//Java 6引入的NavigableSet接口允许更多地控制这些子范围操作,可以指定是否包括边界
NavigableMap<Integer, Integer> navigableMap = new TreeMap<>();
NavigableSet<Integer> navigableSet = new TreeSet<>();
navigableSet.subSet(0, true, 2, true);
navigableSet.headSet(2, true);
navigableSet.tailSet(0, false);
/*不可修改的视图*/
//Collections类还有几个方法,可以生成集合的不可修改视图(unmodifiable view)
//这些视图对现有集合增加了一个运行时检查,如果发现试图对集合进行修改,就抛出一个异常
//可以使用这些方法获得不可修改视图
Collections.unmodifiableCollection(list);
Collections.unmodifiableList(list);
Collections.unmodifiableSet(set);
Collections.unmodifiableSortedSet(sortedSet);
Collections.unmodifiableNavigableSet(navigableSet);
Collections.unmodifiableMap(map);
Collections.unmodifiableSortedMap(sortedMap);
Collections.unmodifiableNavigableMap(navigableMap);
//每个方法都定义处理一个接口
//可以通过不可修改视图调用对应接口中的所有方法,而不只是访问器
//但所有的更改器方法被重定义为抛出一个异常,而不是将调用传递给底层集合
//由于视图只是包装了接口而不是具体的集合对象,所以只能访问接口中定义的方法
//例如LinkedList类中定义的addFirst和addLast,不能通过不可修改视图访问这些方法
//警告:unmodifiableCollection方法、synchronizedCollection方法、checkedCollection方法
//都将返回一个集合,它的equals方法不会调用底层集合的equals,实际上,它继承了Object类的equals,只检查两个对象是不是同一个
//如果将集或列表转换成集合,就再也无法检测其内容是否相同了,视图就采用这种工作方式,因为相等性检测在层次结构的这一层上没有明确定义
//视图将以同样的方式处理hashCode方法
//不过,unmodifiableList 和 unmodifiableSet 方法会使用底层集合的equals和hashCode方法
/*同步视图*/
//类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类
//Collections类的静态synchronizedMap方法可以将任何一个映射转换成有同步访问方法的Map
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(map);
//现在就可以从多线程访问这个map对象了,类似get和put等方法都是同步的,即每个方法调用必须完全结束,另一个线程才能调用另一个方法
/*检查型视图*/
//“检查型”视图用来对泛型类型可能出现的问题提供调试支持
//实际上将错误类型的元素混入泛型集合中的情况极有可能发生
ArrayList<String> strings = new ArrayList<>();
ArrayList rawList = strings;
rawList.add(new Date());
//这个错误的add命令在运行时检测不到
//检查型视图可以探测这类问题,下面定义了一个安全列表
List<String> safeStrings = Collections.checkedList(strings, String.class);
//这个视图的add方法将检查插入的对象是否属于给定的类,如果不属于,就立刻抛出一个异常
//警告:检查型视图受限于虚拟机可以完成的运行时检查。例如对于ArrayList<Pair<String>>,由于虚拟机有一个“原始”Pair类,所以无法阻止插入Pair<Date>
/*关于可选操作的说明*/
//通常,视图有一些限制,可能只读,可能无法改变大小,可能只支持删除而不支持插入
//在集合和迭代器接口的API文档中,许多方法描述为“可选操作”
//这看起来与接口的概念有冲突,毕竟接口的设计就是明确一个类必须实现的方法
//确实,从理论上看,这种安排不太令人满意
//一个更好的解决方案是为只读视图和不能改变集合大小的视图建立单独的接口
//不过,这将会使接口的数量增至原来的三倍,这让类库设计者无法接受
//我们认为不应该将“可选”方法扩展到你自己的设计中
//集合类库的设计者必须解决一组极其严格且相互冲突的需求,用户希望类库易于学习、使用方便、彻底泛型化、面向通用性、又与手写算法一样高效
//在你自己的编程问题中,很少遇到这种极端的约束,你应该能找到一种合适的解决方案,而不必依赖“可选”接口操作这种极端做法
}
public static void algorithm() {
/*【算法】P411*/
//标准C++类库已经有几十种非常有用的算法,每个算法都应用于泛型集合,Java类库中的算法没有这么丰富,但确实包含了一些基本算法
var linkedList = new LinkedList<String>();
Collections.sort(linkedList);
//如果想用其他方式排序
//linkedList.sort(Comparator.comparingDouble(Employee::getSalary));
//如果想降序
linkedList.sort(Comparator.reverseOrder());
//Java对链表的排序不是用归并排序,它只是将所有元素转入一个数组,对数组排序,再将序列复制回列表
//集合类库中使用的排序算法比快速排序要慢一点
//如果提供的列表没有实现RandomAccess接口,shuffle方法会将元素复制到数组,打乱数组再复制回来
Collections.shuffle(linkedList);
ArrayList<String> arrayList = new ArrayList<>();
int index = Collections.binarySearch(arrayList, "", Comparator.comparingInt(String::length));
//如果返回负值,表示没有匹配的元素,但可以利用返回值计算应该插入的位置
int insertionPoint = -index - 1;
//只有采用随机访问,二分查找才有意义,如果提供一个链表,将自动退化为线性查找
Collections.replaceAll(arrayList, "", "");
arrayList.removeIf(w -> w.length() <= 3);
arrayList.replaceAll(String::toLowerCase);
Collections.copy(arrayList, linkedList);
Collections.fill(arrayList, "");
Collections.addAll(linkedList, arrayList.toArray(String[]::new));
Collections.indexOfSubList(linkedList, arrayList);
Collections.lastIndexOfSubList(linkedList, arrayList);
//Collections.swap(arrayList, 0, 1);
Collections.reverse(arrayList);
Collections.rotate(arrayList, 2);
Collections.frequency(arrayList, "返回与o相等的元素的个数");
boolean disjoint = Collections.disjoint(arrayList, linkedList); //两个集合没有共同的元素,则返回true
/*批处理*/
arrayList.removeAll(linkedList);
linkedList.retainAll(arrayList);
//arrayList.addAll(linkedList.subList(0, 10));
//arrayList.subList(0, 10).clear();
/*集合与数组的转换*/
String[] values = {"1"};
HashSet<String> staff = new HashSet<>(List.of(values));
String[] staffArray = staff.toArray(new String[0]);
//如果编写自己的算法,应该尽可能使用接口,而不要使用具体的实现
//例如使用Collection接口代替ArrayList,甚至可以做的更好,接受一个Iterable,它有一个抽象方法iterator
// 增强for循环底层就使用了它,Collection接口扩展了Iterable
//返回类型也是同理
}
public static void leftCollection() {
/*【遗留的集合】P419*/
//经典的Hashtable类与HashMap类作用一样,与Vector类一样,它也是同步的
//现在应使用HashMap,如果需要并发访问,使用ConcurrentHashMap
/*枚举*/
//遗留的集合使用Enumeration接口遍历元素序列,这个接口有两个方法 hasMoreElements 和 nextElement
//完全类似于 Iterator 接口的 hasNext 和 next
//如果发现遗留的类实现了这个接口,可以使用Collections.list将元素收集到一个ArrayList中
ArrayList<String> loggerNames = Collections.list(LogManager.getLogManager().getLoggerNames());
//在Java 9中,可以把一个枚举转换为一个迭代器
LogManager.getLogManager().getLoggerNames().asIterator().forEachRemaining(n -> System.out.println(n));
//如果遗留的方法希望得到枚举参数 Collections.enumeration
List<InputStream> streams = new ArrayList<>();
new SequenceInputStream(Collections.enumeration(streams));
//在C++中,用迭代器作为参数十分普遍,在Java平台,传递集合比传递迭代器更好
/*属性映射*/
//属性映射(property map)是一个特殊的映射结构:键值都是字符串;可以很容易保存到文件和从文件加载;有一个二级表存放默认值
Properties settings = new Properties();
settings.setProperty("width", "600.0");
settings.setProperty("filename", "/var/log/hello.html");
try {
//可以用store方法将属性映射保存到一个文件中
FileOutputStream outputStream = new FileOutputStream("program.properties");
settings.store(outputStream, "Program Properties"); //第二个参数是包含在文件中的注释
//从文件加载可以用load
FileInputStream inputStream = new FileInputStream("program.properties");
settings.load(inputStream);
//在Java 9之前,属性文件使用7位ASCII编码,如今则使用UTF-8
} catch (IOException e) {
throw new RuntimeException(e);
}
//System.getProperty 方法会生成 Properties 对象描述的系统信息,例如获取主目录
String userDir = System.getProperty("user.home");
System.out.println(userDir);
//由于历史原因,Properties类实现了Map<Object, Object>,但你不应该使用get和put方法,最好用处理字符串而不是对象的getProperty
System.out.println(System.getProperty("java.version"));
System.out.println(System.getProperty("os.name"));
System.out.println(System.getProperty("os.version"));
//查找一个值时可以指定一个默认值
settings.getProperty("file", "");
//可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射
Properties defaultSettings = new Properties();
defaultSettings.setProperty("width", "600");
defaultSettings.setProperty("height", "400");
Properties settings2 = new Properties(defaultSettings);
//属性是没有层次结构的简单表格,通常会用类似window.main.color等引入一个假想的层次结构
//如果要存储复杂的配置信息,应该改为使用 Preferences 类
/*栈*/
//从1.0开始标准类库就包含了Stack类,但它扩展了Vector类,从理论角度看,Vector类并不太令人满意,你可以使用并非栈操作的insert和remove
/*位集*/
//BitSet类存储一个位序列,它不是数学上的集,称为位数组可能更合适
//如果需要高效存储位序列,就可以使用位集,它将位包装在字节里,所以比使用Boolean对象的ArrayList高效得多
BitSet bitSet = new BitSet();
bitSet.set(0);
bitSet.get(0);
bitSet.clear(0);
bitSet.and(new BitSet()); //与
bitSet.or(new BitSet()); //或
bitSet.xor(new BitSet()); //异或
bitSet.andNot(new BitSet()); //对应另一个位集中设置为1的位,将相应的位清除为0
}
}
并发
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;
public class Chapter12 {
public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {
//并发执行的进程数目并不受限于CPU数目,操作系统会为每个进程分配CPU时间片,给人并行处理的感觉
//多线程程序在更低一层扩展了多任务的概念:单个程序看起来在同时完成多个任务,每个任务在一个线程(thread)中执行
//[线程是控制线程的简称],如果程序可以同时运行多个线程,那么它就是多线程的(multithreaded)
//多进程 多线程:本质区别在每个进程都拥有自己的一整套变量,而线程则共享数据
//与进程相比,线程更“轻量”,创建、撤销一个线程比启动新进程开销小得多
//可以建立一个Thread类的子类来定义线程,但不推荐这样做,应当把要并行运行的任务与运行机制解耦
Runnable runnable = () -> System.out.println("thread!");
new Thread(runnable).start();
//如果有多个任务,为每个任务分别创建一个单独的线程开销会太大,可以使用一个线程池
//【警告】不要调用Thread或Runnable对象的run方法,这只会在同一个线程中执行这个任务,而没有启动新的线程
ThreadState();
ThreadAttribute();
Synchronize();
ThreadSafetCollection();
TaskAndThreadPool();
AsyncCompute();
Process();
}
public static void ThreadState() {
/*【线程状态】P555*/
//线程可以有6个状态:
//New (新建)
//Runnable (可运行)
//Blocked (阻塞)
//Waiting (等待)
//Timed waiting (计时等待)
//Terminated (终止)
Thread thread = new Thread(() -> {
System.out.println("Thread!");
});
System.out.println(thread.getState());
/*新建线程*/
//new一个线程时,处于新建状态
/*可运行线程*/
//一旦调用start方法,线程就处于可运行状态
//一个Runnable线程可能正在运行也可能没有运行,要由操作系统为线程提供具体的运行时间
// (不过Java规范没有将正在运行作为一个单独的状态,一个正在运行的线程仍然处于可运行状态)
thread.start();
System.out.println(thread.getState());
//一旦一个线程开始运行,它不一定始终保持运行,线程调度的细节依赖于操作系统提供的服务
//抢占式调度系统给每一个可运行线程一个时间片来执行任务,当时间片用完时,操作系统剥夺该线程的运行权,并给另一个线程一个机会来运行
//选择下一个线程时,系统会考虑线程的优先级
//现在所有的桌面以及服务器操作系统都使用抢占式调度,但像手机这样的小型设备可能使用协作式调度
//在这样的设备中,一个线程只有在调用yield方法,或被阻塞或等待时才失去控制权
Thread.yield(); //注意这是一个静态方法,使一个正在执行的线程向另一个线程交出控制权
//在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行
/*阻塞和等待线程*/
//线程处于阻塞或等待时,它暂时是不活动的,消耗最少的资源,要由线程调度器重新激活这个线程,具体细节取决于它是怎样到达非活动状态的
// · 当线程试图获取一个内部的对象锁,而这个锁目前被其他线程占用,就会被阻塞;当所有其他线程都释放了这个锁,且线程调度器允许该线程持有这个锁时,它将变成非阻塞状态
// · 当线程等待另一个线程通知调度器出现一个条件时,就会进入等待状态;调用Object.wait或Thread.join,或是等待java.util.concurrent中的Lock或Condition时,就会出现这种情况
// (实际上,阻塞与等待状态没有太大区别)
// · 有几个方法有超时参数,调用它们会让线程进入计时等待(timed waiting)状态;它将保持到超时期满或者接收到适当的通知
// 带有超时参数的方法有:Thread.sleep() 和计时版的 Object.wait Thread.join Lock.tryLock Condition.await
//当一个线程被重新激活,调度器检查它是否具有比当前运行线程更高的优先级,如果这样,调度器会剥夺某个当前运行线程的运行权,选择一个新线程运行
/*终止线程*/
// · run方法正常退出,线程自然终止
// · 因为一个没有捕获的异常终止了run方法,线程意外终止
}
public static void ThreadAttribute() throws InterruptedException {
/*【线程属性】P558*/
/*中断线程*/
//在Java早期版本中,线程有一个stop方法,其他线程可以调用这个方法来终止一个线程,但它已经被废弃了
//除了stop,没有办法可以强制线程终止,不过可以用interrupt方法请求终止一个线程
new Thread().interrupt();
//interrupt方法会设置线程的中断状态,这是每个线程都有的boolean标志,每个线程都应该不时地检查以判断线程是否被中断
//想知道是否设置了中断标志,首先调用静态Thread.currentThread()获得当前线程,然后调用isInterrupted
boolean interrupted = Thread.currentThread().isInterrupted();
System.out.println(interrupted);
//但如果线程被阻塞,就无法检查中断状态
//当在一个被sleep或wait调用阻塞的线程上调用interrupt方法时,阻塞调用将被一个InterruptedException异常中断
// 有一些阻塞I/O调用不能被中断,对此应该考虑选择可中断的调用
//没有任何语言要求被中断的线程应当终止。中断一个线程只是要引起它的注意,被中断的线程可以决定如何响应中断
//某些线程非常重要,所以应该处理这个异常,然后再继续执行
//但更普遍的情况是,线程只希望将中断解释为一个终止请求,这种线程的run方法有如下形式
Runnable r = () -> {
try {
while (!Thread.currentThread().isInterrupted()) {
//do more work
//只在某些情况下进入sleep
Thread.sleep(1);
}
} catch (InterruptedException e) {
//thread was interrupted during sleep or wait
} finally {
//cleanup, if required
}
};
//如果在每次工作迭代之后都调用sleep方法或其他可中断方法,isInterrupted检查既没有必要也没有用处
//如果设置了中断状态,调用sleep方法它不会休眠,它会清除中断状态并抛出InterruptedException
//如果你的循环调用了sleep,不要检测中断状态,应当捕获异常
r = () -> {
try {
while (true) {
//do more work
Thread.sleep(1);
}
} catch (InterruptedException e) {
//thread was interrupted during sleep
} finally {
//cleanup, if required
}
};
//有两个相似的方法
boolean b = Thread.interrupted();
System.out.println(b);
//interrupted是一个静态方法,它检查当前线程是否被中断,且它会清除该线程的中断状态
Thread.currentThread().isInterrupted();
//isInterrupted是实例方法,调用这个方法不会改变中断状态
//不要在底层抑制InterruptedException,如果想不出在catch中可以做什么,可以在catch中设置中断状态,这样调用者就可以检查中断状态
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
//更好的选择是用 throws InterruptedException 标记你的方法,去掉try,这样调用者或最终的run方法就可以捕获这个异常
Thread.sleep(1);
/*守护线程*/
//可以调用setDaemon将一个线程转换为守护线程(daemon thread)
new Thread().setDaemon(true);
//守护线程唯一的用途是为其他线程提供服务,清理过时缓存的线程也是守护线程
//当只剩下守护线程时,就没必要继续运行程序了,虚拟机就会退出
/*线程名*/
new Thread().setName("TheName");
//默认情况下,线程有类似Thread-2的名字,可以给它改名,这在线程转储时可能很有用
/*未捕获异常的处理器*/
//线程的run方法不能抛出任何检查型异常,但是,非检查型异常可能会导致线程终止,在这种情况下,线程会死亡
//不过,对于可以传播的异常,并没有任何catch子句,实际上,在线程死亡之前,异常会传递到一个用于处理未捕获异常的处理器
//这个处理器必须实现Thread.UncaughtExceptionHandler接口
var uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t + ": " + e.getMessage());
}
};
//可以为任何线程安装一个处理器
new Thread().setUncaughtExceptionHandler(uncaughtExceptionHandler);
//也可以用静态方法为所有线程安装一个默认的处理器
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
//代替处理器可以将未捕获异常的报告发送到日志,如果没有安装默认处理器,默认处理器则为null
//但是,如果没有为单个线程安装处理器,那么处理器就是该线程的ThreadGroup对象
//它实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法执行以下操作:
// · 如果该线程组有父线程组,那么调用父线程组的uncaughtException方法
// · 否则,如果 Thread.getDefaultUncaughtExceptionHandler() 返回一个非null处理器,则调用该处理器
// · 否则,如果Throwable是ThreadDeath的一个实例,什么都不做(ThreadDeath对象由已经废弃的stop方法产生)
// · 否则,将线程的名字以及Throwable的栈轨迹输出到System.err
/*线程优先级【废弃】*/
//每个线程都有一个优先级,一个线程默认会继承构造它的那个线程的优先级
//可以用setPriority提高或降低任何一个线程的优先级
new Thread().setPriority(Thread.MIN_PRIORITY);
new Thread().setPriority(Thread.NORM_PRIORITY);
new Thread().setPriority(Thread.MAX_PRIORITY);
//可以设置MIN_PRIORITY到MAX_PRIORITY之间的任何值
//当线程调度器有机会选择新线程时,优先选择高优先级的线程
//但是,线程优先级高度依赖于系统,虚拟机依赖于宿主机平台的线程实现时,Java线程的优先级会映射到宿主机平台的优先级
//例如,Windows有7个优先级别,Java的一些优先级会映射到同一个操作系统优先级
//在Oracle为Linux提供的Java虚拟机中,会完全忽略线程优先级
//在没有使用操作系统线程的Java早期版本中,线程优先级可能很有用,不过现在不要再使用它了
}
public static void Synchronize() {
/*【同步】P563*/
//如果两个线程存取同一个对象,可能导致对象被破坏,这种情况通常称为竞态条件(race condition)
//例如i++,问题在于这不是原子操作,这个指令可能如下处理:1.将i加载到寄存器 2.增加i 3.将结果写回i
//如果有一个线程执行步骤1、2,然后它的运行权被抢占,第二个线程唤醒,更新同一个元素,然后第一个线程又被唤醒,完成第3步
//这个动作会抹去第二个线程所做的更新
/*锁对象*/
//有两种机制可防止并发访问代码块
//Java提供了一个synchronized关键字来达到这一目的
//另外Java5引入了ReentrantLock类
//synchronized关键字会自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这种机制功能很强大,也很便利
//java.util.concurrent 框架为这些基础机制提供了单独的类,用ReentrantLock保护代码块的基本结构如下:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
//critical section
} finally {
lock.unlock();
}
//这个结构确保任何时刻只有一个线程进入临界区,一旦一个线程锁定了锁对象,其他任何线程都无法通过lock语句
//当其他线程调用lock时,它们会暂停,直到第一个线程释放这个锁对象
//使用锁时,不能使用try-with-resources语句,首先解锁的方法名不是close,其次try-with-resources首部希望声明一个新变量
//但如果使用锁,你应该希望多个线程共享那个变量
//这个锁称为重入(reentrant)锁,因为线程可以反复获得已拥有的锁,锁有一个持有计数(hold count)来跟踪对lock方法的嵌套调用
//线程每一次调用lock后都要调用unlock来释放锁,由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法
/*条件对象*/
//通常,线程进入临界区后却发现只有满足了某个条件之后它才能执行
//可以用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程(由于历史原因,条件对象常被称为条件变量conditional variable)
//使用if测试条件时,可能出现线程在通过这个测试后被中断,必须确保在检查条件和执行工作之间没有其他线程修改
//例如
/*lock.lock();
try {
while (account[from] < amount) {
//wait
}
//transfer
} finally {
lock.unlock();
}*/
//当账户中没有足够资金时,我们要等待,直到另一个线程向账户中增加了资金,但这个线程刚刚获得了锁,因此别的线程没有存款的机会
//这里就要引入条件对象
//一个锁对象可以有一个或多个相关联的条件对象,你可以用newCondition方法获得一个条件对象
//习惯上会给每个条件对象一个合适的名字反映它表示的条件
class Bank {
private Condition sufficientFunds;
public Bank() {
sufficientFunds = lock.newCondition();
}
}
//这样,如果发现余额不足,应调用 sufficientFunds.await();
//当前线程将暂停,并放弃锁
//等待获得锁的线程和已经调用了await方法的线程存在本质上的不同
//[一旦一个线程调用了await方法,它就进入了这个条件的等待集(wait set)]
//当锁可用时,该线程并不会变为可运行状态,实际上,它仍保持非活动状态,直到另一个线程在同一条件上调用 signalAll 方法
//当另一个线程完成转账时,应该调用 sufficientFunds.signalAll();
//这个调用会重新激活等待这个条件的所有线程
//当这些线程从等待集中移出时,它们再次成为可运行的线程,调度器最终将再次将它们激活
//同时,它们会尝试重新进入该对象,一旦锁可用,它们中的某个线程将从 await 调用返回,得到这个锁,并从之前暂停的地方继续执行
//!此时线程应当再次测试条件,通常,await调用应该放在如下形式的循环中:
/*while (!(OK to proceed))
condition.await();*/
//当一个线程调用await时,它没有办法重新自行激活,如果没有其他线程来重新激活等待的线程,就会发生死锁(deadlock)
//应该什么时候调用 signalAll 呢,从经验上讲,只要一个对象的状态有变化,可能有利于等待的线程,就可以调用
/*lock.lock();
try {
while (account[from] < amount) {
sufficientFunds.await();
}
//transfer
sufficientFunds.signalAll();
} finally {
lock.unlock();
}*/
//注意 signalAll() 不会立即激活一个等待的线程,它只是解除等待线程的阻塞,使这些线程可以在当前线程释放锁之后竞争访问对象
//另一个 signal() 方法只是随机选择等待集中的一个线程,解除它的阻塞状态,这比解除所有线程的阻塞更高效,但也存在危险
//!只有当线程拥有一个条件的锁时,它才能在这个条件上调用await、signalAll、signal
/*synchronized 关键字*/
//Lock和Condition接口运行程序员充分控制锁定,不过Java内置一种简便的机制
//Java中的每个对象都有一个内部锁,如果一个方法声明时有 synchronized 关键字,那么对象的锁将保护整个方法
/*public synchronized void method() {
}*/
//等价于
/*public void method() {
this.intrinsicLock.lock();
try {
} finally { this.intrinsicLock.unlock(); }
}*/
//内部对象锁只有一个关联条件,调用 wait() 和 notifyAll(),将一个线程增加到等待集和解除等待线程阻塞
//wait和notifyAll以及notify是 Object 类的 final 方法;Condition方法必须命名为 await、signalAll、signal,从而不会冲突
//将静态方法声明为同步也是合法的,它会获得相关类对象的内部锁
//如果Bank类有一个静态同步方法,调用时,Bank.class对象的锁会锁定,因此,没有其他线程可以调用这个类的该方法或任何其他同步静态方法
//内部锁和条件存在一些限制:
// · 不能中断一个正在尝试获得锁的线程
// · 不能指定尝试获得锁时的超时时间
// · 每个锁仅有一个条件可能是不够的
//建议:
// · 最好既不用Lock/Condition也不用synchronized关键字,在许多情况下,可以使用 java.util.concurrent 包中的某种机制
// 它会为你处理所有的锁定,例如使用阻塞队列来同步完成一个共同任务的线程,还应当研究并行流
// · 如果synchronized关键字适合你的程序,那么尽量使用这种做法,减少编写的代码量也减少出错的概率
// · 如果特别需要Lock/Condition结构提供的额外能力,则使用Lock/Condition
/*同步块*/
//每个Java对象都有一个锁,线程可以通过调用同步方法获得锁
//还有另一种机制可以获得锁:进入一个同步块
//synchronized (obj) { } //它会获得obj的锁
//有时我们会发现一些“专用”锁
Object oLock = new Object();
synchronized (oLock) {
//do something
}
//在这里,创建oLock对象只是为了使用每个Java对象拥有的锁
//有时程序员使用一个对象的锁来实现额外的原子操作,这种做法称为客户端锁定(client-side locking)
//例如,考虑Vector类,这是一个列表,它的方法是同步的
//假设将余额存在一个Vector<Double>中,它的get和set方法是同步的,但对我们并没有什么帮助,在get调用完成后,一个线程完全可能被抢占
//不过我们可以用同步块截获这个锁
/*public void transfer(int from, int to, double amount) {
synchronized (accounts) {
Double fromBalance = accounts.get(from);
Double toBalance = accounts.get(to);
accounts.set(from, fromBalance - amount);
accounts.set(to, toBalance + amount);
}
}*/
//这样是可行的,但完全依赖于这样一个事实:Vector类会对自己的所有更改方法使用内部锁,它的文档没有给出这样的承诺,你必须仔细研究源代码
//还得希望将来的版本不会引入非同步的更改方法,可以看到,客户端锁定是非常脆弱的,通常不推荐使用
/*监视器概念*/
//严格来讲,锁和条件不是面向对象的,研究员多年来一直在寻找不要求程序员考虑显式锁就可以确保多线程安全性的方法
//最成功的解决方案之一是监视器(monitor)
//监视器具有如下特性:
// · 监视器是只包含私有字段的类
// · 监视器类的每个对象都有一个关联的锁
// · 所有方法由这个锁锁定,如果客户端调用obj.method(),那么obj对象的锁在方法调用开始时自动获得,方法返回时自动释放
// 因为所有字段是私有的,这样的安排可以确保一个线程处理字段时,没有其他线程能访问这些字段
// · 锁可以有任意多个相关联的条件
//Java设计者以不太严格的方式采用了监视器概念,Java中每一个对象都有一个内部锁和一个内部条件
//如果一个方法用synchronized关键字声明,那么,它表现得就像是一个监视器方法,可以通过调用wait/notify/notifyAll来访问条件变量
//不过Java对象在以下3个重要方面不同于监视器,削弱了线程的安全性
// · 字段不要求是private
// · 方法不要求是synchronized
// · 内部锁对客户是可用的
/*volatile字段*/
//有时如果只是为了读写一两个实例字段而使用同步,所带来的开销好像有些划不来
//不过,使用现代的处理器与编译器,出错的可能性很大
// · 有多处理器的计算机能够暂时在寄存器或本地内存缓存中保存内存值,结果是,运行在不同处理器上的线程可能看到同一个内存位置不同的值
// · 编译器可以改变指令执行顺序以使吞吐量最大化,编译器不会选择可能改变代码语义的顺序,但编译器有一个假定:
// 认为内存值只在代码中有显式的修改指令时才会改变,然而,内存值可能被另一个线程改变
//如果用锁来保护可能被多个线程访问的代码,那么不存在这种问题
//编译器被要求在必要的时候刷新本地缓存来支持锁,而且不能不相应地重新排列指令顺序
//[同步格言]:如果写一个变量,而这个变量接下来可能会被另一个线程读取,或者,如果读一个变量,而这个变量可能已经被另一个线程写入值,那么必须使用同步
//volatile关键字为示例字段的同步访问提供了一种免锁机制
//如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新
//编译器会插入适当的代码,以确保如果一个线程对done做了修改,这个修改对读取这个变量的所有其他线程都可见
//!volatile变量不能提供原子性,例如方法 public void flipDown() { done = !done; } 不能保证读取、翻转和写入不被中断
/*final变量*/
//除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个字段
//还有一种情况可以安全地访问一个共享字段,即这个字段声明为final
//final var accounts = new HashMap<String, Double>();
//其他线程会在构造器完成构造之后才看到这个变量,如果不用final,就不能保证其他线程看到的是更新后的值,可能都只是看到null
//当然,对这个映射的操作并不是线程安全的,如果有多个线程更改和读取这个映射,仍然需要进行同步
/*原子性*/
//假设对共享变量除了赋值之外并不做其他操作,那么可以将它们声明为volatile
//java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而没有使用锁),来保证其他操作的原子性
AtomicInteger num = new AtomicInteger();
num.incrementAndGet();
num.decrementAndGet();
//它们分别以原子方式将一个整数进行自增或自减
//如果希望完成更复杂的更新,就必须使用compareAndSet方法
//例如,下面的代码是不可行的
num.set(Math.max(num.get(), 10));
//这个更新不是原子的,我们可以提供一个lambda表达式更新变量,它会为你完成更新
//可以这样调用
num.updateAndGet(x -> Math.max(x, 10));
//或者
num.accumulateAndGet(10, Math::max); //accumulateAndGet利用一个二元操作符来合并原子值和提供的参数
//还有getAndUpdate、getAndAccumulate方法可以返回原值
//类AtomicInteger AtomicIntegerArray AtomicIntegerFieldUpdater AtomicLong AtomicReference等也提供了这些方法
//如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试
//LongAdder 和 LongAccumulator 类解决了这个问题
//LongAdder包括多个变量(加数),其总和为当前值,可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数
//对于只有当所有工作都完成后才需要总和的情况,这种方法会很高效,性能会有显著的提升
//如果预期存在大量竞争,只需要使用LongAdder而不是AtomicLong
LongAdder longAdder = new LongAdder();
longAdder.increment();
//LongAccumulator将这种思想推广到任意的累加操作,在构造器中,可以提供这个操作以及它的零元素
LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);
longAccumulator.accumulate(10);
//要加入新的值,调用accumulate
//在内部,这个累加器包含变量a1、a2、...、an,每个变量初始化为零元素
//调用accumulate并提供值时,其中一个变量会以原子方式更新
//如果选择一个不同的操作,就可以计算最小值或最大值,一般来说,这个操作必须满足结合律和交换律;这说明,最终结果必须不依赖于以什么顺序结合这些中间值
new DoubleAdder();
new DoubleAccumulator(Double::sum, 0.0);
//也采用同样的方式,只不过处理的是double值
/*死锁*/
//每一个线程都在等待条件而导致所有线程都被阻塞,这样的状态称为死锁(deadlock)
//Java中没有任何东西可以避免或打破死锁,必须仔细设计程序,确保不会出现死锁
/*线程局部变量*/
//有时要避免在线程间共享变量
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
new Thread(() -> System.out.println(dateFormat.format(new Date()))).start();
new Thread(() -> System.out.println(dateFormat.format(new Date()))).start();
//结果可能很混乱,因为dateFormat使用的内部数据结构可能会被并发的访问破坏
//当然可以使用同步,但开销很大,也可以构造一个局部的对象,不过这样也很浪费
//要为每个线程构造一个实例,可以使用以下代码
//public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
//要访问具体的格式化方法,可以调用
df.get().format(new Date());
//在一个给定线程中首次调用get时,会调用构造器中的lambda表达式,在此之后,get方法会返回属于当前线程的那个实例
//在多个线程中生成随机数也存在类似问题,java.util.Random类是线程安全的,但是如果多个线程需要等待一个共享的随机数生成器,会很低效
//可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器
//不过Java 7还另外提供了一个便利类,只需要如下调用
int random = ThreadLocalRandom.current().nextInt();
//ThreadLocalRandom.current() 会返回特定于当前线程的Random类的实例
/*为什么废弃stop和suspend方法*/
//最初的Java版本定义了一个stop方法来终止一个线程,一个suspend方法来阻塞一个线程直到另一个线程调用resume
//这两个方法有一些共同点:都试图控制一个给定线程的行为,而没有线程的互操作
//这些方法已经被废弃,stop方法天生就不安全,经验证明suspend方法经常会导致死锁
//stop会终止所有未结束的方法,包括run;当线程被终止,它会立即释放被它锁定的所有对象的锁,这会导致对象处于不一致的状态
//例如一个转账操作被终止,钱已经被取出,但还没有存入,银行对象就被破坏了
//当一个线程要终止另一个线程时,它无法知道什么时候调用stop是安全的,因此,该方法已经被废弃
//希望停止一个线程的时候应该中断该线程,被中断的线程可以在安全的时候终止
//如果用suspend挂起一个持有锁的线程,那么,在线程恢复之前这个锁是不可用的
//如果想安全地挂起线程,可以引入一个变量suspendRequested,并在run方法的某个安全的地方测试它
}
private volatile boolean done;
public boolean isDone() {
return done;
}
public void setDone() {
done = true;
}
public synchronized void method() throws InterruptedException {
while (!"".equals("")) wait();
notifyAll();
}
public static void ThreadSafetCollection() throws InterruptedException {
/*【线程安全的集合】P589*/
//如果多个线程要并发修改一个数据结构,如散列表,很容易破坏这个数据结构,可以通过提供锁来保护共享的数据结构,但是选择线程安全的实现可能更为容易
//下面是一些Java类库提供的线程安全的集合
/*阻塞队列*/
//很多线程问题可以用一个或多个队列以优雅而安全的方式来描述
//生产者线程向队列插入元素,消费者线程则获取元素
//使用队列,可以安全地从一个线程向另一个线程传递数据
//例如一个线程插入指令,另一个线程取出指令(当然,线程安全的队列类的实现者必须考虑锁和条件)
//当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列(blocking queue)将导致线程阻塞
//工作线程可以周期性地将中间结果存储在阻塞队列,如果第一组线程运行得比第二组慢,第二组在等待结果时会阻塞;如果第一组更快,队列会填满,直到第二组赶上来
//队列会自动平衡负载
//阻塞队列方法:
//add 如果队列满,抛出异常
//element 队头元素 如果队列空,抛出异常
//remove 如果队列空,抛出异常
//offer 如果队列满,返回false
//peek 如果队列空,返回null
//poll 如果队列空,返回null
//put() 如果队列满,阻塞
//take() 如果队列空,阻塞
//还有带有超时时间的offer方法和poll方法
//queue.offer(x, 100, TimeUnit.MILLISECONDS);
//queue.poll(100, TimeUnit.MILLISECONDS);
//java.util.concurrent包提供了阻塞队列的几个变体,默认情况下,LinkedBlockingQueue的容量没有上界,但也可以指定一个最大容量
//LinkedBlockingDeque是一个双端队列
LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
LinkedBlockingDeque<String> linkedBlockingDeque = new LinkedBlockingDeque<>();
//ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性,若设置了公平参数,那么等待了最长时间的线程会优先得到处理
//通常,公平性会降低性能,只有在确实非常需要时才使用公平参数
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(10, true);
//PriorityBlockingQueue是一个优先队列,而不是先进先出队列,元素按照它们的优先级顺序移除,这个队列没有容量上限,但如果队列是空的,获取元素的操作会阻塞
PriorityBlockingQueue<String> priorityBlockingQueue = new PriorityBlockingQueue<>();
//DelayQueue包含实现了Delayed接口的对象
DelayQueue<Delayed> delayedDelayQueue = new DelayQueue<>();
//getDelay方法返回对象的剩余延迟,负值表示延迟已结束,元素只有在延迟结束的情况下才能从DelayQueue中移除
//还需要实现compareTo方法,DelayQueue使用该方法对元素排序
//Java7增加了一个TransferQueue接口,允许生产者线程等待,直到消费者准备就绪可以接收元素
LinkedTransferQueue<Object> linkedTransferQueue = new LinkedTransferQueue<>();
new Thread(() -> {
try {
linkedTransferQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
//如果生产者调用
linkedTransferQueue.transfer(new Object());
//这个调用会阻塞,直到另一个线程将元素(item)删除
/*高效的映射、集和队列*/
//java.util.concurrent包提供了映射、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue
new ConcurrentHashMap<>();
new ConcurrentSkipListMap<>();
new ConcurrentSkipListSet<>();
new ConcurrentLinkedQueue<>();
new ConcurrentLinkedDeque<>();
//这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分尽可能减少竞争
//与大多数集合不同,这些类的size方法不一定在常量时间内完成,确定这些集合的当前大小通常需要遍历
//有些应用使用庞大的并发散列映射,以至于无法用size方法得到它的大小,因为这个方法只能返回int,mappingCount方法可以把大小作为long返回
new ConcurrentHashMap<>().mappingCount();
//集合返回弱一致性(weakly consistent)的迭代器,这意味着迭代器不一定能反映出它们构造之后的所有更改
//但是,它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException
//注:与之对应的是,java.util包中的集合如果在迭代器构造之后发生改变,集合的迭代器将抛出一个ConcurrentModificationException
//有些应用使用的散列函数不太好,以至于所有条目最后都放在散列映射很少的桶中,这会严重降低性能,即使是还算合理的散列函数,也可能存在问题
//攻击者可以制造大量能得出相同散列值的字符串,让程序速度减慢
//在较新的Java版本中,并发散列映射将桶组织为树,而不是列表,键类型实现Comparable,从而可以保证性能为O(log(n))
/*映射条目的原子更新*/
//ConcurrentHashMap原来的版本只有为数不多的方法可以实现原子更新,这使得编程有些麻烦
//如果多线程修改一个普通的HashMap,它们可能会破坏内部结构,有些链接可能丢失,或者甚至会构成循环
//对于ConcurrentHashMap不会发生这种情况,get和put代码不会破坏数据结构,不过,由于操作序列不是原子的,所以结果不可预知
//在老版本Java中,必须使用replace操作,它会以原子方式用一个新值替换原值,前提是之前没有其他线程把原值替换为其他值
//必须一直这么做,直到替换成功
/*do {
oldValue = map.get(work);
newValue = oldValue == null ? 1 : oldValue + 1;
} while (!map.replace(word, oldValue, newValue));*/
//或者可以使用一个
//ConcurrentHashMap<String, AtomicLong> map = new ConcurrentHashMap<>();
/*map.putIfAbsent(word, new AtomicLong());
map.get(word).incrementAndGet();*/
//很遗憾,这会为每个自增构造一个新的AtomicLong,而不管是否需要
//如今,Java API提供了一些新方法,可以更方便地完成源自更新
//调用compute方法时可以提供一个键和一个计算新值的函数,这个函数接受键和相关联的值(如果没有值,则为null),它会计算新值
//map.compute(word, (k, v) -> v == null ? 1 : v + 1);
//ConcurrentHashMap中不允许有null值,很多方法都使用null来指示映射中某个给定的键不存在
//另外还有
//new ConcurrentHashMap<>().computeIfAbsent()
//new ConcurrentHashMap<>().computeIfPresent()
//它们分别只在已经有原值的情况下计算新值,或者只在没有原值的情况下计算新值
//map.computeIfAbsent(word, k -> new AtomicLong()).incrementAndGet();
//首次增加一个键时通常需要做些特殊处理,利用merge方法可以非常方便地做到这一点
//这个方法有一个参数表示键不存在时使用的初始值,否则就调用你提供的函数来结合原值与初始值(与compute不同,这个函数不处理键)
//map.merge(word, 1L, Long::sum);
//如果传入compute或merge的函数返回null,将从映射中删除现有的条目
//!使用compute或merge时,你提供的函数不能做太多工作,这个函数运行时,可能会阻塞对映射的其他更新
/*对并发散列映射的批操作*/
//Java API对并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全地执行
//批操作会遍历映射,处理遍历过程中找到的元素
//注意!这里不会冻结映射的当前快照,除非你恰好知道批操作运行时映射不会被改变,否则就要把结果看作是映射状态的一个近似
//有3种不同的操作:
// · search 为每个键或值应用一个函数,直到函数生成一个非null的结果,然后搜索终止,返回这个函数的结果
// · reduce 归约,组合所有键或值,这里要使用所提供的一个累加函数
// · forEach 为所有键或值应用一个函数
//每个操作都有4个版本:
// · operationKeys 处理键
// · operationValues 处理值
// · operation 处理键和值
// · operationEntries 处理Map.Entry对象
//对于上述操作,需要指定一个参数化阈值(parallelism threshold)
//如果映射包含的元素多余阈值,就会并行完成批操作,如果希望在一个线程中运行,可以使用 Long.MAX_VALUE
//如果希望使用尽可能多的线程,可以使用阈值 1
//找出第一个出现次数大于1000的单词
new ConcurrentHashMap<String, Long>().search(Long.MAX_VALUE, (k, v) -> v > 1000 ? k : null);
//会返回第一个匹配的键,如果搜索函数对所有输入都返回null,则返回null
//forEach方法有两种形式
//第一种只对各个映射条目应用一个消费者函数
new ConcurrentHashMap<String, Long>().forEach(1, (k, v) -> System.out.println(k + " -> " + v));
//第二种还有一个额外的转换器函数作为参数,要先应用这个函数,其结果会传递到消费者
new ConcurrentHashMap<String, Long>().forEach(1, (k, v) -> k + " -> " + v, System.out::println);
//转换器可以用作一个过滤器,只要转换器返回null,这个值就会被跳过
new ConcurrentHashMap<String, Long>().forEach(1, (k, v) -> v > 1000 ? v : null, System.out::println);
//reduce操作用一个累加函数组合其输入
new ConcurrentHashMap<String, Long>().reduceValues(1, Long::sum);
//与forEach类似,也可以提供一个转换器函数
new ConcurrentHashMap<String, Long>().reduceKeys(1, String::length, Integer::max);
//如果映射为空,或所有条目都被过滤掉,reduce会返回null,如果只有一个元素,则返回其转换结果,不会应用累加器
//对于int、long和double输出还有相应的特殊化操作,有ToLong、ToInt、ToDouble后缀,需要把输入转换为一个基本类型值,并指定一个默认值和一个累加器函数
//映射为空时返回默认值
new ConcurrentHashMap<String, Long>().reduceValuesToLong(1, Long::longValue, 0, Long::sum);
//警告:这些特殊化版本与对象版本的操作有所不同,对象版本的操作只需要考虑一个元素;
// 这里不是返回转换得到的元素,而是要与默认值累加;因此,默认值必须是累加器的零元素
/*并发集视图*/
//假设你想要的是一个很大的线程安全的集而不是映射
//静态newKeySet方法会生成一个Set<K>,这实际上是ConcurrentHashMap<K, Boolean>的一个包装器
ConcurrentHashMap.KeySetView<String, Boolean> set = ConcurrentHashMap.<String>newKeySet();
//所有映射值都为Boolean.TRUE,不过因为只是要把它用作一个集,所以并不关心映射值
//当然,如果原来有一个映射,keySet方法可以生成这个映射的键集,可以从这个集中删除元素,不过向键集增加元素没有意义
//ConcurrentHashMap还有第二个keySet方法,它包含一个默认值,为集增加元素时可以使用这个方法
ConcurrentHashMap.KeySetView<String, Long> keySet = new ConcurrentHashMap<String, Long>().keySet(1L);
keySet.add("Java");
//如果"Java"在映射中不存在,现在它会有一个值 1
/*写数组的拷贝*/
//CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合
//其中所有更改器会建立底层数组的一个副本;如果迭代访问集合的线程数超过更改集合的线程数,这样的安排是很有用的
new CopyOnWriteArrayList<>();
//当构造一个迭代器的时候,它包含当前数组的一个引用;如果这个数组后来被更改了,迭代器仍然引用旧数组,但是,集合的数组已经替换
//因而,原来的迭代器可以访问一致的(但可能过时的)视图,而且不存在任何同步开销
/*并行数组算法*/
//Arrays类提供了大量并行化操作
//静态Arrays.parallelSort方法可以对一个基本类型值或对象的数组排序
Arrays.parallelSort(new int[1]);
//对对象排序时,可以提供一个Comparator
//对于所有方法都可以提供一个范围的边界
int[] ints = new int[10];
Arrays.parallelSort(ints, ints.length / 2, ints.length);
//parallelSetAll方法会用由一个函数计算得到的值填充一个数组
//这个函数接收元素索引,然后计算相应位置上的值
Arrays.parallelSetAll(ints, index -> index % 10);
//parallelPrefix方法用一个给定结合操作的相应前缀的累加结果替换各个数组元素
Arrays.parallelPrefix(new int[]{1, 2, 3, 4}, (x, y) -> x * y);
//{1, 2, 3, 4} 变为 {1, 1*2, 1*2*3, 1*2*3*4}
/*较早的线程安全集合*/
//从Java的初始版本开始,Vector和Hashtable类就提供了动态数组和散列表的线程安全的实现
//现在这些类已经过时,被ArrayList和HashMap取代,不过它们不是线程安全的
//实际上,集合库中提供了一种不同的机制,任何集合类都可以通过使用[同步包装器(synchronization wrapper)]变成线程安全的
List<Object> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<Object, Object> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
//结果集合的方法使用锁加以保护,可以提供线程安全的访问
//应该确保没有线程通过原始的非同步方法访问数据结果,最好是不保存原始对象的任何引用,构造一个集合并立即传递给包装器
//如果希望迭代访问一个集合,同时另一个线程仍有机会更改这个集合,那么仍然需要使用“客户端”锁定
synchronized (synchronizedMap) {
Iterator<Object> iterator = synchronizedMap.keySet().iterator();
while (iterator.hasNext()) continue;
}
//在迭代过程中,如果另一个线程更改了集合,迭代器会失效,抛出ConcurrentModificationException
//同步仍然是需要的,这样才能可靠地检测出并发修改
//通常最好使用java.util.concurrent包中定义的集合,而不是同步包装器
//特别是ConcurrentHashMap经过了精心实现,如果多个线程访问的是不同的桶,它们都能访问而不会相互阻塞
//需要经常更改的数组列表是一个例外,在这种情况下,同步的ArrayList要胜过CopyOnWriteArrayList
}
public static void TaskAndThreadPool() throws InterruptedException, ExecutionException {
/*【任务和线程池】P603*/
//构造一个新线程开销有点大,因为涉及到与操作系统的交互
//如果你的程序创建了大量生命期很短的线程,那么不应该把每个任务映射到一个单独的线程,而应该使用线程池(thread pool)
//线程池中包含许多准备运行的线程,为线程池提供一个Runnable,就会有一个线程调用run方法
//当run方法退出时,这个线程不会死亡,而是留在池中准备为下一个请求提供服务
/*Callable 与 Future*/
//Runnable封装一个异步运行的任务,可以把它想象成一个没有参数的返回值的异步方法
//Callable与它类似,但是有返回值
//Callable接口是一个参数化的类型,只有一个方法call,类型参数是返回值的类型
//Future保存异步计算的结果,可以启动一个计算,将Future对象交给某个线程,然后忘掉它
//这个Future对象的所有者在结果计算好后就可以获得结果
//Future<V>接口有以下的方法:get()、get(long timeout, TimeUnit unit)、cancel(boolean mayInterrupt)、isCancelled()、isDone()
//第一个get会阻塞,直到计算完成,第二个get超时会抛出一个TimeoutException
//如果运行该计算的线程被中断,两个方法都将抛出InterruptedException,如果计算已经完成,get立即返回
//如果计算还在进行,isDone返回false
//可以用cancel方法取消计算,如果计算还没开始,它会被取消且不再开始,如果计算正在进行,mayInterrupt参数为true,它就会被中断
//!如果一个Future对象不知道任务在哪个线程中执行,或者没有监视执行该任务的线程的中断状态,那么取消任务没有任何效果
//执行Callable的一种方法是使用FutureTask,它实现了Future和Runnable接口,所以可以构造一个线程来运行这个任务
Callable<Integer> task = () -> 1;
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
try {
Integer result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
//更常见的情况是,可以将一个Callable传递到一个执行器
/*执行器*/
//执行器(Executors)类有许多静态工厂方法,用来构造线程池
Executors.newCachedThreadPool(); // 必要时创建新线程,空闲线程保留60s
Executors.newFixedThreadPool(10); // 池中包含固定数目的线程,空闲线程会一直保留
Executors.newWorkStealingPool(); // 适合“fork-join”任务的线程池,复杂的任务会分解为更简单的任务,空闲线程会“密取”较简单的任务
Executors.newSingleThreadExecutor(); // 只有一个线程的“池”,会顺序执行所提交的任务
Executors.newScheduledThreadPool(1); // 用于调度执行的固定线程池
Executors.newSingleThreadScheduledExecutor(); // 用于调度执行的单线程“池”
//newCachedThreadPool构造一个线程池,会立即执行各个任务,如果没有可用空闲线程,则创建一个新线程
//newFixedThreadPool构造一个有固定大小的线程池,如果提交的任务数多余空闲线程数,就把它放到队列中
//这些方法返回实现了ExecutorService接口的ThreadPoolExecutor对象
//如果线程生存期很短,或者大量时间都在阻塞,那么可以使用一个缓存线程池
//不过,如果线程工作量很大且并不阻塞,你肯定不希望运行太多线程
//为了得到最优的运行速度,并发线程数等于处理器内核数
//这种情况下,就应使用固定线程池,即并发线程总数有一个上限
//单线程执行器对于性能分析很有帮助,临时用一个单线程池替换缓存或固定线程池,就能测量不使用并发的情况下应用的运行速度会慢多少
//Java EE提供了一个ManagedExecutorService子类,很适用于Java EE环境中的并发任务
//可用下面的方法之一将Runnable或Callable对象提交给ExecutorService
//Future<T> submit(Callable<T> task)
//Future<?> submit(Runnable task)
//Future<T> submit(Runnable task, T result)
//线程池会在方便的时候尽早执行提交的任务,调用submit时,会得到一个Future对象,可用于得到结果或取消任务
//第二个submit返回一个有些奇怪的Future<?>,可以用它调用isDone、cancel、isCancelled,但get方法在完成时只是返回null
//第三个submit的get方法在完成时返回指定的result对象
//使用完一个线程池后,调用shutdown
Executors.newCachedThreadPool().shutdown();
//这个方法启动线程池的关闭序列,被关闭的执行器不再接受新的任务,所有任务完成时,线程池中的线程死亡
Executors.newCachedThreadPool().shutdownNow();
//另一个方法时调用shutdownNow,线程池会取消所有尚未开始的任务
//1.调用Executors类的静态方法构造线程池
//2.调用submit提交Runnable或Callable对象
//3.保存好返回的Future对象,以便得到结果或取消任务
//4.当不想再提交任何任务时,调用shutdown
//ScheduledExecutorService接口对调度执行或重复执行任务提供了一些方法,这是对支持建立线程池的java.util.Timer的泛化
//可以调度Runnable或Callable在一个初始延迟后运行一次,也可以调度Runnable定期运行
/*控制任务组*/
//有时我们使用执行器有更策略性的原因,需要控制一组相关的任务
//Integer integer = Executors.newCachedThreadPool().invokeAny(new ArrayList<Callable<Integer>>()); //UncaughtExceptionHandler
//invokeAny方法提交一个Callable对象集合中的所有对象
//并且返回某个已完成任务的结果,不知道返回的究竟是哪个任务的结果,这往往是最快完成的那个任务
//对于搜索问题,如果我们愿意接受任何一种答案,就可以使用这个方法
//例如,假定需要对一个大整数进行因数分解,这是RSA解码时需要完成的一种计算;可以提交很多任务,每个任务尝试对不同范围内的数进行分解
//只要其中一个任务得到了答案,计算就可以停止了
//invokeAll方法提交一个Callable对象集合中的所有对象,这个方法会阻塞,直到所有任务都完成,并返回表示所有任务答案的一个Future对象列表
List<Future<Integer>> futures = Executors.newCachedThreadPool().invokeAll(new ArrayList<Callable<Integer>>());
//将一批任务提交给线程池后,虽然任务都是异步的,但Future的get是阻塞的,遍历得到的Future不够高效,应该按任务完成的顺序得到结果
//可以利用ExecutorCompletionService来管理
//使用一个执行器构造ExecutorCompletionService,将任务提交到这个服务,它会管理Future对象的一个阻塞队列,其中包含所提交任务的结果(一旦结果可用,就会放入队列)
ExecutorService executor = Executors.newCachedThreadPool();
ArrayList<Callable<Integer>> tasks = new ArrayList<>();
ExecutorCompletionService<Integer> service = new ExecutorCompletionService<>(executor);
for (Callable<Integer> t : tasks) service.submit(t);
for (int i = 0; i < tasks.size(); i++) {
service.take().get();
}
//take()如果没有可用的结果,阻塞;poll()如果没有可用的结果,返回null,或等待给定时间
//注意:一旦有任务返回,invokeAny方法就会终止,所以不能让任务返回boolean来指示失败,应该在失败时抛出一个异常
System.out.println(((ThreadPoolExecutor) executor).getLargestPoolSize());
/*fork-join框架*/
//有些应用使用了大量线程,但其中大多数都是空闲的
//例如Web服务器可能会为每个连接分别使用一个线程,另外图像或视频处理应用可能对每个处理器内核分别使用一个线程
//Java7中引入了fork-join框架,专门用于支持后一类应用
//假设有一个处理任务,它可以很自然地分解为子任务
/*if (problemSize < threshold)
* solve problem directly
* else {
* break problem into sub-problems
* recursively solve each sub-problems
* combine the results
* }*/
//假设想统计一个数组中有多少个元素满足某个特定属性,可以将这个数组一分为二,分别统计,再将结果相加
//要采用框架可用的一种方式完成这种递归计算,需要提供一个扩展 RecursiveAction 的类(如果不生成任何结果),再覆盖compute方法来生成并调用子任务,然后合并其结果
class Counter extends RecursiveTask<Integer> {
@Override
protected Integer compute() {
// if (to - from < THRESHOLD)
// solve problem directly
// else {
// int mid = (from + to) / 2;
// var first = new Counter(values, from, mid, filter);
// var second = new Counter(values, mid, to, filter);
// invokeAll(first, second);
// return first.join() + second.join();
// }
return null;
}
}
//在这里,invokeAll接收到很多任务并阻塞,直到所有任务全部完成,join方法将生成效果
//get也可以得到结果,不过一般不太使用,因为它可能抛出检查型异常,而在compute方法中不允许抛出这种异常
ForkJoinPool forkJoinPool = new ForkJoinPool();
Counter counter = new Counter();
forkJoinPool.invoke(counter);
System.out.println(counter.join());
//在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载
//这种方法称为工作密取(work stealing),每个工作线程都有一个双端队列(deque)来完成任务
//一个工作线程将子任务压入其双端队列的队头(只有一个线程可以访问队头,所以不需要加锁),一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务
//由于大的子任务都在队尾,这种密取很少出现
//警告!fork-join池时针对非阻塞工作负载优化的,如果向它增加很多阻塞任务,会让它无法有效工作;但可以让任务实现ForkJoinPool.ManagedBlocker接口来解决这个问题
}
public static void AsyncCompute() {
/*【异步计算】P615*/
//目前为止,我们的并发计算方法都是先分解一个任务,然后等待,直到所有部分完成
//不过等待并不总是个好主意,下面介绍如何实现无等待或异步的计算
/*可完成Future*/
//当有一个Future对象时,需要调用get获得值,这个方法会阻塞,直到值可用
//CompletableFuture类实现了Future接口,它提供了获得结果的另一种机制
//你需要注册一个回调,一旦结果可用,就会(在某个线程中)利用该结果调用这个回调
new CompletableFuture<Integer>().thenAccept(i -> System.out.println(i));
//有一些API方法返回CompletableFuture对象,例如,可以用试验性的HttpClient类异步获取一个网页
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://bing.cn")).GET().build();
CompletableFuture<HttpResponse<Object>> future = client.sendAsync(request, responseInfo -> null);
//不过大多数情况下,你都需要建立自己的CompletableFuture
//要想异步运行任务并得到CompletableFuture,不要把它直接提交给执行器服务,而应当调用静态方法CompletableFuture.supplyAsync
CompletableFuture<String> completableFuture = new CompletableFuture<>();
//completableFuture = readPage(URI.create("https://bing.cn").toURL(), Executors.newCachedThreadPool());
//如果省略执行器,任务会在一个默认执行器(ForkJoinPool.commonPool()返回的执行器)上运行,通常你可能不希望这样
//注意supplyAsync方法的第一个参数是Supplier<T>而不是Callable<T>,这两个接口都描述了无参数且返回值为T的函数,不过Supplier不能抛出检查型异常
//CompletableFuture可以采用两种方式完成:
//得到一个结果,或者有一个未捕获的异常
//要处理这两种情况,可以使用whenComplete方法,要对结果(没有为null)和异常(没有为null)提供函数
completableFuture.whenComplete((s, t) -> {
if (t == null) System.out.println("success: " + s);
else System.out.println(t.getMessage());
});
//CompletableFuture之所以被称为是可完成的,是因为你可以手动设置一个完成值
//(在其他并发库中,这样的对象称为承诺(promise))
//当然,用supplyAsync创建一个CompletableFuture时,任务完成时就会隐式地设置完成值
//不过,显式设置结果可以提供更大的灵活性
//例如,两个任务可以同时计算一个答案:
ExecutorService executor = Executors.newCachedThreadPool();
var f = new CompletableFuture<Integer>();
executor.execute(() -> {
int n = 1;
f.complete(n);
});
executor.execute(() -> {
int n = 1;
f.complete(n);
});
//要对一个异常完成future,需要调用
Throwable throwable = new RuntimeException();
f.completeExceptionally(throwable);
//可以在多个线程中在同一个future上安全地调用complete或completeExceptionally,如果这个future已经完成,这些调用没有任何作用
//isDone()指出一个Future对象是否已经完成(正常完成或产生一个异常)
//在上例中,如果结果已经给出,另一个线程中可以使用这个信息停止工作
//警告!与不同的Future不同,调用cancel时,CompletableFuture的计算不会中断,取消只会把这个Future对象设置为以异常方法完成(有一个CancellationException)
//一般来讲,这是有道理的,因为CompletableFuture可能没有一个线程负责它的完成
//不过,这个限制也适用于supplyAsync等方法返回的CompletableFuture实例,而这些对象原则上讲是可以中断的
executor.shutdownNow();
/*组合可完成Future*/
//非阻塞调用通过回调来实现
//当然,如果下一个动作也是异步的,在它之后的下一个动作就会在不同的回调中
//程序逻辑会分散到不同的回调中,如果必须增加错误处理,情况会更糟
//CompletableFuture类提供了一种机制来解决这个问题,可以将异步任务组合为一个处理管线
//thenApply方法不会阻塞,它会返回另一个future,第一个future完成后,其结果会提供给第二个
//利用CompletableFuture,可以指定你希望做什么,以及希望以什么顺序执行这些工作
//thenApply方法取一个接收类型T返回类型U的函数
//thenCompose方法取一个接收T返回CompletableFuture<U>的函数
//假设有两个方法
//CompletableFuture<String> readPage(URL url)
//CompletableFuture<URL> getURLInput(String prompt)
//在这里我们有 T -> CompletableFuture<U> 和 U -> CompletableFuture<V>
//显然,如果第二个函数在第一个函数完成时调用,它们就可以组合为一个函数 T -> CompletableFuture<V>,这正是thenCompose方法所做的
//我们已经了解whenComplete方法用于处理异常
CompletableFuture<String> future1 = new CompletableFuture<>();
future1.whenComplete((s, t) -> {
if (t == null) System.out.println("success: " + s);
else System.out.println(t.getMessage());
});
//还有一个handle方法,它需要一个函数处理结果和异常,并计算一个新结果
future1.handle((result, ex) -> {
if (null != ex) {
System.out.println(ex.getMessage());
return "";
} else {
return result;
}
});
//很多情况下,更简单的做法是调用exceptionally方法,出现一个异常时,这个方法会计算一个假值
future1.exceptionally(ex -> "<html></html>"); //.thenApply(this::getImageURLs);
//可以采用同样的方式处理超时
future1.completeOnTimeout("<html></html>", 30, TimeUnit.SECONDS); //.thenApply(this::getImageURLs);
//或者也可以在超时时抛出一个异常
future1.orTimeout(30, TimeUnit.SECONDS);
//P618
//thenApply T->U
//thenAccept T->void
//thenCompose T->CompletableFuture<U>
//thenRun Runnable
//handle (T, Throwable)->U
//whenComplete (T, Throwable)->void
//exceptionally Throwable->T
//completeOnTimeout
//orTimeout
//组合多个future的方法
//thenCombine CompletableFuture<U>, (T, U)->V
//thenAcceptBoth CompletableFuture<U>, (T, U)->void
//runAfterBoth CompletableFuture<?>, Runnable
// 前3个方法并发运行一个CompletableFuture<T>和CompletableFuture<U>动作,并组合结果
//applyToEither CompletableFuture<T>, T->V
//acceptEither CompletableFuture<T>, T->void
//runAfterEither CompletableFuture<?>, Runnable
// 接下来3个方法并发运行两个CompletableFuture<T>动作,其中一个完成就传递它的结果,忽略另一个结果
//allOf CompletableFuture<?>...
//anyOf CompletableFuture<?>...
// 取一组可完成future,并生成一个CompletableFuture<Void>,allOf方法不会生成任何结果,anyOf方法不会终止其余任务
//理论上讲,这些方法接受CompletionStage类型的参数,而不是CompletableFuture
//CompletionStage接口描述了如何组合异步计算,而Future接口强调的是计算的结果
// CompletableFuture既是CompletionStage也是Future
/*用户界面回调中的长时间运行任务*/
//不能在用户界面线程进行耗时的工作,否则用户界面会冻结
//不过,不能直接从工作线程更新用户界面,Swing、JavaFX、Android等用户界面都不是线程安全的
//不能从多个线程操纵用户界面元素,否则它们会被破坏
// JavaFX和Android会检查这一点,如果试图从UI线程以外的某个线程访问用户界面,会抛出一个异常
//因此,需要调度所有UI更新都在UI线程中执行
//每个用户界面库都提供了一些机制,可以用来调度一个Runnable在UI线程执行
//例如,在Swing中:
//EventQueue.invokeLater(() -> label.setText(""));
//在工作线程中实现用户反馈很繁琐,所以每个UI库都提供了某种辅助类来管理有关细节
//如Swing中的SwingWorker、JavaFX中的Task以及Android中的AsyncTask
}
public static CompletableFuture<String> readPage(URL url, Executor excutor) {
return CompletableFuture.supplyAsync(() -> {
try {
return new String(url.openStream().readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, excutor);
}
public static void Process() throws IOException, InterruptedException {
/*【进程】P628*/
//我们已经了解了如何在同一个程序的不同线程中执行Java代码
//有时你还需要执行另一个程序,为此,可以使用 ProcessBuilder 和 Process 类
new ProcessBuilder();
//Process类在一个单独的操作系统进程中执行一个命令,允许我们与标准输入、输出和错误流交互
//ProcessBuilder类则允许我们配置Process对象
//注:ProcessBuilder类可以取代Runtime.exec调用,而且更灵活
/*建立一个进程*/
//首先指定你想执行的命令,可以提供一个List<String>,或者直接提供命令字符串
ProcessBuilder gcc = new ProcessBuilder("gcc", "myapp.c");
//!第一个字符串必须是一个可执行的命令,而不是一个shell内置命令
//!例如要在Windows中运行dir命令,就需要如下提供
ProcessBuilder dir = new ProcessBuilder("cmd.exe", "/C", "dir");
//每个进程都有一个工作目录,用来解析相对目录名
//默认情况下,进程的工作目录与虚拟机相同,通常是启动java程序的那个目录
//可以用directory方法改变工作目录
Path path = Path.of("D:\\");
gcc.directory(path.toFile());
//配置ProcessBuilder的各个方法都返回其自身,所以可以把命令串起来
Process dir1 = new ProcessBuilder("cmd.exe", "/C", "dir").directory(path.toFile()).start();
//接下来,要指定如何处理进程的标准输入、输出和错误流,默认情况下,它们分别是一个管道,可以如下访问
OutputStream processIn = dir1.getOutputStream();
InputStream processOut = dir1.getInputStream();
InputStream processErr = dir1.getErrorStream();
//注意,进程的输入流是JVM的一个输出流!我们会写入这个流,而我们写的内容会成为进程的输入
//与之相反,我们会读取进程写入输出和错误流的内容,对我们来说,它们都是输入流
//可以指定新线程的输入、输出和错误流与JVM相同
//如果用户在一个控制台运行JVM,所有用户输入都会转发到进程,而进程的输出将显示在控制台上,如下调用为这3个流建立这个设置
dir.inheritIO();
//如果你只想继承某些流,可以把值ProcessBuilder.Redirect.INHERIT传入redirectOutput/Input/Error方法
dir.redirectOutput(ProcessBuilder.Redirect.INHERIT);
//通过提供File对象,可以将进程流重定向到文件
//进程启动时,会创建或删除输出和错误文件,要追加到现有文件,可以用
//dir.redirectOutput(ProcessBuilder.Redirect.appendTo(file));
//合并输出和错误流通常很有用,这样就能按进程生成这些消息的顺序显示输出和错误
dir.redirectErrorStream(true);
//如果这样做,就不能再在ProcessBuilder上调用redirectError,也不能在Process上调用getErrorStream
//你可能还想修改进程的环境变量
//你需要得到构建器的环境(由允许JVM的那个进程的环境变量初始化),然后加入或删除环境变量条目
Map<String, String> env = dir.environment();
env.put("LANG", "zh_CN");
env.remove("JAVA_HOME");
//如果希望利用管道将一个进程的输出作为另一个进程的输入
//Java 9提供了一个 startPipeline 方法
//可以传入一个进程构建器列表,并从最后一个进程读取结果
/*List<Process> processes = ProcessBuilder.startPipeline(List.of(new ProcessBuilder("find", "/opt/jdk-11"),
new ProcessBuilder("grep", "-o", "\\.[^./]*$"),
new ProcessBuilder("sort"),
new ProcessBuilder("uniq")));*/
//当然,对于这个特定任务,用Java建立目录遍历来解决比运行4个进程更高效
/*运行一个进程*/
//配置了构建器之后,要调用它的start方法启动进程,如果把输入输出和错误流配置为管道,现在可以写输入流,并读取输出和错误流
try (var in = new Scanner(dir1.getInputStream())) {
while (in.hasNextLine())
System.out.println(in.nextLine());
}
//!进程流的缓冲空间是有限的,不能写入太多输入,而且要及时读取输出
//如果有大量输入和输出,可能需要在单独的线程中生产和消费这些输入输出
//要等待进程完成,可以调用
int i = dir1.waitFor();
//或者,如果不想无限期等待
if (dir1.waitFor(1000, TimeUnit.SECONDS)) {
int result = dir1.exitValue();
} else {
dir1.destroyForcibly();
}
//第一个waitFor返回过程的退出值,按惯例,0表示成功
//如果没有超时,第二个waitFor返回true,然后需要调用exitValue方法获取退出值
//你可能并不会等待进程结束,而只是让它继续运行,不时调用
dir1.isAlive();
//查看进程是否存活
//要杀死进程,可以调用
dir1.destroy();
dir1.destroyForcibly();
//两者的区别取决于平台,在UNIX上,前者以SIGTERM终止进程,后者以SIGKILL终止进程
//如果destroy可以正常终止进程,下面的方法返回true
dir1.supportsNormalTermination();
//最后会在进程完成时接收到一个异步通知,调用onExit()会得到一个CompletableFuture<Process>,可以用来调度任何动作
CompletableFuture<Process> processCompletableFuture = dir1.onExit();
processCompletableFuture.thenAccept(p -> System.out.println("Exit value: " + p.exitValue()));
/*进程句柄*/
//要获得程序启动的一个进程的更多信息,或者想更多地了解你的计算机上正在运行的任何其他线程,可以使用ProcessHandle接口
//可以用4种方式得到一个ProcessHandle
//1. 给定一个Process对象p,p.toHandle()会生成它的ProcessHandle
//2. 给定一个long类型的操作系统进程ID,ProcessHandle.of(id)可以生成这个进程的句柄
//3. ProcessHandle.current()是运行这个Java虚拟机的进程的句柄
//4. ProcessHandle.allProcesses()可以生成对当前进程可见的所有操作系统进程的Stream<ProcessHandle>(只是当时的快照,流中的任何进程在你看到时可能已经终止了)
//给定一个进程句柄,可以得到它的进程ID、父进程、子进程和后代进程
ProcessHandle current = ProcessHandle.current();
long pid = current.pid();
Optional<ProcessHandle> parent = current.parent();
Stream<ProcessHandle> children = current.children();
Stream<ProcessHandle> descendants = current.descendants();
//info方法可以生成一个ProcessHandle.Info对象,它提供了一些方法来获得进程的有关信息
ProcessHandle.Info info = current.info();
System.out.println(info.arguments());
System.out.println(info.command());
System.out.println(info.commandLine());
System.out.println(info.startInstant());
System.out.println(info.totalCpuDuration());
System.out.println(info.user());
//所有这些方法都返回Optional值,因为可能某个特定的操作系统不能报告这个信息
//要监视或强制进程终止,与Process类一样,ProcessHandle接口也有isAlive、supportsNormalTermination、destroy、destroyForcibly、onExit方法
//不过没有对应waitFor的方法
}
}
Bank.java
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
public static void main(String[] args) {
MyBank bank = new MyBank(100, 1000);
for (int i = 0; i < 100; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = 1000 * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((long) (10 * Math.random()));
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
new Thread(r).start();
}
}
}
class MyBank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public MyBank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try {
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
} finally {
bankLock.unlock();
}
}
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts) sum += a;
return sum;
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
参考资料
[1] 霍斯特曼, 林琪, 苏钰函. Java核心技术 卷Ⅰ 基础知识[M]. 北京:机械工业出版社, 2020.