文章目录
- 卷I
- 3. Java的基础程序设计结构
- 3.1 零散知识
- 3.3 数据类型
- 3.3.1 整型
- 3.3.2 浮点
- 3.3.3 char类型
- 3.3.5 boolean类型
- 3.4 变量与常量
- 3.4.1 声明变量
- 3.4.2 变量初始化
- 3.4.3 常量
- 3.4.4 枚举类型
- 3.5 运算符
- 3.5.1 算术运算符
- 3.5.2 数学函数与常量
- 3.5.3 数值类型之间的强制转换
- 3.5.4 强制类型转换
- 3.5.5 结合赋值和运算符
- 3.5.6 自增与自减运算符
- 3.5.7 关系与boolean运算符
- 3.5.8 位运算符
- 3.5.9 运算符
- 3.6 字符串
- 3.6.1 子串
- 3.6.2 拼接
- 3.6.3 不可变字符串
- 3.6.4 检测字符串是否相等
- 3.6.5 空串与Null串
- 3.6.6 码点与代码单元
- 3.6.7 StringBuilder构建字符串
- 3.7 输入与输出
- 3.7.1 读取输入
- 3.7.2 格式化输出
- 3.7.3 文件的输入与输出
- 3.8 控制流程
- 3.8.1 块作用域
- 3.8.4 确定循环
- 3.8.5 多重选择:switch语句
- 3.8.6 中断控制流程的语句
- 3.9 大数
- 3.10 数组
- 3.10.3 for each循环
- 3.10.4 数组拷贝
- 3.10.5 命令行参数
- 3.10.6 数组方法
- 3.10.7 多维数组
- 3.10.8 不规则数组
- 4. 类
- 4.2 使用预定义类
- 4.2.1 对象与对象变量
- 4.2.2 Java类库中的LocalDate类
- 4.3 用户自定义类
- 4.3.1 Employee类
- 4.3.5 用var声明局部变量
- 4.3.6 null引用
- 4.3.7 隐式参数与显式参数
- 4.3.8 封装的优点
- 4.3.9 基于类的访问权限
- 4.3.10 私有方法
- 4.3.11 final实例字段
- 4.4 静态字段与静态方法
- 4.4.1 静态字段
- 4.4.2 静态常量
- 4.4.3 静态方法
- 4.4.4 工厂方法
- 4.5 方法参数
- 4.6 对象构造
- 4.6.1 重载
- 4.6.2 默认字段初始化
- 4.6.3 无参数的构造器
- 4.6.4 显示字段初始化
- 4.6.5 参数名
- 4.6.6 调用另一个构造器
- 4.6.7 初始化块
- 4.7 包
- 4.7.1 包名
- 4.7.2 类的导入
- 静态导入
- 4.10 类设计技巧
- 5 继承
- 5.1 类、超类和子类
- 5.1.1 定义子类
- 5.1.5 多态
- 5.1.6 理解方法调用
- 5.1.7 阻止继承:final 类和方法
- 5.1.8 强制类型转换
- 5.1.9 抽象类
- 5.1.10 受保护访问
- 5.2 Object: 所有类的超类
- 5.2.1 Object 类型的变量
- 5.2.2 equals 方法
- 5.2.3 相等测试与继承
- 5.2.4 hashCode方法
- 5.2.5 toString方法
- 5.3 泛型数组列表
- 5.3.1 声明数组列表
- 5.3.2 访问数组列表元素
- 5.3.3 类型化与原始数组列表的兼容性
- 5.4 对象包装器与自动拆箱
- 5.5 参数数量可变的方法
- 5.6 枚举类
- 5.7 反射
- 5.7.1 Class类
- 5.7.2 声明异常入门
- 5.7.3 资源
- 5.7.4 利用反射分析类的能力
- 5.7.5 利用反射在运行时分析对象
- 5.7.6 使用反射编写泛型数组代码
- 5.7.7 调用任意方法和构造器
- 5.8 继承设计技巧
- 6.接口、lambda表达式与内部类
- 6.1 接口
- 6.1.1 接口的概念
- 6.1.2 接口的属性
- 6.1.3 接口与抽象类
- 6.1.4 静态和私有方法
- 6.1.5 默认方法
- 6.1.6 解决默认方法冲突
- 6.1.7 接口与回调
- 6.1.8 Comparator接口
- 6.1.9 对象克隆
- 6.2 lambda表达式
- 6.2.1 为什么引入 lambda 表达式
- 6.2.2 lambda表达式的语法
- 6.2.3 函数式接口
- 6.2.4 方法引用
- 6.2.5 构造器引用
- 6.2.6 变量作用域
- 6.2.7 处理lambda表达式
- 6.2.8 再谈 Comparator
- 6.3 内部类
- 6.3.2 内部类的特殊语法规则
- 6.5 代理
- 7. 异常、断言和日志
- 7.1 处理错误
- 7.1.4 创建异常类
- 7.2 捕获异常
- 7.2.2 捕获多个异常
- 7.5 日志
- 7.5.2 高级日志
- 8. 泛型程序设计
- 8.4 类型变量的限定
- 8.5 泛型代码和虚拟机
- 8.5.3 转换泛型方法
- 8.6 限制与局限性
- 8.6.3 不能创建参数化类型的数组
- 8.6.4 Varargs警告
- 8.6.6 不能构造泛型数组
- 8.7 泛型的继承规则
- 8.8 通配符类型
- 8.8.1 通配符概念
- 9. 集合
- 9.1 Java集合框架
- 9.1.4 泛型实用方法
- 9.3 具体集合
- 9.3.1 链表
- 9.3.4 树集
- 9.3.6 优先队列
- 9.4 映射
- 9.4.6 枚举集与映射
- 9.5 视图与包装器
- 9.5.1 小集合
- 9.5.5 检查型视图
- 9.6 算法
- 9.6.6 集合与数组的转化
- 9.7.3 映射
- 9.7.5 位集
- 12. 并发
- 12.1 什么是线程
- 12.3 线程属性
- 12.3.1 中断线程
- 12.3.4 未捕获异常的处理器
- 卷II
- 1. Java8的流库
- 1.1 从迭代到流的操作
- 1.2 流的创建
- 1.3 filter、map和flatMap方法
- 1.4 抽取子流和组合流
- 1.5 其他流的转换
- 1.7 Optional类型
- 1.7.4 不适合使用 Optional 值的方式
- 1.7.6 用flatMap构建Optional值的函数
- 1.7.7 将Optional转换为流
- 1.8 收集结果
- 1.9 收集到映射表中
- 1.10 约简操作
- 2. 输入与输出
- 2.1 输入/输出流
- 2.1.1 读写字节
卷I
3. Java的基础程序设计结构
3.1 零散知识
Java虚拟机总是从指定类中的main方法的代码开始执行,注意:main方法必须声明为public且必须是静态的
public static void main(String[] args)
如果main方法正常退出,返回0,表示成功运行了程序。如果希望在终止程序时返回其他的退出码,就需要使用System.exit(int status)
方法。
🐯 打印空行
System.out.println();
System.out.print(); // 不输出空行
3.3 数据类型
Java是一种强类型语言。即必须为每一个变量声明一种类型。
Java有一个能够表示任意精度的算术包,通常称为大数,是一种Java对象
3.3.1 整型
包括四种,int
、short
、byte
、long
-
在Java中,整型的范围与运行Java代码的机器无关,在任何机器上各种数据类型的取值范围都是固定的,因此移植不会造成数据的出错。但是C/C++会针对不同的处理器选择最高效的整型,移植可能会出问题。
-
可为数字字面量下划线
1_000_000
-
0b
做数字前缀可表示二进制,如0b1001
。 -
Java没有任何符号(unsigned)形式的 int、long、short 或 byte 类型。但是也提供了相应的解决方案
如
Byte.toUnsignedInt(b)
,不过需要非常仔细。使用时请查阅。 -
long类型后面有一个后缀L或l
🐯 无符号数的使用
需要找到比当前范围表示大一级的类型来表示负数
public static void main(String[] args) {Integer a = -1_000;System.out.println(a);System.out.println(Integer.toUnsignedString(a));//通过&运算先把int转为无符号long再转为String ((long) x) & 0xffffffffL;System.out.println(Integer.toString(a));System.out.println(Integer.toBinaryString(a));a = -a;System.out.println(a);System.out.println(Integer.toUnsignedString(a));System.out.println(Integer.toString(a));System.out.println(Integer.toBinaryString(a));byte b = -127;int c = Byte.toUnsignedInt(b);// process c}-10004294966296-1000111111111111111111111100000110001000100010001111101000
Integer.toString(int x);// 中的位数判断算法final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,99999999, 999999999, Integer.MAX_VALUE };// Requires positive x 传入的时候会进行大小判断
static int stringSize(int x) {//通过比较大小判断位数for (int i=0; ; i++)if (x <= sizeTable[i])return i+1;
}
3.3.2 浮点
-
float类型数值有一个后缀F或f
-
浮点数默认为double
-
可以使用十六进制表示浮点数值 仅限十六进制
0x9p-3 // 1.125 基数为十六进制,指数为十进制 9*(1/8)
-
三个特殊的浮点数值(double和float都有对应的三个特殊值)
// 通过Double.调用 public static final double POSITIVE_INFINITY = 1.0 / 0.0; //因为浮点数的精度问题,所以不会报错 public static final double NEGATIVE_INFINITY = -1.0 / 0.0; public static final double NaN = 0.0d / 0.0;//测试 System.out.println(3.0/0.0==Double.POSITIVE_INFINITY); // true System.out.println(3/0==Double.POSITIVE_INFINITY);// error 编译错误
检测一个值是否为NaN
Double.isNaN(x);// 不能去比较 x == NaN 因为所有非数值的值都认为是不相同的
🐯 精度问题
浮点数不适用与无法接受舍入误差的金融计算,因为二进制系统有些数无法精确表示如1/10,类似于十进制系统的1/3
如果不允许有任何误差,应该使用 BigDecimal
类
public static void main(String[] args) {System.out.println(1-1.1); // -0.10000000000000009System.out.println(1+1.1); // 2.1System.out.println(0.2-0.1); //0.1System.out.println(1.0 / 2.0); //0.5System.out.println(2.0 - 1.1); //0.8999999999999999
}
3.3.3 char类型
-
转义序列\u可以出现在加引号的字符常量和字符串之外
注释中加转义字符可能会出现错误,或者 c:\users --> c:\u会出错
public static void main(String\u005B\u005D args) // String[] args // Unicode转义序列会在解析代码之前得到处理。"\u0022+\u0022" --> ""+"" --> "" 一个空串
-
将字符转化为unicode
String str = "Java\u2122"; System.out.println(Integer.toHexString('D')); // 44 final String constStr = "J\u0044K"; System.out.println(constStr); // JDK System.out.println(str); // Java™
强烈建议不要使用 char 类型,如果非必要,建议使用字符串类型。
3.3.5 boolean类型
数值型与boolean类型的数据不能相互转换
if(x = 1) // 在C++中成立,会被看为true。但是Java中不可以
3.4 变量与常量
3.4.1 声明变量
变量名必须是一个以子母开头并由子母或数字构成的序列。
如果想知道那些Unicode字符属于Java中的 "字母" ,可以使用Character类的isJavaIdentifierStart和isJavaIdentifierPart方法来检查。
尽管$字符是一个合法的Java字符,但是不要在代码中使用,他只用在Java编译器或其他工具生成的名字中。
在Java9中,单下划线 _ 不能作为变量名
在Java中,变量的声明尽可能地靠近第一次使用的地方是一种良好的编程风格
3.4.2 变量初始化
从Java10开始,对于局部变量,如果可以从变量的初始值推断出它的类型,可以不再声明类型。只需要使用关键字var而无需指定类型 --> 方便但是可读性降低。
var字符只能初始化并且始终为一种数据类型。
var v;// error 未指定初始值
3.4.3 常量
-
关键字 final 指定常量
-
常量使用全大写
-
使用
static final
指示类常量,该类下的所有方法均可以访问,如果使用public修饰,那么其他类的方法都可以访问。
// 常量一般在定义时初始化。 也可以只定义,但是只能赋值一次。
public class Test {public static final int TWO = 2;// 不加public只能本类的方法使用。public static void main(String []args){final String DOG;DOG="dog";}
}
- const是Java保留的关键字,但目前没在使用。
🐯 判断数据类型的方法
Integer id;// 这里Integer有getClass(),但是int没有
id.getClass().getName();
3.4.4 枚举类型
变量的取值只在一个有限的集合中。
枚举类型只能定义在类中,方法中不行
public class Test {enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE};public static void main(String []args){//enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE}; error Java 11 不支持本地枚举Size s = Size.EXTRA_LARGE;Size d = null;System.out.println(s);}
}
Size类型的变量取值只能为限定的值或者null,null指Size不指向任何值。
3.5 运算符
3.5.1 算术运算符
整数除零会报错,浮点数除零得到NaN或无穷大。
public static void main(String []args){System.out.println(1.0 / 0); // InfinitySystem.out.println(0.0 / 0);// NaNSystem.out.println(Double.isInfinite(1.0 / 0)); // true
}
不同处理器的浮点寄存器的位数可能不同,比如有些处理器使用80位浮点寄存器,而Java浮点计算为64位,这些寄存器就增加了中间计算过程的精度,因此可能导致最终计算结果的不同。在默认情况下,允许对中间计算结果采用扩展的精度,但是,可以使用strictfp关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。
public static strictfp viod main(String[] args){
}
3.5.2 数学函数与常量
Math.floorMod()
Math.floorMod();// 解决取余过程复杂的问题,但是还是不能解决负除数的问题
public static void main(String []args){Integer position = 5;Integer adjustment = -12;System.out.println(((position + adjustment) % 12 + 12) % 12);System.out.println(Math.floorMod(position + adjustment, 12)); // 第一个参数无论为正负,结果都为[0,11]
}
Math.E
Math.PI
System.out.println(Math.exp(0)); // e的x次方 1.0
System.out.println(Math.log(Math.E)); //log以e为底x的对数 1.0
System.out.println(Math.log10(10)); // 1.0
在Math类中,大部分都是用的StrictMath类方法。
Math类中提供了一些方法使整数有更好的运算安全性。如果一个计算溢出,数学运算符返回错误的结果而不是溢出,比如
System.out.println(1000000000 * 3); // -1294967296 默认为int类型,会溢出且不报错
System.out.println((long)1000000000*3); // 3000000000
Math.multiplyExact(1000000000,3); // 产生异常 integer overflow还有 addExact()、subtractExact(减)、incrementExact(自加加)、decrementExact(自减减)、negateExact(取反)
3.5.3 数值类型之间的强制转换
-
两个操作数进行二元运算时最低也要被转化为int,(byte,short,char运算时都要转化为int才能进行运算)
-
double > float > long > int 类型转化优先级别
转化流程
if(两个操作数中有一个为double)两个操作数都转为double
else if(两个操作数中有一个为float)两个操作数都转为float
else if(两个操作数中有一个为long)两个操作数都转为long
else两个操作数都转为int
3.5.4 强制类型转换
double x = 9.92;
System.out.println((int)x); // 9
System.out.println(Math.round(x)); // 10
-
强制类型转换时可能会损失信息。如果想要进行舍入操作,需要使用
Math.round(x)
-
如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,
(byte)300
的实际值为 44。 -
boolean类型不要和任何类型进行强制转换
如果需要可以使用条件表达式b ? 1 : 0
3.5.5 结合赋值和运算符
int x = 3;
x+=3.5;
System.out.println(x); // 6 等价于 (int)(x+3.5) 会进行强制类型转换
3.5.6 自增与自减运算符
建议不要在表达式中使用++,因为这样的代码容易让人困惑,而且 会带来烦人的bug
3.5.7 关系与boolean运算符
if(x!=0){if(1/x > x+y){...}
}
等价于
if(x!=0 && 1/x > x+y) // 简洁且可读性更高
3.5.8 位运算符
-
应用在布尔值上时,
&
和|
运算符也会得到一个布尔值。 -
>>
和<<
可以将位模式左移或右移
注:移位运算的右操作数需要完成模32的运算(long则模64)。1 << 35 值 等价于 1 << 3
-
Java提供
>>>
运算符,会用0
来填充高位,>>
会用符号位来填充高位。
不存在<<<
运算符
3.5.9 运算符
Java不适用逗号运算符
3.6 字符串
当将一个字符串与一个非字符串的值进行拼接时,后者会转换为字符串
任何一个Java对象都可以转换为字符串
3.6.1 子串
str.substring(int startIndex,int endIndex);
str.substring(0,3);
// [0,3)
3.6.2 拼接
如果把多个字符串放在一起,用一个界定符分隔,可以使用静态方法join方法。
String.join(CharSequence delimiter,CharSequence... elements)
String all = String.join("/","a","b","c");
System.out.println(all);out: a/b/c
Java11提供了repeated方法
String repeated = "Java".repeat(3);
System.out.println(repeated); // JavaJavaJava
3.6.3 不可变字符串
与C++不同,在Java中,不能修改Java字符串中的单个字符
如果想要修改,只能结合其他方式(循环遍历、字符串方法等)通过重新赋值来修改字符串内容
String greeting = "hello";
greeting = greeting.substring(0,3)+"p";
System.out.println(greeting);
-
Java的这种模式可以让字符串实现共享!!比较底层了可能,待学
为什么 Java 字符串是不可变的?
3.6.4 检测字符串是否相等
String s = "hello";
String t = "hello";
s.equals(t) // 比较字符串内容是否相同
System.out.println(s.compareTo(t)); // 0表示相等,<0 表示小于 >0表示大于
字符串共享!
String s = "Hello";
String t = "Hello";
System.out.println(s == t); // true
System.out.println(s == "Hello"); // true
System.out.println(s.substring(0,1) == t.substring(0,1)); // false
3.6.5 空串与Null串
判断空串
if(str.length() == 0)
if(str.equals("")) // 空串内容为空,长度为0
检查字符串为null且内容不为空
if(str != null && str.length() != 0){// 要先判断不为空 如果为空是无法访问length的
}
3.6.6 码点与代码单元
- UTF-16某些码点是无法用char表示的!待学
3.6.7 StringBuilder构建字符串
有些时候,需要由很短的字符串构建字符串,例如,按键后来自文件中的单词,如果每次都用字符串拼接,效率会很低,因为每次拼接都要构造字符串对象,耗时且浪费空间。使用 java.lang.StringBuilder
类可以避免这个问题的发生。
StringBuilder builder = new StringBuilder();
// 添加内容
builder.append("two");
builder.append("dog");
// 构建完成时使用toString()方法
String completedString = builder.toString();
System.out.println(completedString); // twodog
// 其他方法
void setCharAt(int i, char c);
StringBuilder insert(int offset, String str);
StringBuilder delete(int startIndex, int endIndex);
3.7 输入与输出
3.7.1 读取输入
import java.util.Scanner;Scanner in = new Scanner(System.in);System.out.println("What is your name? ");
String name = in.nextLine(); // 读取一行输入包括空格,如果想要读取一个单词(空白符作为分隔符)使用 next()System.out.println("How old are you? ");
int age = in.nextInt();System.out.println("name: "+name+" age: "+age);
此外,还有 hasNext()
检测输入中是否还有其它单词等
Java Scanner的hasNext()方法
Scanner类不适用于从控制台读取密码,因为读取是可见的,可以使用Console
类来实现
不常用,使用时在学习
java.io.ConsoleConsole cons = System.console();
String username = cons.readLine("User name: ");
char[] password = cons.readPassword("Password: ");// 返回char数组
hasNext…方法
public static void main(String []args){Scanner in = new Scanner(System.in);int a = 0;if(in.hasNextInt()){ // 如果下一个读取的是int类型的再赋值,如果不是,则跳出a = in.nextInt();}System.out.println(a);in.close();// 关闭Scanner
}
3.7.2 格式化输出
用的时候再看
有各种用于格式化输出外观的标志可以设置,如左对齐、补零、分隔、正负符号参数索引、说明
System.out.printf("%+,8.2f",10000.0/3); // +3,333.33 多余的一个位置加空格,逗号也算一个占位
可以使用s转换符格式化任意的对象。对于实现了Formattable接口的任意对象,将调用这个对象中的formatTo方法;否则调用toString方法。
还可以格式化时间类型的,不过是老方法,目前普遍使用 java.time
包的方法
使用 $
符指示要格式化的参数索引。又因必须跟在 %
后面,并以 $
终止。
System.out.println("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
或者使用 <
来指示前面格式说明中的参数被再次引用
System.out.println("%s %tB %<te, %<tY", "Due date:", new Date()); // 简洁
- 🐯 格式说明符语法
-
%
argumentIndex$
flag width.
precision conversionCharacter
时间: %
argumentIndex $
flag width t
conversionCharacter
3.7.3 文件的输入与输出
文件的获取以及内容读取
public static void main(String []args) throws IOException {Scanner in = new Scanner(Path.of("C:\\Users\\16331\\Desktop\\新建文本文档.txt"),StandardCharsets.UTF_8);// 利用Scanner方法对文件进行读取while(in.hasNextLine()){ // 每次读一行System.out.println(in.nextLine());}
}
如果是字符串参数的Scanner读取文件,而不是Path.of
Scanner in = new Scanner("myfile.txt"); // 会将字符串解析为数据而不是文件,参数就是十个字符 `m`,`y`,`f`,....
写入文件
PrintWriter out = new PrintWriter("myFile.txt",StandardCharsets.UTF_8);
out.println("二狗"); // 自动换行
out.print(2);
out.append('3');
out.write("hello");
out.flush();// flush()后内容才会被写入
out.close();// 文件内容
二狗
23hello
当指定一个相对文件名时,如"myfile.txt","../myfile.txt"。文件位置是相对于Java虚拟机启动目录的位置。可以查看启动目录的位置信息
String dir = System.getProperty("user.dir");
3.8 控制流程
3.8.1 块作用域
不能在嵌套的两个块中声明相同的变量
C++允许嵌套的块中重定义一个变量。但是有可能带来编程错误,因此Java不允许
int n;
if(true){int n; // error 范围中已定义变量n
}
3.8.4 确定循环
在循环中,检测两个浮点数是否相等需要格外小心,如 for(double x = 0; x != 10; X += 0.1) 可能永远都不会结束。
3.8.5 多重选择:switch语句
case标签可以是:
- char、byte、short或int的常量表达式
- 枚举常量
- 字符串字面量 如
case "yes":
当在switch语句中使用枚举变量时,不必在每个标签中指明枚举名,可以由switch的表达式值推导出
enum Size {SMALL,MIDDLE,LARGE}public static void main(String[] args) throws IOException {Size sz = Size.SMALL;switch (sz){case LARGE:...;break;case MIDDLE:}
}
3.8.6 中断控制流程的语句
- 带标签的break语句
保留了goto语句跳出循环的优点,并使用带标签的break进行替代
Scanner in = new Scanner(System.in);
int n;
if_sentence: // label后面加上冒号:
if(true) {System.out.println("if");read_data:while (true) {n = in.nextInt();if (n > 0) {} else if (n == 0) {break read_data; // 跳出read_data的循环} else if (n < 0) {break if_sentence; // 用在if语句上也可以 可以跳出多层循环}System.out.println(n);}
}
3.9 大数
使用 java.math
包中的 BigInteger
BigDecimal
两个类来处理任意长度的数值。
构造大数的两个方法
BigInteger n = BigInteger.valueOf(long n); // 数字较小,long范围内
BigInteger n = new BigInteger(String val); // 数字大,用字符串表示
BigInteger n = BigInteger.valueOf(1000);
System.out.println(n.multiply(new BigInteger("10000000000000000000000")));
不过大数并没有提供 + - * /
等运算符,而是需要调用方法
n.add(BigInteger other);
n.subtract(BigInteger other);
n.multiply(BigInteger other);
n.divide(BigInteger other);
n.mod(BigInteger other);
对于 BigDecimal
有一些不同的地方
n.divide(BigDecimal other, RoundingMode mode); // 如果除法除不尽,会报异常,可以设置RoundingMode来对结果进行处理,如 RoundingMode.HALF_UP 即四舍五入,还有其他方法,可参看API
java不支持运算符重载
3.10 数组
3.10.3 for each循环
-
数组长度可以设置为0,但是长度为零并不为null。
-
数组在创建后,数字数组的所有元素初始化为0,boolean数组元素初始化为false,对象(String也是对象)初始化为null
int[] a = new int[10];for (int x:a // 猜测是创建的一个新变量来赋值为a) {System.out.print(x);} // 0000000000
-
一旦创建了数组,就不能再改变它的长度
-
数组长度
array.length
for(variable: collection) statement
collection这一集合表达式必须是一个数组或者是一个实现了Iterable接口的类对象(如ArrayList)。
int[] a = new int[10];System.out.println(a.toString()); // [I@48140564
System.out.println(Arrays.toString(a)); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
3.10.4 数组拷贝
如果想进行数组的拷贝,需要使用 Arrays.copyOf(int[] original,int newLength)
方法
通常使用newLength来增加新数组的大小,如果newLength < original.length,那么只拷贝前面的值。
int[] a = {1,2,3,4,5};
int[] b = Arrays.copyOf(a,2 * a.length);
System.out.println(Arrays.toString(b)+" length: "+b.length); // [1,2,3,4,5,0,0,0,0,0] length: 10
System.out.println(a == b); // false
b=a;
System.out.println(a == b); // true
Java中的 [] 运算符被预定义为会完成越界检查,而且没有指针运算,即不能通过 a加1 得到数组中的下一个元素
3.10.5 命令行参数
即 public static void main(String args[])
中的参数如何传入
3.10.6 数组方法
-
sort(xxx[] a)
使用优化的快速排序 -
binarySearch(xxx[] a, xxx v)
二分查找
-
fill(xxx[] a, xxx v)
将数组中所有的元素设置为v -
equals(xxx[] a, xxx[] b)
如果两个数组的大小相同,且对应下标的值对应相同返回true
, 否则返回false
3.10.7 多维数组
如果想要快速打印二维数组,需要使用 Arrays.deepToString(a)
方法
int[][] a = {{1,2,3},{4,5,6}};
System.out.println(Arrays.toString(a)); // [[I@58ceff1, [I@7c30a502]
System.out.println(Arrays.deepToString(a)); // [[1, 2, 3], [4, 5, 6]]
for each 循环语句
for(int [] row : a)for(int value : row)do something with value
3.10.8 不规则数组
Java实际上没有多维数组,只有一维数组,所以可以构造一个不规则数组,每一行的数组可以有不同长度。
因此在创建二维数组时,一维的长度必须指明,二维的长度可以不知名
int[][] a = new int[size][];
// 对于二维数组的长度需要自己去设置
for(int i = 0;i < a.length;i++){a[i] = new int[...];
}当采用如下方式时
int[][] a = new int[size][6];时
实际上自动调用了循环去分配了行数组
for(int i = 0;i < a.length;i++){a[i] = new int[6];
}
4. 类
-
OOP(面向对象编程) 将数据放在第一位,然后再考虑操作数据的算法
-
封装是处理对象的一个重要概念
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。封装给对象赋予了"黑盒"特性,这是提高重用性和可靠性的关键。
public String name; public违背了封装的意义,因为外部可以随便更改 应使用private
public static final String name; 经常使用,虽然为public且并没有违背封装概念,因为有final
4.2 使用预定义类
4.2.1 对象与对象变量
Date birthday = new Date();
System.out.println(birthday); // Sat Sep 03 19:11:04 CST 2022
Date deadline = birthday;
System.out.println(deadline == birthday); // true
对象变量并没有实际包含一个对象,只是引用了一个对象
在Java中,任何对象变量的值都是对存储在另一个地方的某个对象的引用。new操作符的返回值也是一个引用。
当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。
-
在Java中,创建的对象或数组是存放在堆中的,而对象的引用和基础类型变量都是放在栈中的。
-
在Java中,必须使用使用clone方法获得对象的完整副本
4.2.2 Java类库中的LocalDate类
Date
类返回一个时间点
LocalDate
来表示日期
Java8 有更好的方法来表示日期类和时间类
Sat Sep 03 20:22:29 CST 2022
LocalDate now = LocalDate.now(); // 不能使用构造器来构造对象,应当使用静态工厂方法
LocalDate oneDay = now.plusDays(0); // 加0返回this 看源码可知
LocalDate twoDay = now.plusDays(1); // 创建新对象
System.out.println(now == oneDay); // true
System.out.println(now == twoDay); // false
方法如果只访问对象而不修改对象的方法有时称为访问器方法 --> 如上面的 plusDays(long)
即访问器方法
🐯 深度刨析
观察 LocalDate
类源码可知,里面存储的 year
,month
,day
均为 final
类型,一旦赋值就不能再被改变,所以 plusDays()
方法也只能是访问器方法。
思考:语句 now.plusDays(1);
因没有赋值给变量毫无意义。
方法如果访问对象并且修改了对象的方法称为更改器方法
4.3 用户自定义类
Java对象都是在堆中构造的
4.3.1 Employee类
在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
public class EmployeeTest{...
} // 源文件名即公共类的名: EmployeeTest
class Employee{...
}
class worker{...
}
// 会有多个类文件 EmployeeTest.class Employee.class worker.class
4.3.5 用var声明局部变量
如果可以从变量的初始值推导出它们的类型,那么可以用 var
关键字声明局部变量而无需指定类型
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
var关键字只能用于方法中的局部变量
对数值类型使用var,如int、long、double,当心0、0L和0.0的区别
4.3.6 null引用
在方法中,如果不希望一个参数赋值为空,可以进行判断
public Test(String n) {if(n==null){ // 为 null 就进行赋值n="unknow";}else{this.name = n;}
}new Test(null);// Java9中提供了一个等价的方法
public Test(String n) {this.name = Objects.requireNonNullElse(n,"unknow");
}// 如果要求更严格,不能为空,为空报错则可以使用如下方法
// 抛出空指针异常,并显示提示信息
public Test(String n){Objects.requireNonNull(n,"The name cannot be null");name = n;
}
Objects.requireNonNull()
异常报告会提供这个问题的描述,并会准确指出问题出现的位置,与其他空指针异常区分开。
4.3.7 隐式参数与显式参数
在调用对象方法时,参数看着是一个,实际是两个,一个是隐式,一个是显式
number007.raiseSalary(5); // 隐式参数是第一个,即调用方法的对象,第二个才是我们传入的参数。在方法中{double raise = salary * byPercent / 100;salary += raise;
}--> 根据隐式参数和显式参数在执行时会变为如下形式{double raise = number007.salary * byPercent / 100;number007.salary += raise;
}
如果在对象方法中调用实例字段时,使用this.是一个好习惯,可以将局部变量与实例字段区别开来
🐯 C++与Java内联
在C++中,如果在对象内定义方法,则称为内联方法,而在Java中,哪个方法是内联方法是Java虚拟机的任务。即时编译器会监视那些简短、经常调用而且没有被覆盖的方法调用,并进行优化
4.3.8 封装的优点
不要编写返回可变对象引用的访问器方法,因为返回的是引用!!返回一个副本,应该使用clone
public class Test_01 {private String name;private double salary;private Date date;public Test_01(){this.date = new Date();}public Date getDate() {return date; // 返回的是引用,在外部就会被修改// return (Date) date.clone();}public void setDate(Date date) {this.date = date;}public static void main(String[] args) {var test = new Test_01();Date x = test.getDate();System.out.println(x);x.setHours(2);Date m = test.getDate();System.out.println(x);System.out.println(m);}
}
4.3.9 基于类的访问权限
class Employee{private String name;public boolean equals(Employee other){ // 参数为同类的另一个对象,可以访问其私有数据return this.name.equals(other.name);}
}
一个方法可以访问所属类的所有对象的私有数据。 C++也有同样的方法。
4.3.10 私有方法
方法通常被设置为公有,如果被设置为私有方法,通常为内部使用(提高可读性和代码简洁性、复用性等)
只要方法是私有的,类的设计者就可以确信它不会在别处使用,只会出现在该类中,可以将其删去。如果一个方法是公共的,就不能简单的将其删除,因为可能有其他代码依赖于这个方法。
4.3.11 final实例字段
- 可以将实例字段定义为final。这样的字段必须在构造对象时初始化
- 不可变类是指类的实例一旦创建后,不能改变其成员变量的值,比如
String
就是一个不可变类
类中的所有方法都不会改变其对象,这样的类就是不可变类。
注意如下情况
private final StringBuilder evaluations;构造器中初始化
evaluations = new StringBuilder(); //不变的是引用,不能指向另一个不同的StringBuilder对象,但是这个对象可以更改StringBuilder 的 append()是返回this,改变了原 StringBuilder的内容,引用不变
String 的 +,+= 不改变字符串,而是赋值给一个新的字符串,并返回该字符串引用
同是对象,String 不能变长, StringBuilder 可以public void giveGoldStar(){evaluations.append(LocalDate.now() + ": Gold star!\n");
}
final常量如果不初始化,则必须要在构造函数中初始化才可以
final 对于基本类型是无法再赋值的,对于对象,是该引用变量不能再被赋值,其引用的内容可以(注意区分String
,StringBuilder
)
4.4 静态字段与静态方法
4.4.1 静态字段
类的所有实例共享静态字段,也可以称为类字段
class Employee{private static int nextId = 1;private int id;
}
比如设置员工号码时,在构造函数中可以如此写
id = nextId;
nextId++; // 一直在增加
4.4.2 静态常量
静态常量用的比较少,但静态常量经常用
public class Math{public static final double PI =3.14159265358979323846;
}public 表示可以外部可以直接访问
static 可以通过Math.PI直接访问,可以不用创建对象
final 表示该变量不能被改变,是一个常量
特殊情况
public class System{public static final PrintStream out = ...;
}但是System中有一个 setOut 方法可以设置不同的流,这是因为该方法是一个原生方法,不是在Java语言中实现的,可以绕过Java语言的访问机制。自己写程序的时候不要模仿
4.4.3 静态方法
静态方法不是在对象上执行的,而是在类上执行的,因此,它没有隐式参数
但是对于对象调用方法时,有一个隐式参数,就是它本身,如 person.getSon();
--> 隐式参数有 this
Math.pow(x,a); // 并没有隐式参数
静态方法不能访问实例字段,但是可以访问静态字段
建议使用类名调用静态方法和静态常量,而不是使用类名
使用静态方法的场景
-
方法不需要访问对象状态(实例字段)
-
方法只需要访问类的静态字段
4.4.4 工厂方法
静态方法的一个常见 用途:构造对象
如 LocalDate.now()
和 LocalDate.of()
好处:
- 可以自定义构造器名字(原来的构造器必须与类名相同),可以根据构造器的名字来调用。如:可根据构造器名字构造对象,很方便,也实用,
getCurrencyInstance()
和getPercentInstance()
- 可以返回不同类型的对象(即可以返回A类的子类),而不是必须返回A类型对象。
4.5 方法参数
Java中参数都是按值传递的
对象传递时虽是引用,但是对于引用也是按值传递的
在Java中虽然有引用类型的数据,但是在传参时并不像C++一样传递的引用,实际对象引用是按值传递的
证明:
public static void swap(Employee x,Employee y){Employee temp = x;x = y;y = temp;
}Employee first = new Employee();
Employee second = new Employee();
swap(first,second); // 执行后 实际上first和second并没有交换
// 交换的是 副本 x 和 y, 方法结束后参数变量 x 和 y都被丢弃了
不可变类和可变类的区别
场景一:
public void strengthString1(String s){s += "a";// s 引用改变了
}public void strengthStringBuilder1(StringBuilder s){s.append("a");
}// main函数测试
Main main = new Main();
String s1 = "a";
StringBuilder sb1 = new StringBuilder("a"); // 初始都为 a
main.strengthString1(s1);
main.strengthStringBuilder1(sb1); // 调用方法都加a
System.out.println(s1); // a
System.out.println(sb1);// aa原因:1. (s1)中 s 对引用按值传递,但仍为不可变类, strengthString()中的 s += "a"只改变了方法中的 s 的引用,对 main函数中的s1变量并没有改变2. (sb1)中 s 对引用按值传递,但其为可变类, strengthStringBuilder1()中的s没有改变, 而是对引用的对象里的值进行了变化,所以返回后值修改仍存在
场景二:即场景一中的原因解释
public String strengthString2(String s){s += "a";return s;
}public StringBuilder strengthStringBuilder2(StringBuilder s){s.append("a");return s;
}// main函数
Main main = new Main();
String s2 = "a";
StringBuilder sb2 = new StringBuilder("a");System.out.println(main.strengthString2(s2) == s2); // false
System.out.println(main.strengthStringBuilder2(sb2) == sb2); // true
4.6 对象构造
4.6.1 重载
方法名及参数类型称作方法的 签名,唯一标识一个方法,与方法返回类型无关
4.6.2 默认字段初始化
方法中的局部变量必须明确地初始化。但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值(0,false或null)
缺点 : 如果此时调用构造器未对对象做初始化,
getObject()
将返回null
。
4.6.3 无参数的构造器
可以在无参构造器中设置对对象状态设置适当的默认值
public ClassName(){this.name = "dog";
}
// 使用该构造器对三个字段中的一个进行初始化,其余两个默认设置为默认值
相当于
public ClassName(){this.name = "dog";this.salary = 0;this.date = LocalDate.now();
}
4.6.4 显示字段初始化
Class Employee{private String name = ""; // 在执行构造器之前完成赋值操作。private static int nextId;private int id = assignId();private static int assignId(){int r = nextId;nextId++;return r;}
}
4.6.5 参数名
构造器参数名可如下
public Employee(String name, double salary){this.name = name;this.salary = salary;
}
// 使用 this 指定字段
4.6.6 调用另一个构造器
public Employee(){ // 第一个构造器this.date = new Date();
}public Employee(String name){this(); // 调用第一个构造器,不能使用 Test_01() 来调用,必须使用this()this.name = name;
}
4.6.7 初始化块
初始化数据字段的方法:
- 在构造器中设置值
- 在声明中赋值
private int salary = 1;
- 初始化块(先运行初始化块,再运行构造器主体部分)
// 在构造器之前调用
{id = nextId++;
}
调用顺序
private String name = ""; // 1{name = "a";
} // 2public Employee(){name = "aa";
} // 3
可以使用静态初始化块初始化静态字段
public static int nextId = 1;static{nextId = new Random().nextInt(10000);
}最终再访问时 nextId是为一个随机数的,因为先调用的初始化让nextId=1,然后调用的static块
4.7 包
4.7.1 包名
为了保证包名的绝对唯一性,要用一个因特网域名(这一定是唯一的)以逆序的形式作为包名,然后对于不同的工程使用不同的子包,比如域名horstmann.com,工程名为corejava,那么包就为 com.horstmann.corejava 然后在该包下放入java类 com.horstmann.corejava.hello
4.7.2 类的导入
private java.time.LocalDate localDate; // 完全限定名import java.time.*;
private LocalDate localDate; // 直接导入一个包
如果想同时使用 java.util.Date , java.sql.Date 在使用Date时会发成冲突,如何解决?
var deadline = new java.util.Date();
var today = new java.sql.Date(...);
静态导入
import static ...
导入静态方法和静态字段,而不只是类。
import static java.lang.System.*;out.println("Goodbye, World!");
exit(0); 可以直接使用静态方法和静态成员变量
比如当导入 Math
包后,在使用 sqrt
等方法时更清晰。
import static java.lang.Math.*;sqrt(2);
4.10 类设计技巧
-
保证数据私有; 如果需要,可以编写一个访问器方法或更改器方法
-
一定要对数据进行初始化;最好不要依赖于默认值,应该显式初始化所有的数据,可以提供默认值,或在构造器设置默认值
-
不要在类中使用过多的基本类型 ;使用其他的类替换使用多个相关的基本类型,可以使类更容易理解,也易于修改
-
不是所有的字段都需要单独的字段访问器和字段修改器;如员工的雇用日期,入职后就不会变了
-
分解有过多职责的类;数量要合理,一个类一个职责
-
类名和方法名能够体现它们的职责;名词,形容词+名称,动词(有-ing后缀)+名称;访问器加
get
,更改器加set
-
优先使用不可变类;安全,在多线程间也可以安全的共享对象,当然,有些场景使用不可变类会很奇怪,要根据情况。
5 继承
5.1 类、超类和子类
5.1.1 定义子类
extends
表示继承,子类会继承超类(父类)中所有的方法和字段(包括set 和 get)。
通过扩展超类定义子类的时候,只需要指出子类与超类的不同之处。
在子类中可以增加字段、增加方法或覆盖超类的方法,不过,继承不会删除超类的任何字段和方法。
对于从超类继承的字段
static
类字段也同样适用
private
类型字段只能通过超类中定义的 public
类型的访问器方法访问,而不能直接访问
protected
public
可以直接访问
对于构造器
子类的构造器也访问不到超类的 private
字段,所以需要通过 super(...)
来调用超类构造器对这些字段初始化,然后再对子类新加入的字段初始化
super(...)
语句必须处在第一句的位置,如果子类没有显示的调用超类的构造器,则自动调用无参数构造器。
对于方法
对于 private
修饰的方法也无法继承,只能继承 public
方法,可以进行覆盖,并可以通过 super.method()
调用超类的 public
方法
5.1.5 多态
public class Employee {private String name;private double salary;private LocalDate hireDay;public Employee(String name,double salary,int year,int month,int day){this.name = name;this.salary = salary;hireDay = LocalDate.of(year,month,day);}// ... get set
}
public class Manager extends Employee{private double bonus;public Manager(String name, double salary, int year, int month, int day) {super(name, salary, year, month, day);}@Overridepublic double getSalary() {return super.getSalary()+bonus;}// ... get set
}
public static void main(String[] args) {Manager boss = new Manager("Carl Cracker",80000,1987,12,15);boss.setBonus(5000);Employee[] staff = new Employee[3];staff[0] = boss; // 可以将子类的引用赋值给超类staff[1] = new Employee("Harry Hacker",50000,1989,10,1);staff[2] = new Employee("Tony Tester",40000,1990,3,15);for(Employee e:staff){System.out.println(e.getName() + " " + e.getSalary());}
}
虽然指定了e的类型为Employee,但实际上它既可以引用Employee类型的对象,也可以引用Manager类型的对象。 e引用Manager对象时会调用Manager类中的getSalary方法。 因为虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的方法。
一个对象变量(此处的e)可以指示多种实际类型的现象称为多态。
一个对象变量(此处的e)可以指示多种实际类型的现象称为多态。在运行时能够自动地选择适当的方法,称为动态绑定。
在C++中,如果要实现动态绑定,需要声明virtual,而Java中,动态绑定是默认的行为,如果不希望它是虚拟的,可以将它标记为
final
Java不支持多重继承,只能继承(extends)一个超类
在本例子中,虽然变量staff[0]和boss引用同一个对象。但编译器只将staff[0]看成是一个Employee对象
boss.setBonus(5000); // ok
staff[0].setBonus(5000); // ERROR
这是因为staff[0]声明的类型是Employee,而setBonus不是Employee的方法。
也不能将超类的引用赋给子类变量。
Manager m = staff[i]; // ERROR
==警告:==在Java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。
public static void main(String[] args) {Manager[] manager = new Manager[10];Employee[] staff = manager;staff[0] = new Employee("",0,1990,10,10);// ERROR manager及staff引用的是一个manager数组,不能存储Employeemanager[0].setBonus(1);}
==解释:==编译器会接纳这个赋值操作。但在这里,staff(0]与anager0]是相同的引用,似乎我们把一个普通员工擅自归入经理行列中了。这是一种很不好的情形,当调用managers[0].setBonus(1000)的时候,将会试图调用一个不存在的实例字段,进而搅乱相邻存储空间的内容。
为了确保不发生这类破坏,所有数组都要牢记创建时的元素类型,并负责监督仅将类型兼容的引用存储到数组中。例如,使用new managers[10]创建的数组是一个经理数组。如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常。
5.1.6 理解方法调用
注释: 方法的名字和参数列表称为方法的签名。有不同名字或不同参数的两个方法有不同的签名,如果子类定义了一个与超类签名相同的方法,那么就会覆盖。 由于返回值不影响签名,所以在子类覆盖方法时可以更改返回值。
如:
超类:
public Employee getBuddy(){}子类可重写
public Manager getBuddy(){}
🐯 调用过程解析
如调用 x.f(args) x为类C的一个对象
第一步:编译器查看对象的声明类型和方法名。
编译器一一列举类C和其超类中所有名为f的方法(超类的私有方法不可访问)。
第二步:编译器确定方法调用中提供的参数类型。该过程称为 重载解析
如果发现经过类型转换后有多个方法与之匹配,或者没有找到与参数类型匹配的方法,就会报一个错误。
第三步:如果是 private
方法、 static
方法、 final
方法 或 构造器方法,那么编译器可以准确地知道应该调用哪个方法。这称为 静态绑定
如果调用的方法依赖于隐式参数的实际类型,那么就必须在运行时进行 动态绑定
第四步:非第三步说的四种方法,则在运行时进行动态绑定。如:超类C和子类D都定义了该方法,该数据类型实际为D,如果D中定义了f()
,则调用D中的方法,如果没有,则调用C中的 f()
。若该类型实际为C,则调用C中的。
⚠️ 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性 超类为
public
,子类也必为public
5.1.7 阻止继承:final 类和方法
不允许扩展的类称为 final
类,不能被派生。例如 String
类,如果有一个 String
引用,它引用的一定是一个 String
对象,而不是它的子类或其他对象。
public final class Executive extends Manager{}
- 在类上加
final
--> 该类不能被派生,类中的所有方法都默认加上final
修饰符,字段是默认不加的(思考一下,final
对字段的作用是不能被修改,类中的方法可能会修改字段,所以字段如果也加final
是极其不合理的)。 - 类上不加
final
,方法加final
--> 该类可以被派生,但是方法不能被覆盖 - 字段上加
final
--> 不能被修改,可以被子类继承
🍪 小知识点
- 方法如果加上
final
字段可以避免动态绑定带来的系统开销。- 如果一个方法没有被子类覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为 内联
5.1.8 强制类型转换
子类可直接转为超类而不用强制转换
如前面的程序
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);Employee[] staff = new Employee[3];
staff[0] = boss; // 可以将子类的引用赋值给超类
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tony Tester",40000,1990,3,15);
对于 staff[0]
是无法调用 .setBonus()
方法的,需要强制转化为 Manager
类型才可以
Manager boss = (Manager) staff[0]; // 之后即可调用 .setBonus()方法
对于 staff[1]
是无法进行强制转换的
Manager boss = (Manager) staff[1]; // ERROR: ClassCastException
因为 staff[1]
存储的内容是 Employee
类型的引用,变量类型也为 Employee
类型引用;staff[0]
存储的内容是 Manager
类型的引用,变量类型为 Employee
类型引用。
子类的引用赋给一个超类变量是允许的
Manager
类型可以用Employee
引用超类的引用不能给一个子类变量
Employee
类型不能用Manager
引用
良好习惯:安全起见,在强制转换前可先查看能否成功转换。
if(staff[1] instanceof Manager){boss = (Manager) staff[1];
}
⚠️ 注意
- 只能在继承层次内进行强制类型转换
- 在超类强制转为子类之前,应该使用
instanceof
进行检查- 一般只有在使用子类特有的方法时才需要强制类型转换
一般情况下,最好尽量少用强制类型转换和instanceof
运算符
5.1.9 抽象类
-
不能创建实例对象
-
子类继承后
1
实现抽象方法,则子类无需是抽象的(也可以是)
2
如果抽象方法未全部实现,则子类必须是抽象的 -
类中没有一个抽象方法也可以定义为抽象类,如果有多于或等于一个抽象方法,则必须定义为抽象类
-
可以定义一个抽象类的对象变量,引用子类非抽象的对象
5.1.10 受保护访问
- 仅对本类可见
private
- 对外部完全可见
public
- 对本包和所有子类可见
protected
- 对本包可见 默认,不需要修饰符
访问修饰符 | 本类 | 本包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
(default) | √ | √ | × | × |
private | √ | × | × | × |
实际上,protected的可见性在于两点:
- 基类的protected成员是包内可见的,并且对子类可见;
- 若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法。
注:如 Object.clone()
方法,如果子类 Employee extends Ojbect
,但是没有重写方法,那么 protected
修饰的 clone()
方法可见性为包 java.lang
及其子类 Employee
,如果重写了 clone()
方法,那么可见性为Employee所在包极其子类
5.2 Object: 所有类的超类
Java中每个类都是由 Object类 扩展而来的
如果没有明确地指出超类,Object就被认为是这个类的超类
即
public class Employee extends Object{}
5.2.1 Object 类型的变量
在Java中,只有基本类型不是对象,所有数组类型,不管是对象数组还是基本类型的数组都扩展了Object类
可以使用 Object
类型的对象引用任何类型的对象
Object obj = new Employee("dog", 35000);Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
5.2.2 equals 方法
Object类中实现
equals
方法是将确定两个对象引用是否相等。
public boolean equals(Object obj) {return (this == obj);
}
如果要根据两个对象的状态来判断是否相等(如姓名,薪水,雇佣日期等),那么需要覆盖 equals
方法。
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 name.equals(other.name)&& salary == other.salary&& hireDay.equals(other.hireDay);
}
注释:为了安全性(可以帮助判断参数是否为null),其实这里不改也行,因为
String
类 和LocalDate
类中覆盖的equals
方法都进行了检测,安全。(猜测:为了防止我们覆盖的equal方法没考虑到)最后返回语句可以修改为
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
Objects.equals()
public static boolean equals(Object a, Object b) {return (a == b) || (a != null && a.equals(b));
}
🐯 解释
- 如果两个参数都为
null
返回true
- 如果只有一个参数为
null
返回false
- 都不为
null
,调用a.equals(b)
a对象中覆盖的equals方法
⚠️ 如果在子类定义了 equals
方法,首先调用超类的 equals
public boolean equals(Object otherObject){if(!super.equals(otherObject)) return false;Manager other = (Manager) otherObject;return bonus == other.bonus;
}
5.2.3 相等测试与继承
🐯 equals 方法要求
- 自反性 对于非空引用
x
,有x.equals(x)
返回true
- 对称性 对于非空引用
x
y
,有x.equals(y)
与y.equals(x)
的返回结果不同 - 传递性
x
y
z
不必啰嗦也懂 - 一致性 如果
x
y
的引用对象没有发生变化,无论调用多少次x.equals(y)
结果都应相同 - 对于任意非空引用
x
,x.equals(null)
应该返回false
前面对于不同类的处理
if(getClass() != otherObject.getClass()) return false;
直接返回 false;有些程序员的处理是
if(!(otherObject instanceof Employee)) return false;
这样就允许 otherObject
属于一个子类。但也可能会招来麻烦,不建议使用。
比如两个类 Employee
Manager
,其中 Manager
是 Employee
的子类,且多一个 bonus
字段
这里使用首字母代替对象,调用 e.equals(m)
时没问题,返回 true
, 我的 Employee
没有 bonus
字段,无需比较;而 m.equals(e)
需要考虑对方是 Manager
还是 Employee
,即是否有 bonus
字段来判断是否相等。
总结:两个情形
- 如果子类可以有自己相等性概念,则对称性需求将强制使用
getClass
检测- 如果超类决定相等性概念(即子类的
equals
方法相同),那么就可以使用instanceof
检查,这样可以在不同子类的对象之间进行相等性比较
解释:
- 即同一类对象之间进行比较
- 不同子类之间进行比较,但是比较有些特殊
- 超类可以决定子类之间的比较,如
A
有字段ID
, 且有子类B
C
,比较对象是否相同只看ID
,那么B
与C
、B
与B
、C
与C
都只需要看ID
即可。无需关注子类有什么 新的字段,此时就可以用instanceof
- 但是如上述
Employee
与Manager
还是要考虑不同类的类别,需要用getClass
- 超类可以决定子类之间的比较,如
完美 equals 方法
⚠️ 易错点
参数类型必须为
Object
才算是覆盖了Object
类中的equals
方法,即注意签名
public boolean equals(Object otherObject){// 为同一引用必相等if(this == otherObject) return true;// 不为空对象if(otherObject == null) return false;// 不属于同一类的对象if(getClass() != otherObject.getClass()) return false;// 或者是 instanceof// 强制转换Employee other = (Employee) otherObject;return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}
对于数组类型的字段,可以用静态的 Arrays.equals
方法检测
如果两个数组均不为 null
且长度相同,对应位置上数据元素也相同,将返回 true
5.2.4 hashCode方法
散列码是由对象导出的一个整数值。
每个对象都有一个默认的散列码,其值由对象的存储地址得出
var s = "OK";
var sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
var t = new String("OK");
var tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
结果
s | t | sb | tb |
---|---|---|---|
2524 | 2524 | 997110508 | 2052915500 |
解释:
s
t
均为 String
类型,而 String
类覆盖了 hashCode
方法,使字符串按内容得出散列码(算法里学过哦,实现方法一样) s
t
的散列码相同。而字符串构建器 sb
tb
不同,是因为 StringBuilder
类中没有覆盖 hashCode
方法,而是调用的 Object
类中的默认的 hashCode
方法,从对象的存储地址中得出散列码。
equals() 与 hashCode()
如果重新定义了 equals
方法,那么就必须重新定义 hashCode
方法。原因:两个对象相等,散列值应该也相同。所以在 Object
类中,equals
方法是根据地址来判断对象是否相等,而散列值也由地址得出。
覆盖 hashCode
public int hashCode(){return 7 * name.hashCode()+ 11 * new Double(salary).hashCode()+ 13 * hireDay.hashCode();
}
-
Double
类中才能使用hashCode
方法,double
变量不行,所以要进行创建对象// Double类中的hashCode public static int hashCode(double value) {long bits = doubleToLongBits(value);return (int)(bits ^ (bits >>> 32)); }
-
// LocalDate类中的 @Override public int hashCode() {int yearValue = year;int monthValue = month;int dayValue = day;return (yearValue & 0xFFFFF800) ^ ((yearValue << 11) + (monthValue << 6) + (dayValue)); }
都进行了覆盖,不取决于地址,而取决于内容(和 equals
相对应)
hashCode覆盖方法升级
-
使用
null
安全的Objects.hashCode
方法,如果参数为null
方法返回0
public int hashCode(){return 7 * Objects.hashCode(name)+ 11 * Double.hashCode(salary)+ 13 * Objects.hashCode(hireDay); }
Objects.hashCode
public static int hashCode(Object o) {return o != null ? o.hashCode() : 0; } // 注, hashCode()方法不由Java实现,由底层实现
-
更方便的方法
public int hashCode(){return Objects.hash(name, salary, hireDay); } // 如果该类覆盖了hashCode()方法则调用该类hashCode()方法,否则调用Object类的
Objects.hash()
源码// Objects.hash() public static int hash(Object... values) {return Arrays.hashCode(values); } // Arrays.hashCode() public static int hashCode(Object a[]) {if (a == null)return 0;int result = 1;for (Object element : a)result = 31 * result + (element == null ? 0 : element.hashCode());return result; }
对于数组类型的字段,可以使用静态的 Arrays.hashCode
方法计算散列码
5.2.5 toString方法
绝大多数的 toString
方法都遵循如下格式:类的名字 + 方括号括起来的字段值
public String toString(){return getClass().getName() // 子类调用时显示子类的类名,该类调用时显示自己的类名+ "[name=" + name+ ",salary=" + salary+ ",hireDay=" + hireDay+ "]";
}
当对象与一个字符串通过操作符 +
连接起来,Java编译器就会自动调用 toString
方法来获取这个对象的字符串描述。
再比如 System.out.println(x);
会输出 x.toString()
的内容
Object中的toString()方法
类名 + 散列值
// Object 的 toString()
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
强烈建议为自定义的每一个类添加
toString
方法
⚠️ 数组的 toString()
注:数组也是一种对象
数组继承了 Object
类的 toString
方法,即 类名 + 地址
要想输出数组的内容,需要调用 Arrays.toString()
int[] numbers = {2, 3, 5, 7, 9};
String s = "" + numbers;
System.out.println(s); // [I@79b4d0f I表示整数类型
s = Arrays.toString(numbers);
System.out.println(s); // [2, 3, 5, 7, 9]
如果要打印多维数组,应使用 Arrays.deepToString
方法
5.3 泛型数组列表
Java中可以在运行时(根据变量值)确定数组大小(C++不行)
var staff = new Employee[actualSize];
还有一个更好的方式 ArrayList
,在增删时可以自动调整数组容量
5.3.1 声明数组列表
// 较麻烦,类型写两遍
ArrayList<Employee> staff = new ArrayList<Employee>();// Java10后,可使用 var 关键字避免重复写类名
var staff = new ArrayList<Employee>();// 不用 var 关键字
ArrayList<Employee> staff = new ArrayList<>();
⚠️ 如下是错误写法
var elements = new ArraysList<>(); // 未指明类型,会默认为Object类型
// 即
ArrayList<Object>
🐯 添加元素
如果调用 add
而内部数组已经满了,数组列表就会自动创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
staff.add(new Employee("Harry Hacker",...));
源码分析
public boolean add(E e) {// 此列表在结构上被修改的次数,不知道干嘛的modCount++;add(e, elementData, size);return true;
}private void add(E e, Object[] elementData, int s) {if (s == elementData.length)elementData = grow();elementData[s] = e;size = s + 1;
}
// 每次容量只加 1
private Object[] grow() {return grow(size + 1);
}
// 数组拷贝
private Object[] grow(int minCapacity) {return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
}
如果已经知道或能估计出数组可能存储的元素数量,就可以在填充数组之前调用 ensureCapacity
方法,避免每次扩容拷贝带来的开销。
staff.ensureCapacity(100);
// 或者初始化时确定
ArrayList<Employee> staff = new ArrayList<>(100);
// 只有进行 add 后 size 才会变化哦
源码
public void ensureCapacity(int minCapacity) {if (minCapacity > elementData.length&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA&& minCapacity <= DEFAULT_CAPACITY)) {modCount++;grow(minCapacity); // 扩容}
}
🐯 返回数组列表中的元素个数
staff.size();
🐯 固定数组列表的大小
一旦能够确认数组列表的大小将保持不变,可使用该方法,这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间。垃圾回收器将回收多余的存储空间。
staff.trimToSize();
源码
public void trimToSize() {modCount++;if (size < elementData.length) {elementData = (size == 0)? EMPTY_ELEMENTDATA// 拷贝: Arrays.copyOf(elementData, size);}
}
之后再 add
remove
也不会出问题,但是你 trimToSize
操作没啥意义了就,还浪费了时间
5.3.2 访问数组列表元素
由于Java不同于C++,没有提供重载 []
的功能,ArrayList
作为一个类,使用 set
get
方法
⚠️ 注意set方法
set
方法只是用来替换数组中已经加入的元素,添加需要使用 add
方法。
// set时
var list = new ArrayList<Employee>(100); // capacity 100, size 0
list.set(0, x); // error
add(index, element)
remove(index)
效率很低,移动大量元素
套路:在添加和删除时可以灵活操作,结束后也可以灵活访问
// 数组列表,便于增删
var list = new ArrayList<X>();
while(...){x = ...;list.add(x);
}
// 存放在数组中,便于访问
var a = new X[list.size()];
list.toArray(a);
5.3.3 类型化与原始数组列表的兼容性
在虚拟机中实际上没有类型参数 ArrayList<Employee>
,一律看作 ArrayList
。这是因为 Java5之前没有泛型,为了兼容。
获取对象的引用地址
Integer.toHexString(System.identityHashCode(i));
5.4 对象包装器与自动拆箱
包装器类 Integer
等
包装器类也是不可变类,同 String
,其值一旦确定就不能变,一变就换了引用。同时,包装器类还是 final
,因此不能派生它们的子类。
Integer i = 1;
System.out.println(Integer.toHexString(System.identityHashCode(i)));
i = 2;
System.out.println(Integer.toHexString(System.identityHashCode(i)));// 3b6eb2ec
// 1e643faf
⚠️ 数组列表中尖括号中的类型参数不允许是基本类型,比如
ArrayList<int>
不允许,只能写成ArrayList<Integer>
由于每个值都包装在对象中,所以 ArrayList<Integer>
的效率远远低于 int[]
数组。因此,只有当方便性比执行效率更重要的时候,才选择数组列表。
🐯 自动装箱\自动拆箱
var list = new ArrayList<Intger>();
list.add(3);
// 在执行时编译器自动转化为如下,称为自动装箱
list.add(Integer.valueOf(3));int n = list.get(i);
// 自动拆箱, Integer 对象的值赋给 int
int n = list.get(i).intValue();// 先拆箱获取值,再自增,再装箱
Integer n = 3;
n++;
🐯 ==比较运算符
比较两个对象的值时应调用 equals
方法,==
运算符是在比较地址
📖 自动装箱规范要求
boolean
、byte
、char
<=127
,介于-128
和127
之间的short
和int
被包装到固定的对象中。
Integer a = 100;
Integer b = 100;
Integer a2 = 1000;
Integer b2 = 1000;
System.out.println(a == b); // true
System.out.println(a2 == b2); // false
如果一个条件表达式中混合使用 Integer
和 Double
类型,Integer
值就会拆箱,提升为 double
,再封装为 Double
。
Integer n = 1; Double x = 2.0;
System.out.println(true ? n : x); // 2.0
装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。
🐯 有关类的方法
java.lang.Intger
int intValue()
Integer对象的值以int类型返回
static String toString(int i, int radix)
基于数值返回一个 String
对象,第二个参数指定进制,可省略,默认为十进制
static int parseInt(String s, int radix)
String
对象转为 int
类型
static Integer valueOf(String s, int radix)
String
对象转为 Intger
对象
java.text.NumberFormat
Number parse(String s)
返回数字值(long 或 double)
NumberFormat b = NumberFormat.getNumberInstance();
Number a = b.parse("12");
System.out.println(a); // 12
5.5 参数数量可变的方法
允许方法中的最后一个参数是可变数量的
// 对于System.out.println()的定义如下
public PrintStream printf(String fmt, Object...args){}
第二个参数可以传递任意数量的对象,对于基本类型,会自动装箱为对象(由此可知 Object...args
可以接受任意数量的对象)
再如,对于方法 max()
取一些数据中的最大值
double m = max(3.1, 40.4, -5);
编译器将 new double[]{3.1, 40.4, -5}
传递给 max
方法
再如,原来在写 main
方法时是 public static void main(String[] args){}
,也可以写为 public static void main(String...args)
。也就是说最后一个参数如果是数组类型,写成可变参数的类型效果是一样的,毫无影响。
5.6 枚举类
public enum Size {// 顺序不能改哦// 括号中的参数是构造器中的参数SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");private String abbreviation;// 构造器私有化,防止外部进行 newprivate Size(String abbreviation) {this.abbreviation =abbreviation;}public String getAbbreviation(){return abbreviation;}
}
枚举类的构造器是私有的,也可以什么都不加。但是如果写为 public
或 protected
都会出现语法错误。
所有枚举类都是 Enum
类的子类。
枚举值进行比较时,可以直接使用 ==
号进行比较
🐯 方法
- 枚举对象
toString()
返回枚举常量名。如Size.SMALL.toString()
将返回字符串SMALL
- Enum类的类方法
Enum.valueOf
是toString()
的逆方法。如Size s = Enum.valueOf(Size.class, "SMALL");
根据类名和枚举常量构造 - 自定义枚举类的类方法
Size.values
获取类中的所有枚举值 - 枚举对象的方法
ordinal()
获取enum
声明中枚举常量的位置,从0开始计数。
5.7 反射
反射是Java框架的核心机制。比如根据某个配置文件
.properties
来创建一个对象就是利用反射机制。即通过外部文件配置,在不修改源码的情况下,来控制程序。
// 传统方式 new 对象 --> 调用方法// 根据配置文件 re.properties指定信息,创建对象并调用方法
re.properties
classfullpath=com.lh.reflection.Cat
method=sayHi
Cat
public class Cat {private String name = "二狗";public void sayHi(){System.out.println("hi" + name);}
}
ReflectionQuestion
// 1. 使用Properties类,读写配置文件
Properties properties = new Properties();
properties.load((new FileInputStream("src\\re.properties")));
String classpath = properties.get("classfullpath").toString();
String methodName = properties.get("method").toString();
System.out.println(classpath + methodName);// 2. 创建对象 --> 反射机制
// (1) 加载类
Class cls = Class.forName(classpath); // Class是一个类,类名为Class
// (2) 通过 cls 得到加载的类 Cat 的对象实例// Object o = cls.newInstance();// System.out.println("o的运行类型=" + o.getClass()); Cat类型
Cat o = (Cat)cls.newInstance();
// (3) 通过 cls 得到加载类的 Cat 的 methodName"hi" 的方法对象
// 即:在反射中,可以把方法视为对象
Method method1 = cls.getMethod(methodName);
// (4) 通过 method1 调用方法:即通过方法对象来实现调用方法
method1.invoke(o); // 反射机制: 方法.invoke(对象)// 输出了 hi二狗
.getClass()
获取的是运行类型。
反射机制允许程序在执行期借助于 Reflection API
取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。
加载完类之后,在堆中就产生了一个 Class
类型的对象(一个类只有一个 Class
对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。
🐯 Java反射机制可以完成如下操作
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时得到任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
反射的优点与缺点
- 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支持
- 缺点:使用反射基本是解释执行,对执行速度有影响
速度比较
// 传统方法调用sayHi()
public static void m1(){Cat cat = new Cat();long start = System.currentTimeMillis();for(int i = 0; i < 9000000; i++){cat.sayHi();}long end = System.currentTimeMillis();System.out.println(end - start);
}// 反射机制调用方法sayHi()public static void m2() throws Exception {Class cls = Class.forName("com.lh.reflection.Cat");Cat cat = (Cat)cls.newInstance();Method hi = cls.getMethod("sayHi");// hi.setAccessible(true);long start = System.currentTimeMillis();for(int i = 0; i < 9000000; i++){hi.invoke(cat);}long end = System.currentTimeMillis();System.out.println(end - start);
}// 传统方法 6
// 反射方法 33
🐯 反射调用优化-关闭访问检查
Method
和 Field
、Constructor
对象都有 setAccessible()
方法
setAccessible
作用是启动和禁用访问安全检查的开关
参数值为 true
表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为 false
则表示反射的对象执行访问检查。
5.7.1 Class类
-
Class
也是类,因此也继承Object
类 -
Class
类对象不是 new 出来的,而是系统创建的// 传统方法 Cat cat1 = new Cat(); // 反射 Class cls = Class.forName("com.lh.reflection.Cat");
上述两种方法,如果在执行时之前从未加载该类,底层都会去执行 类加载器
ClassLoader
类中的loadClass
方法,将该类的Class
对象放入堆中。 -
对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
Class cls1 = Class.forName("com.lh.reflection.Cat"); Class cls2 = Class.forName("com.lh.reflection.Cat"); // cls1 底层会执行 loadClass,第二个不再执行 System.out.println(cls1.hashCode()); System.out.println(cls2.hashCode()); // hashCode()相同,为同一个对象
-
每个类的实例都会记得自己是由哪个
Class
实例所生成(同一个类的对象都指向堆中的同一个Class
对象) -
通过
Class
对象可以完整地得到一个类的完整结构,通过一系列 API -
Class
对象是放在堆中的 -
不懂:类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括 方法代码,变量名,方法名,访问权限等等)
🐯 Class类常用方法
Car类
public class Car {public String brand = "宝马";public int price = 50000;public String color = "white";}
Class类中的 getFields
、getMethods
和 getConstructors
方法将分别返回这个类支持的公共字段、方法和构造器的数组,包括超类的公共成员。Class类中的 getDeclareFields
、getDeclareMethods
和 getDeclaredConstructors
方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员(即默认的修饰符,什么都没有)和受保护的成员,但不包含超类的成员。
String classAllPath = "com.lh.reflection.entity.Car";// <?> 表示不确定的Java类型
Class<?> cls = Class.forName(classAllPath); // cls 是一个Class类型的对象哦!!!
// 1. 输出cls
System.out.println(cls); // 输出这是哪个类的对象 class com.lh.reflection.entity.Car
System.out.println(cls.getClass()); // 输出cls运行类型 class java.lang.Class
// 2. 得到包名
System.out.println(cls.getPackage().getName()); // com.lh.reflection.entity
// 3. 得到全类名
System.out.println(cls.getName()); // com.lh.reflection.entity.Car
// 4. 通过cls创建实例对象
Car car = (Car) cls.newInstance();
System.out.println(car);
// 5. 通过反射获取属性 brand
Field brand = cls.getField("brand"); // 注:不能为私有属性,私有属性会报错
System.out.println(brand.get(car));
// 6. 通过反射给属性赋值
brand.set(car,"奔驰");
System.out.println(brand.get(car));
// 7. 获取所有的字段
Field[] fields = cls.getFields();
for (Field f : fields) {// 所有的字段属性名System.out.println(f.getName());
}
🐯 获取Class类对象的六种方式
-
已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法
forName()
获取
应用场景:多用于配置文件,读取类全路径,加载类。String classAllPath = "com.lh.reflection.entity.Car"; // 一般通过读取配置文件获取 Class<?> cls = Class.forName("classAllPath");
-
已知具体的类,通过类的class获取,该方式最安全可靠,程序性能最高。
类名.class
应用场景:多用于参数传递,比如通过反射得到对应构造器对象Class cls = Car.class;
-
已知某个类的实例,调用该实例的
getClass()
方法获取Class对象
应用场景:存在对象实例Car car = new Car(); Class cls = car.getClass();
-
通过类加载器【4种】来获取类的Class对象
String classAllPath = "com.lh.reflection.entity.Car"; Car car = new Car(); // (1) 得到类加载器 ClassLoader classLoader = car.getClass().getClassLoader(); // (2) 通过类加载器获取Class对象 classLoader.loadClass(classAllPath);
以上四种获取的 Car 的Class对象的 hashCode 值是相同的,即是同一个对象。
-
基本数据类型(
int
,char
,boolean
,float
,double
,byte
,long
,short
) 按如下方式得到Class
类对象Class<Integer> integerClass = int.class; Class<Character> characterClass = char.class; System.out.println(integerClass); // int System.out.println(characterClass);// char // 有自动装箱,拆箱
-
基本数据类型对应的包装类,可以通过
.TYPE
得到Class
对象Class<Integer> type = Integer.TYPE; System.out.println(type); // int
Integer
和int
的 Class 对象是同一个。包装类和基本数据类型(相对应)的Class对象是同一个。
🐯 哪些类型有 Class 对象
- 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
interface
: 接口- 数组
enum
: 枚举annotation
: 注解- 基本数据类型
void
Class<String> cls = String.class; // 外部类
Class<Serializable> cls2 = Serializable.class; // 接口
Class<Integer[]> cls3 = Integer[].class; // 数组
Class<float[][]> cls4 = float[][].class; // 二维数组
Class<Deprecated> cls5 = Deprecated.class; // 注解
Class<Thread.State> cls6 = Thread.State.class; // 枚举
Class<Long> cls7 = long.class; // 基本数据类型
Class<Void> cls8 = void.class; // void
Class<Class> cls9 = Class.class; // Class类也有哦System.out.println(cls); // class java.lang.String
System.out.println(cls2); // interface java.io.Serializable
System.out.println(cls3); // class [Ljava.lang.Integer;
System.out.println(cls4); // class [[F
System.out.println(cls5); // interface java.lang.Deprecated
System.out.println(cls6); // class java.lang.Thread$State
System.out.println(cls7); // long
System.out.println(cls8); // void
System.out.println(cls9); // class java.lang.Class
5.7.2 声明异常入门
当运行时发生错误时,程序就会抛出一个异常。如果没有提供处理器,程序就会终止,并在控制台上打印出一个消息,给出异常的类型。
异常有两种类型:非检查型异常和检查型异常
对于检查型异常,编译器将会检查你是否知道这个异常并做好准备来处理后果。也有很多常见的异常,如越界错误或访问null引用,都属于非检查型异常,编译器并不期望你为这些异常提供处理器,而是集中精力避免这些错误的发生。
5.7.3 资源
5.7.4 利用反射分析类的能力
🐯 反射相关的主要类
java.lang.Class
代表一个类,Class对象表示某个类加载后在堆中的对象java.lang.reflect.Method
代表类的方法,Method对象表示某个类的方法java.lang.reflect.Field
代表类的成员变量,Field对象表示某个类的字段java.lang.reflect.Constructor
代表类的构造方法,Constructor对象表示某个类的构造器
举例
// java.lang.reflect.Field// getField不能得到私有的属性
// Field nameField = cls.getField("name");
Field ageField = cls.getField("age");
System.out.println(ageField.get(o)); // 传统写法:对象.成员变量 反射:成员变量对象.get(对象)// java.lang.reflect.ConstructorConstructor constructor = cls.getConstructor(); // ()中可以指定构造器参数类型,获取不同的构造器
Constructor constructor1 = cls.getConstructor(String.class);
System.out.println(constructor); // public com.lh.reflection.Cat()
System.out.println(constructor1);// public com.lh.reflection.Cat(java.lang.String)
三个类中均有 getName()
的方法,用于返回字段、方法或构造器的名称。
Class cls = Cat.class;
// 三个都要求是公有的才能访问到
Field field = cls.getField("age");
Method method = cls.getMethod("sayHi");
Constructor constructor = cls.getConstructor();
System.out.println(field.getName()); // age
System.out.println(method.getName()); // sayHi
System.out.println(constructor.getName()); // com.lh.reflection.entity.Cat
Field
类中有一个 getType()
方法,用于返回描述字段类型的一个对象,这个对象的类型同样是 Class
Class cls = Cat.class;
Field field = cls.getField("age");
Class<?> type = field.getType();
System.out.println(type); // int
Method
和 Constructor
类有报告参数类型的方法
System.out.println(Arrays.toString(method.getParameterTypes())); // []
System.out.println(Arrays.toString(constructor.getParameterTypes())); // [class java.lang.String]
Method
有一个报告返回类型的方法
System.out.println(method.getReturnType()); // void
三个类均有一个名为 getModifiers
的方法,将返回一个整数来表示被什么修饰符修饰了
System.out.println(field.getModifiers()); // 17 final + public
System.out.println(method.getModifiers()); // 1 public
System.out.println(constructor.getModifiers());// 1 public
下面是各种修饰符对应的值,你会发现, 17 = 16 + 1,当得到整数值后,从中抽取一个最大的,如此循环即可(不会有歧义,如 1 + 2 + 4 + 8 = 15 < 16)。
修饰符 | 对应的int类型(二进制0/1位) |
---|---|
public | 1 |
private | 2 |
protected | 4 |
static | 8 |
final | 16 |
synchronized | 32 |
volatile | 64 |
transient | 128 |
native | 256 |
interface | 512 |
abstract | 1024 |
strict | 2048 |
我们这种身份,当然不能自己算了,可以利用 java.lang.reflect.Modifier
类的静态方法分析 getModifiers
返回的这个整数。利用 isPublic
,isFinal
等方法来判断
还可以利用 Modifier.toString()
方法将修饰符打印出来
System.out.println(Modifier.isPublic(17)); // true
System.out.println(Modifier.toString(17)); // public final
Field | Method | Constructor | 说明 | |
---|---|---|---|---|
getName() | √ | √ | √ | 只有公有的才能被访问到名称 |
getType() | √ | 获取字段Class对象 | ||
getParameterTypes | √ | √ | 参数类型Class对象 | |
getReturnType | √ | 返回类型Class对象 | ||
getModifiers | √ | √ | √ | 用不同的 0/1位描述所使用的修饰符 |
输出一个类的完整信息
public class ReflectionTest {public static void main(String[] args) throws ClassNotFoundException {// read class name from user inputString name;Scanner in = new Scanner(System.in);name = in.next();// print class name and superclass name (if != Object)Class cl = Class.forName(name);// get SuperClassClass supercl = cl.getSuperclass();// print class modifiersString modifiers = Modifier.toString(cl.getModifiers());if(!modifiers.isEmpty())System.out.print(modifiers + " ");System.out.print("class " + name);if(supercl != null && supercl != Object.class)System.out.print(" extends " + supercl.getName());System.out.print("\n{\n");printConstructors(cl);System.out.println();printMethods(cl);System.out.println();printFields(cl);System.out.println("}");}private static void printFields(Class cl) {Field[] fields = cl.getDeclaredFields();for (Field field : fields) {System.out.print(" ");System.out.print(Modifier.toString(field.getModifiers()));System.out.print(" " + field.getType());System.out.println(" " + field.getName() + ";");}}private static void printConstructors(Class cl) {Constructor[] constructors = cl.getDeclaredConstructors();for (Constructor constructor : constructors) {System.out.print(" ");System.out.print(Modifier.toString(constructor.getModifiers()));System.out.print(" " + cl.getName());System.out.print("(");Class[] parameterTypes = constructor.getParameterTypes();for (int j = 0; j < parameterTypes.length; j++) {if(j > 0)System.out.print(",");System.out.print(parameterTypes[j].getName());}System.out.println(");");}}private static void printMethods(Class cl) {Method[] methods = cl.getDeclaredMethods();for (Method method: methods) {System.out.print(" ");System.out.print(Modifier.toString(method.getModifiers()));System.out.print(" " + method.getReturnType());System.out.print(" " + method.getName());System.out.print("(");Class[] parameterTypes = method.getParameterTypes();for (int j = 0; j < parameterTypes.length; j++) {if(j > 0)System.out.print(",");System.out.print(parameterTypes[j].getName());}System.out.println(");");}}
}
输出结果
// 输入
java.lang.Double
// 输出
public final class java.lang.Double extends java.lang.Number
{public java.lang.Double(double);public java.lang.Double(java.lang.String);public boolean equals(java.lang.Object);public static class java.lang.String toString(double);public class java.lang.String toString();public int hashCode();public static int hashCode(double);public static double min(double,double);public static double max(double,double);public static native long doubleToRawLongBits(double);public static long doubleToLongBits(double);public static native double longBitsToDouble(long);public volatile int compareTo(java.lang.Object);public int compareTo(java.lang.Double);public byte byteValue();public short shortValue();public int intValue();public long longValue();public float floatValue();public double doubleValue();public static class java.lang.Double valueOf(java.lang.String);public static class java.lang.Double valueOf(double);public static class java.lang.String toHexString(double);public static int compare(double,double);public static boolean isNaN(double);public boolean isNaN();public static boolean isFinite(double);public static boolean isInfinite(double);public boolean isInfinite();public static double sum(double,double);public static double parseDouble(java.lang.String);public static final double POSITIVE_INFINITY;public static final double NEGATIVE_INFINITY;public static final double NaN;public static final double MAX_VALUE;public static final double MIN_NORMAL;public static final double MIN_VALUE;public static final int MAX_EXPONENT;public static final int MIN_EXPONENT;public static final int SIZE;public static final int BYTES;public static final class java.lang.Class TYPE;private final double value;private static final long serialVersionUID;
}
5.7.5 利用反射在运行时分析对象
利用反射机制可以查看在编译时还不知道的对象字段。
只能对可以访问的字段使用get和set方法。Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写那些字段的值。
反射机制的默认行为受限于Java的访问控制。不过,可以调用Field、Method或Constructor对象的setAccessible方法覆盖Java的访问控制
🌰 举栗
如果 f
是 Field
类型的对象(通过getDeclaredFields才可以获取私有类型的字段),obj
是该类的一个对象,f.get(obj)
将返回一个对象,其值为 obj
的当前字段值。设置值,调用 f.set(obj,value)
。
Class cls = Cat.class;Cat cat = (Cat)cls.getConstructor().newInstance();
Field f = cls.getDeclaredField("name"); // 需要使用 getDeclareField 来获取私有类型的成员
f.setAccessible(true); // 开启读写权限
Object o = f.get(cat);
System.out.println(o.getClass()); // class java.lang.String
System.out.println(o); // 二狗
- 课本toString方法实现所有字段的访问没看懂
5.7.6 使用反射编写泛型数组代码
java.lang.reflect
包中的 Array
类允许动态地创建数组。例如,Array
类中的 copyOf
方法实现就使用了这个类。
Why ?
假设不使用反射实现
public static Object[] copyOf(Object[] a, int newLength){Object[] newArray = new Object[newLength];System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));return newArray;
}public static void main(String[] args) {Cat[] cats = new Cat[5];// error: [Ljava.lang.Object; cannot be cast to [Lcom.lh.reflection.entity.Cat;cats = (Cat[]) copyOf(cats,10);Arrays.toString(cats);
}
原因是因为我们使用的 new Object[newLength]
创建的数组,不能强制转换为 Cat[]
。
将一个 Cat[]
临时转换成 Object[]
数组,然后再把它转换回来是可以的,但一个从开始就是 Object[]
的数组却永远不能转换成 Cat[]
数组。
因此,通用代码应如下:
int[]可以转为Object,但不能转为Object[]
// 参数为Object类型,而不能为数组,因为 int[]可以转为Object,但不能转为Object[]
// 返回类型为 Object 原因也同理
public static Object copyOf(Object a, int newLength){// 获取类型Class cls = a.getClass();// 判断是否为数组,不为数组不扩大if(!cls.isArray())return null;// 获取元素类型, java.lang.Class.getComponentType()Class componentType = cls.getComponentType();// 获取数组长度, java.lang.reflect.Array.getLength()int length = Array.getLength(a);// 创建该类型数组Object newArray = Array.newInstance(componentType,newLength);System.arraycopy(a, 0, newArray, 0, Math.min(length,newLength));return newArray;
}public static void main(String[] args) {Cat[] cats = new Cat[5];cats = (Cat[]) copyOf(cats,10);Arrays.toString(cats);
}
5.7.7 调用任意方法和构造器
在C和C++中,可以通过一个函数指针执行任意函数。从表面上看,Java没有提供方法指针,也就是说,Java没有提供途径将一个方法的存储地址传给另一个方法,以便第二个方法以后调用。事实上,Java的设计者曾说过:方法指针是很危险的,而且很容易出错。他们认为Java的接口(interface)和 Lambda表达式是一个很好的解决方案。不过,反射机制允许你调用任意的方法。
🐯 调用任意方法
Object invoke(Object obj, Object... args);
第一个参数是隐式参数,其余的对象提供了显式参数。
对于静态方法,第一个参数设置为 null
。
🌰 举栗
Method getName = cls.getDeclareMethods("getName");
String name = (String)getName.invoke(harry); // Method.invoke(对象)
如果返回类型是基本类型,invoke
方法会返回其包装器类型。
double s = (Double)m2.invoke(harry); // 先强转为Double,再自动拆箱为double
获取 Method
: getMethod
的签名是:
Method getMethod(String name, Class... parameterTypes)
Method m = Cat.class.getMethod("resize", int.class);
构造器类似。
warning
由于
invoke
方法的参数和返回值必须是Object
类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正会麻烦得多。不仅如此,使用反射获得方法指针的代码要比直接调用方法的代码慢得多。因此,建议仅在绝对必要的时候才在程序中使用
Method
对象。通常更好的做法是使用接口以及lambda表达式。
建议不要使用回调函数的Method对象。可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护
5.8 继承设计技巧
继承的所有字段和方法都有意义,再去继承
-
将公共操作和字段放在超类中
-
不要使用受保护的字段
原因:第一,子类集合是无限制的,任何一个人都能够由你的类派生一个子类,然后编写代码直接访问protected
实例字段,从而破坏了封装性。第二,在 Java 中,在同一个包中的所有类都可以访问protected
字段,而不管它们是否为这个类的子类。
不过,protected
方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。 -
使用继承实现
is-a
关系
即派生的子类必须合理的拥有超类的所有字段和方法。比如员工有姓名、年龄、雇佣日期、工资等字段,钟点工应该有姓名、年龄、雇佣日期、一小时的薪资等字段。不能盲目的让钟点工派生自员工,然后加上一个hourlyWage
字段,因为这样钟点工同时有了时薪和工资,在打印信息toString()
等方法时很麻烦,在日后维护中,你要一直记着这个本该不存在但实际存在的字段–工资。 -
除非所有继承的方法都有意义,否则不要使用继承
-
在覆盖方法时,不要改变预期的行为。
即在子类中覆盖方法时,不要偏离最初的设计想法 -
不要滥用反射
反射对于编写系统程序极其有用,但是通常不适于编写应用程序。如果使用反射,编译器将无法帮助查找编程错误,因此只有在运行时才会发现错误并导致异常。 -
使用多态,而不要使用类型信息
如看到以下类似代码if(x is of type 1)action1(x); else if(x is of type 2)action2(x);
都应该考虑使用多态。
action1 与 action2 表示的是相同概念吗?如果是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类型的超类或接口中,然后,就调用x.action()
使用多态固有的动态分配机制执行正确的动作。
使用多态方法或接口实现的代码比使用多个类型检测的代码更易于维护和扩展
6.接口、lambda表达式与内部类
内部类技术在设计具有相互协作关系的类集合时很有用。
6.1 接口
6.1.1 接口的概念
接口不是类,而是对希望符合这个接口的类的一组需求。
Arrays
类中的 sort
方法承诺可以对对象数组进行排序,但要求满足对象所属的类必须实现 Comparable
接口。
Comparable接口
public interface Comparable{int compareTo(Object other);
}
在Java5之后,Comparable
接口已经提升为一个泛型类型。
public interface Comparable<T>
{int compareTo(T other);
}
因此将有如下两个实现接口的方式,推荐使用第一种
// 指定泛型类型,为泛型接口提供一个类型参数,只要在类中需要实现 compareTo(Employee other) 即可
class Employee implements Comparable<Employee> {public int compareTo(Employee other){}
}
第二种:
// 不使用泛型
class Employee implements Comparable{public int compareTo(Object otherObject){Employee other = (Employee)otherObject; // 需要先进行强制转换,再进行比较}
}
🐯 接口注意事项
- 接口中的所有方法都自动是
public
方法。因此,在接口中声明方法时,不必提供关键字public
- 接口中可以定义常量。但不能有实例字段
- 接口中的字段总是
public static final
- 在实现接口时,必须把方法声明为
public
🐯 整数比较方法
比较两个整数大小,可直接返回相减的结果,但是还注意整数范围不能过大,避免溢出。 若过大,可以使用 Integer.compare
方法。
🐯 浮点数比较方法
相减技巧不适用于浮点数,因为可能两个数很接近但又不相等,差值四舍五入可能变为0。可以使用 Double.compare(x,y)
方法
Comparable接口的文档建议compareTo方法应当与equals方法兼容。也就是当
x.equals(y)
时x.compareTo(y)
就应当等于0。不过也有一个重要的例外,就是BigDecimal
。
举例 🌰
x = new BigDecimal("1.0");
y = new BigDecimal("1.00");
x.equals(y); // false 因为两个数的精度不同
x.compareTo(y); // 0
🍪 Arrays.sort
小知识点
实际上 Arrays.sort
可以接受没有实现 Comparable
接口的参数数组,但是在实现方法时,会先将元素转为 Comparable
类型,转化失败就会抛出异常。
在实现 comparable 时仍可能出现同前面定义 equals 方法,处理方法也一样
6.1.2 接口的属性
也可以使用 instanceof
检查一个对象是否实现了某个特定的接口
if(anObject instanceof Comparable){}
🐯 建议
有些接口只定义常量,而没有定义方法,这样的接口更像是退化,所以建议最好不要这样使用。
6.1.3 接口与抽象类
接口也可以扩展 (extends
) 类
6.1.4 静态和私有方法
在Java8中,允许在接口中增加静态方法,这样以来类就不是必要的了,可以使用接口,如 public interface Path
作为接口提供静态方法,而 public final class Math
作为类提供静态方法。
在Java9中,接口中的方法可以是 private
。private
方法可以是静态方法或实例方法,不过这些私有方法只能在本接口中使用,用法有限。
6.1.5 默认方法
可以为接口方法提供一个默认实现,使用 default
修饰符标记这样一个方法
默认方法在接口中必须提供实现,在实现类中可以按需重写
同时默认方法可以调用其他方法。
举例 🌰
public interface Collection
{int size();default boolean isEmpty(){ return size() == 0; }...
} // 实际并非如此,这里只是举例
如此,在继承 Collection
时可以不实现 isEmpty()
方法。如果需要再去实现修改。
🐯 使用 default 的一个好处 接口演化
如,很久之前你提供了这样一个类
public class Bag implements Collection
之后 Collection
增加了一个新方法 stream
。作为 Bag
类由于没有实现 stream
方法将不能编译。而使用了 default
关键字将可以正常编译且可以调用 Collection.stream
方法。
6.1.6 解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,应该如何解决这种二义性?
🐯 规则
-
超类优先 如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
-
接口冲突 如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
举例 🌰interface Person{default String getName() { return ""; } }interface Named{default String getName() { return getClass().getName() + "_" + hashCode(); } }class Student implements Person, Named {...}
此时编译器会报告错误,由程序员来解决这个二义性问题。例如
class Student implements Person, Named {// 自己选择一个,也可以自定义public String getName() { return Person.super.getName(); } }
如果继承的接口中,有的默认实现了
getName
,有的没有,编译器也会报错,让程序员来解决。即如果至少有一个接口提供了一个实现,编译器就会报告错误,程序员就必须解决这个二义性。 -
如果从超类和接口继承了相同的方法,那么只会考虑超类方法,接口的所有默认方法都会被忽略。这即是 类优先 原则。
千万不要让一个默认方法重新定义Object类中的某个方法。例如,不能为toString或equals定义默认方法,尽管对于List之类的接口这可能很有吸引力。由于类优先规则,这样的方法绝对无法超越Object.toString或Object.equals。写了也等于没写
事实上,想这么做也不可能,编译器会报错。
6.1.7 接口与回调
回调 是一种常见的程序设计模式。在这种模式中,可以指定某个特定的事件发生时应该采取的动作。如设置定时器,每隔一个时间间隔执行一个事件。在很多程序设计语言中,都是定期调用一个函数,而Java标准类库中的类采用的是面向对象方法。所以可以向定时器传入某个类的对象,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活的多。
定时器测试
public class TimerTest {public static void main(String[] args) {TimerPrinter listener = new TimerPrinter();// java.swing.Timer类 定时器Timer timer = new Timer(1000, listener);// 启动定时器timer.start();JOptionPane.showMessageDialog(null, "Quit program?");System.exit(0);}
}
// 实现 java.awt.event.ActionListener接口
class TimerPrinter implements ActionListener{// 事件@Overridepublic void actionPerformed(ActionEvent e) {// 打印时间System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen()));// 蜂鸣器Toolkit.getDefaultToolkit().beep();}
}
6.1.8 Comparator接口
在前面的介绍的 Comparable
接口,我们得知,Arrays.sort()
函数可以实现该继承该接口的对象数组的排序。 String
类也继承了该接口,并实现 String
按字典序大小比较。
有该使用场景:我想让 String
按长度进行比较,但由于 String
是 final
类型,不能继承和实现,不能重写 compareTo
。该如何做呢?
🐯 解决方案
Arrays.sort()
提供了一个版本,一个数组和一个比较器作为参数,比较器是实现了 Comparator
接口的类的实例。
Comparator接口
public interface Comparator<T>
{int compare(T first, T second);
}
实现方式如下
/*** 实现字符串按照长度进行比较*/
public class StringCompare {public static void main(String[] args) {// 创建比较器对象LengthComparator comparator = new LengthComparator();String[] friends = {"dog","Peter","Mary"};// 两个字符串的比较if(comparator.compare(friends[0],friends[1]) < 0){System.out.println("二狗菜鸡");}// 排序,第二个参数为比较器对象Arrays.sort(friends,comparator);System.out.println(Arrays.toString(friends));}}
// 比较器定义
class LengthComparator implements Comparator<String>{@Overridepublic int compare(String o1, String o2) {return o1.length() - o2.length();}
}
6.1.9 对象克隆
首先介绍一下 Cloneable
接口,这个接口指示一个类提供了一个安全的 clone
方法。
这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很偏执,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。
Cloneable
接口是Java提供的少数标记接口之一。标记接口不包含任何方法;它唯一的作用就是运行在类型查询中使用instanceof
建议程序中不要使用标记接口。
Comparable
等接口的通常用途是确保一个类实现一个或一组特定的方法。
🐯 Object.clone()重写
然后我们介绍一下 Ojbect.clone()
方法, clone()
方法是 Ojbect
的一个 protected
方法,这说明我们的代码不能直接调用这个方法。只有 Employee
类可以克隆 Employee
对象。
解释
我们创建了一个类,这个类位于 com.lh.clone
包下,它所继承的 clone
方法来自 java.lang
(如果重新那么 clone
方法也来自 com.lh.clone
)。此时 clone
方法可见性为 java.lang
包以及 子类 Employee
。
此时同一目录下我们的测试类创建了 Employee
对象,却无法调用 clone
方法,报错原因是 ‘clone()’ has protected access in ‘java.lang.Object’。因为 clone
方法可见性为 java.lang
包以及 子类 Employee
。对 CloneTest
不可见,检测不到 clone
方法,所以无法编译。但是如果我们在 Employee
方法中重写 clone
方法,那么 clone
方法可见性就变为了 com.lh.clone
包 及其子类(无子类),在测试类中就可以访问到 clone
方法。所以如果要使用克隆方法,我们必须重新定义 clone 方法为 public 才能允许所有方法克隆对象。 (一般情况下,实体类单独一个包,测试类单独一个包,所以不能用 protected
修饰符,否则即使重写也访问不到)。
🐯 浅拷贝
Object.clone()
默认使用浅拷贝,在进行拷贝时,基本数据类型拷贝数值,对象拷贝引用。我们对这几种情况进行分析
- 基本数据类型,浅拷贝即可,不会共享信息。
- 对象属于可变的,如
Date
类型,共享引用后,一方修改会影响到另一方 - 对象属于不可变的,如
String
类型,共享引用后仍是安全的,一方修改并不会影响到另一方 - 对象的生命期中,一直包含不变的常量,并且没有修改器方法会改变它,也没有方法会生成它的引用供外部修改,也是安全的。
如果对象中的变量都属于 1,3,4,那么仅使用浅拷贝是安全的。如果浅拷贝能够满足需求,还是要实现 Cloneable
接口,将 clone
重新定义为 public
,再调用 super.clone()
浅拷贝clone()
// 简单重写即可
class Employee implements Cloneable{public Employee clone() throws CloneNotSupportedException{return (Employee) super.clone();}
}
🐯 深拷贝
如对象是可变的,需要的工作:
- 也需要改为
public
并继承Cloneable
接口 - 对可变对象进行深拷贝 (
Date
类重写了clone
,为深拷贝)
class Employee implements Cloneable{public Employee clone() throws CloneNotSupportedException{Employee clone = (Employee) super.clone();// Date的深拷贝clone.hireDay = (Date)hireDay.clone();return clone;}
}
🐯 数组拷贝
所有数组类型都有一个公共的 clone
方法,而不是受保护的。可以利用这个方法建立一个新数组,包含原数组所有元素的副本。
int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
int[] cloned = luckyNumbers.clone();
cloned[5] = 12; // doesn't change luckyNumbers
🤔 思考
Employee
实现了 clone
方法,那么其子类 Manager
也可以调用 clone
方法,并且 Manager
对象
可以调用 clone
方法进行克隆,如果 Manager
添加了新字段应该如何做?
解:如果新增加的字段不需要深拷贝则不需要管,如果新字段需要深拷贝则需要在子类重写 clone
方法,调用超类(Employee)的 clone
,并对新字段深拷贝。
Manager的深拷贝
// 新字段
private int bonus;
private Date d;@Override
public Manager clone() throws CloneNotSupportedException {Manager clone = (Manager) super.clone();clone.d = (Date) d.clone();return clone;
}
6.2 lambda表达式
6.2.1 为什么引入 lambda 表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。(前面的计时器及字符串按长度排序等都需要一个对象,很麻烦,因为实际上他们只需要对象中的一个特定方法)。
6.2.2 lambda表达式的语法
语法形式如下:
实现的这个接口中的抽象方法中的形参列表 -> 抽象方法的处理
eg:
(String first, String second)
-> first.length() - second.length()
⚠️ 注意
-
如果抽象方法的处理无法放在一个表达式中,就可以像写方法一样,放在
{}
中,并包含一个return
语句。(String first, String second) -> { if(first.length() > second.length()) return 1;else if(first.length() > second.length()) return -1; else return 0;
-
即使 lambda 表达式没有参数,仍要提供空括号。
() -> System.out.println("hello");
-
如果可以推导出一个 lambda 表达式的参数类型,可以忽略其类型。
(first, second) -> first.length() - secod.length();
-
如果方法只有一个参数,还可以省略小括号。
event -> System.out.println("hello");
-
无需指定返回值类型, lambda表达式会自己推导出来
举例1
接口定义
public interface LambdaInterface {void show(int a,int b);
}
测试
public static void main(String[] args) {// ---------------- 匿名内部类LambdaInterface lambdaInterface = new LambdaInterface() {@Overridepublic void show(int a, int b) {System.out.println(a + b);}};lambdaInterface.show(10,20);// ---------------- lambda表达式// lambda简写// 1. 参数类型可以忽略,由接口定义可知为 int 类型// 2. 执行语句只有一句LambdaInterface lambdaInterface1 = (a, b) -> System.out.println(a + b);lambdaInterface1.show(10,10);
}
🐯 匿名内部类与lambda表达式
lambadInterface的class com.lh.lambda.LambdaTest$1
lambadInterface的class com.lh.lambda.LambdaTest$$Lambda$1/1324119927
第一个 new LambdaInterface
看似违背了不能创建接口对象这个事情,实际上JVM会创建一个实现了LambdaInterface
接口的匿名内部类。
第二个 lambda 表达式实际最终为成为该类的一个静态的私有方法。
https://blog.csdn.net/weixin_43687181/article/details/116053262
6.2.3 函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式(就不需要定义一个类继承这个接口然后再创建对象)。这种接口成为函数式接口。如前面的 ActionListener
和 Comparator
。
// Arrays.sort()的第二个参数为Comparator,为一个函数式接口,所以可以直接使用lambda表达式
Arrays.sort(words, (first,second) -> first.length() - second.length());
lambda 表达式可以转化为接口
不能把lambda表达式赋给类型为Object的变量,Object不是函数式接口。
想用 lambda 表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
🐯 java.util.function
中的通用函数式接口
注意接口函数规定的返回值类型
-
BiFunction<T, U, R>
// 函数式接口类型的变量可以接收lambda表达式 BiFunction<String,String,Integer> comp = (first,second) -> first.length() - second.length();
不过该函数式接口不能用于
Arrays.sort()
,因为它的第二个参数是Comparator
类型。 -
Predicate<T>
Predicate接口public interface Predicate<T> {boolean test(T t);// 只有这一个抽象方法,但还有其他默认实现的方法和静态方法// 注:仍为函数式接口 }
如:
ArrayList
类中有一个removeIf
方法,参数就是一个Predicate
。list.removeIf(e -> e == null); // 删除数组列表中所有 null 值
-
Supplier<T>
Supplier接口public interface Supplier<T> {T get(); }
供应者(Supplier) 没有参数,调用时会生成一个
T
类型的值。供应者用于实现懒计算。
懒计算举例// MyDay.class public class MyDay {public MyDay(){System.out.println("constructor is done!");} }// SupplierTest.class public class SupplierTest {public void test(MyDay myDay){// 无论参数是否为空都会创建 MyDay 对象MyDay m = Objects.requireNonNullElse(myDay, new MyDay());}public void test2(MyDay myDay){// 参数不为空才会创建 MyDay 对象MyDay m = Objects.requireNonNullElseGet(myDay, ()-> new MyDay());}public static void main(String[] args) {SupplierTest t = new SupplierTest();t.test(new MyDay()); // 输出两个 constructor is done!t.test2(new MyDay()); // 输出一个 constructor is done!} }
requireNonNullOrElseGet
方法只在需要值时才调用供应者。
6.2.4 方法引用
方法引用的提出: 由于如果存在一种情况,我们新建了多个接口的实现对象,其方法都是相同的,但是如果方法需要修改,那么修改的复杂度就随着对象数量的上升而上升。
方法引用的定义: 快速将一个Lambda表达式的实现指向一个已经写好的方法 方法引用可以看作是lambda表达式的特殊形式,或者称之为语法糖。一般方法已经存在才可以使用方法引用,而方法若未存在,则只能使用lambda表达式。
var timer = new Timer(1000, event -> System.out.println(event)); // lambdavar timer = new Timer(1000, System.out::println); // 方法引用同理之前的
list.removeIf(e -> e == null); 可简化为
list.removeIf(Objects::isNull);
方法引用会指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。本例中,会生成一个
ActionListener
,它的actionPerformed(ActionEvent e)
方法要调用System.out.println(e)
类似于lambda表达式,方法引用也不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对象。
🐯 延伸
// 由于Timer构造函数中的第二个参数为 ActionListener ,里面只有一个抽象方法(参数只有一个),所以可以作为函数式接口,且在分析 System.out::println 时会根据抽象方法选择合适的类型,由于只有一个参数,选择了比较合适的 System.out.println(Object o); println共十几个重载的方法Runable task = System.out::println;// Runable 函数式接口有一个无参数的抽象方法 run(),所以此处会选择 System.out.println() 打印一个空行// 忽略大小写,对字符串数组排序
// 本质上 compareToIgnoreCase方法是一个 Comparator 实例对象调用了内部的 compare() 方法
Arrays.sort(strings, String::compareToIgnoreCase);
// 若要不忽略大小写,使用 String::compareTo
🐯 方法引用的三种形式
object::instanceMethod
Class::instanceMethod
Class::staticMethod
🐇 解释
object::instanceMethod
例如System.out.println();
System.out
是一个对象,属于PrintStream
类。该方法引用等价于向方法传递参数的 lambda 表达式。System.out::println
等价于x -> System.out.println(x);
Class::instanceMethod
第一个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase
等同于(x,y) -> x.compareToIgnoreCase(y);
❓ 这句话什么意思呢,哪个参数呢?—> 它这里所说的参数是函数式接口中方法的参数。
比如Arrays.sort()
第二个参数是Comparator
函数式接口,它里面的唯一抽象方法是int compare(T o1, T o2);
参数就是指的这两个。Class::staticMethod
所有参数都传到静态方法。例如Math::pow
等价于(x,y) -> Math.pow(x,y);
🐯 方法引用的缺点
只是作调用方法的作用而不能有其他操作。
🍺 知识梳理
方法定义时需要的参数其实都是函数式接口实现的一个对象,无论lambda表达式还是方法引用都只是一个简化方法。都不能独立存在,总是会转化为函数式接口的实例
参数都在函数式接口中规定好了,lambda相当于一个即兴的方法,在该处修改只影响这一个地方。而方法引用是在对象中规定好的方法,修改后所有引用它的地方都会收到影响。
- lambda的引出:由于原来Java8之前在参数需要使用一个代码块时,都是需要一个对象,在对象中指定一个方法。很麻烦,由此出现了 lambda表达式,在lambda表达式中使用简洁的代码说明抽象方法的实现过程而不需要创建这个对象。
- 方法引用的引出:lambda表达式实际是一个代码块,有参数、执行体、返回值(参数、返回值可有可无,但我们可以将lambda看为一个函数)。如果多个方法都使用同样的lambda表达式,在需要修改时,我们需要修改多处代码,所以引入了方法引用。等同于我们将 lambda 表达式封装为一个固定的函数,并在使用时将这个方法传入。
🐯 方法引用举例
/*** 自定义一个方法引用,实现字符串数组实现按长度排序 Class::staticMethod*/
public class MethodReference {public static int compareToLength(String x, String y){return x.length() - y.length();}public static void main(String[] args) {String []strings = new String[3];strings[0] = "ab";strings[1] = "c";strings[2] = "def";Arrays.sort(strings, MethodReference::compareToLength);System.out.println(Arrays.toString(strings)); // [c, ab, def]}}
Class::instanceMethod
需要我们在 String
中定义方法 public int compareToLength(String y)
。其中应 return length() - y.length()
因为自身即为 String
。
object::instanceMethod
即这个对象调用这个方法,抽象方法中的参数传入这个方法里。
包含对象引用与等价的lambda表达式还有一个细微的差别。考虑一个方法引用,如 separator::equals。如果separator为null,构造 separator::equals时立即会抛出空指针异常,而 lambda 表达式 x -> separator.equals(x) 只会在调用时才会抛出空指针异常。
🐯 this 与 super
可以在方法引用中使用 this
super
参数。
public class MethodReference02 extends Greeter{// superpublic void greet(){var timer = new Timer(1000,super::greet);}// thispublic void greet2(){var timer = new Timer(1000,this::helloDog);}public void helloDog(ActionEvent actionEvent){System.out.println("hello, tow dogs!");}
}class Greeter{public void greet(ActionEvent e){System.out.println("hello");}
}
6.2.5 构造器引用
待学,看不懂
6.2.6 变量作用域
在 Java 中,Lambda 表达式中的变量作用域问题与常规的变量作用域有所不同。Lambda 表达式允许访问外部作用域的变量,但是有一些限制和注意事项:
-
访问外部变量:
- Lambda 表达式可以访问外部作用域中的 final 或 effectively final 变量(在 Lambda 表达式内部没有修改过的变量)。
- 在 Java 8 中,Lambda 表达式中可以访问外部作用域的 final 变量,而从 Java 10 开始,也可以访问 effectively final 变量,即一旦赋值后就不再被修改的变量。
int x = 10; int y = 20; y = 30; // y 不再是 effectively final 变量// Lambda 表达式访问外部变量 Runnable r = () -> {System.out.println(x); // 可以访问外部作用域的 final 或 effectively final 变量// System.out.println(y); // 编译错误,y 不是 effectively final 变量 };
-
变量捕获:
- Lambda 表达式内部的变量并不是新声明的变量,而是对外部作用域的变量的引用。Lambda 表达式会捕获这些变量,并且在 Lambda 表达式执行期间保持对这些变量的引用。
int z = 15; Runnable r = () -> {System.out.println(z); // Lambda 表达式捕获了外部作用域的变量 z };
-
变量修改和限制:
- Lambda 表达式中的变量必须是 final 或 effectively final,否则编译器会报错。
- 对于Lambda表达式中的局部变量,实际上它们在Lambda表达式内部是隐式的 final 变量。尝试在Lambda表达式内部修改这些局部变量会导致编译错误。
int count = 0; Runnable r = () -> {// count++; // 编译错误,Lambda 表达式尝试修改外部作用域的 count 变量 };
-
引用成员变量和静态变量:
- Lambda 表达式可以访问类的成员变量和静态变量,它们的作用域与 Lambda 表达式的生命周期相同。
public class MyClass {int instanceVar = 100;static int classVar = 50;public void myMethod() {Consumer<Integer> consumer = (num) -> {System.out.println(instanceVar); // 可以访问类的实例变量System.out.println(classVar); // 可以访问类的静态变量};} }
6.2.7 处理lambda表达式
使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个 lambda 表达式中。之所以希望以后再执行代码,有很多原因,如
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码(例如,排序中的比较操作)
- 发生某些情况时执行代码(如,点击了一个按钮,数据到达,等等)
- 只在必要时才运行代码
🐯 常用函数式接口
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runable | 无 | void | run | 作为无参数或返回值的动作运行 | |
Supplier<T> | 无 | T | get | 提供一个T类型的值 | |
Consumer<T> | T | void | accept | 处理一个T类型的值 | andThen |
BiConsumer<T> | T,U | void | accept | 处理T和U类型值 | andThen |
Function<T,R> | T | R | apply | 有一个T类型参数的函数 | compose, andThen, identity |
BiFunction<T,U,R> | T,U | R | apply | 有T和U类型参数的函数 | andThen |
UnaryOperator<T> | T | T | apply | 类型T上的一元操作符 | compose, andThen, identity |
BinaryOperator<T> | T,T | T | apply | 类型T上的二元操作符 | andThen, maxBy, minBy |
Predicate<T> | T | boolean | test | 布尔值函数 | and, or, negate, isEqual |
BiPredicate<T,U> | T,U | boolean | test | 有两个参数的布尔值函数 | and, or, negate |
🐯 基本类型的函数式接口
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 |
---|---|---|---|
BooleanSupplier | 无 | boolean | getAsBoolean |
PSupplier | 无 | p | getAs p |
PConsumer | p | void | accept |
Obj PConsumer<T> | T,p | void | accept |
PFunction<T> | p | T | apply |
PTo QFunction | p | q | applyAs Q |
To PFunction<T> | T | p | applyAs P |
To PBiFunction<T,U> | T,U | p | applyAs P |
PUnaryOperator | p | p | applyAs P |
PBinaryOperator | p ,p | p | applyAs P |
pPredicate | p | boolean | test |
注:p q 是 int、 long、double;P、Q 是 Int、Long、Double
大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如
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
页里会指出你的接口是一个函数式接口。 注解并不是必须要加的,推荐加。
6.2.8 再谈 Comparator
静态 comparing 方法取一个“键提取器”函数,它将类型 T
映射为一个可比较的类型(如String
)。对要比较的对象应用这个函数,然后对返回的键完成比较。
// 将 Person数组 按名字(String类型)进行排序
Arrays.sort(people, Comparator.comparing(Person::getName));
也有 thenComparing
方法,可根据结果再比较。
// 先比较姓,再比较名
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
也可为提取的键指定比较器(String类比较默认按字典序,这里可以设置为长度)
// 为提取的键指定比较器,按长度比较
Arrays.sort(people, Comparator.comparing(Person::getName, (s,t) -> Integer.compare(s.length(),t.length())));// 更简便的方式
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
如果键函数可能返回 null
,可能就要用到 nullsFirst 和 nullsLast 适配器。
-
如果属性值是 null 时使用 nullsFirst() 那么排序时默认为空的这个值就比别的大 ,那排序后就靠前;
-
如果属性值是 null 时使用 nullsLast() 那么排序时默认为空的这个值就比别的小 ,那排序后就靠后;
这些方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。
nullsFirst
方法需要一个比较器,在这里就是比较两个字符串的比较器。 naturalOrder
方法可以为任何实现了 Comparable
的类(实现了CompareTo
方法)建立一个比较器。只是形式上的简洁,也更推荐。
Arrays.sort(people, Comparator.comparing(Person::getMiddleName, Comparator.nullsFirst(Comparator.naturalOrder())));
// 等价于如下,但上面的更简洁
Arrays.sort(people, Comparator.comparing(Person::getMiddleName, Comparator.nullsFirst((s,t) -> s.compareTo(t))));
// tom为空,排在最前面,mary,dog中,ar在o字典序前面
System.out.println(Arrays.toString(people)); // tom,mary,dog
naturalOrder
即自然顺序,将所有 middleName
为 null
值的元素按照数组的顺序。
6.3 内部类
内部类是定义在另一个类中的类。使用内部类的原因主要有以下两点:
- 内部类可以对同一个包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据
一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段
举例
class TalkingClock {private int interval;private boolean beep;public TalkingClock(int interval, boolean beep){this.interval = interval;this.beep = beep;}public void start(){var listener = new TimePrinter();var timer = new Timer(interval,listener);timer.start();}public class TimePrinter implements ActionListener{@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen()));// 可以访问外围类的变量if(beep) Toolkit.getDefaultToolkit().beep();}}
}
外围类在内部类的引用在构造器中设置。编译器会修改所有内部类构造器,添加一个对于外围类引用的参数。例如本内部类默认生成的无参构造器
public TimePrinter(TalkingClock clock){outer = clock; // outer只是一个外围类一个形式化引用,即大致构造意思如此,实际构造方法并不是如此。
}
6.3.2 内部类的特殊语法规则
🐯 外围类的引用
OuterClass.this
外围类类名.this
if(TalkingClock.this.beep) // 上述例子的 beep 即该形式
🐯 内部类引用
// 在外围类内部使用内部类的构造器
var listener = this.new TimePrinter();
也可以通过显式地命名将外围类引用设置为它的对象。例如,由于 TimePrinter
是一个公共内部类,对于任意的语音时钟对象都可以构造一个 TimePrinter
var clock = new TalkingClock(1000,true);
// 外围类 内部类 对象.new 内部类
TalkingClock.TimePrinter listener = clock.new TimePrinter();
在外围类的作用域之外,可以这样引用内部类:OuterClass.InnerClass
// 外围类的作用域之外,InnerClassTest
public class InnerClassTest{public static void main(String[] args) {var clock = new TalkingClock(1000,true);// OuterClass.InnerClassTalkingClock.TimePrinter listener = clock.new TimePrinter();}
}
class TalkingClock {public class TimePrinter implements ActionListener{@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen()));if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();}}
}
内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。如果这个字段不是一个常量,就可能不唯一。
内部类不可能有static方法。Java语言规范对这个限制没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态字段和方法。显然,Java设计者认为相对于这种复杂性来说,它带来的好处有些得不偿失
6.5 代理
🖱 Java动态代理
🐯 动态代理
package com.lh.proxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;/*** 该代理模式即Java动态代理*/
public class ProxyTest {public static void main(String[] args){var elements = new Object[1000];for(int i = 0; i < elements.length; i++){Integer value = i + 1;// 由TraceHandler接手Integer类型的对象,负责作为中间对象var handler = new TraceHandler(value);// Proxy.newProxyInstance 生成代理对象Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ Comparable.class }, handler);// 数组中的所有元素都是代理类elements[i] = proxy;}Integer key = new Random().nextInt(elements.length) + 1;// 动态代理对象调用一个方法时,这个方法的调用就会被转发到实现 InvocationHandler 接口类的 invoke// 二分查找会用到代理对象的 compareTo 方法int result = Arrays.binarySearch(elements, key);if(result >= 0) System.out.println(elements[result]);// 所有代理类都要覆盖 Object类的toString、equals和hashCode方法。System.out.println(elements[0].equals(elements[1])); //1.equals(2.toString() 2) falseSystem.out.println(elements[0].hashCode()); // 1.hashCode() 1System.out.println(elements[0]); // 1.toString() 1}}/*** 代理对象对应的自定义 InvocationHandler*/
class TraceHandler implements InvocationHandler{/*** 代理类中的真实对象*/private Object target;public TraceHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 调用方法前的操作System.out.print(target);System.out.print("." + method.getName() + "(");if(args != null){for(int i = 0; i < args.length; i++){System.out.print(args[i]);if(i < args.length - 1) System.out.println(", ");}}System.out.println(")");// 调用方法并返回return method.invoke(target, args);}
}
7. 异常、断言和日志
7.1 处理错误
7.1.4 创建异常类
/*** 自定义异常类*/
public class IntegerException extends Exception{String message;public IntegerException(int m){message = "年龄" + m + "不合理";}public IntegerException(String gripe){super(gripe);}
}/*** 测试异常类的类*/
public class People {private int age = 1;public void setAge(int age) throws IntegerException {if(age >= 160 || age <= 0){// String 中为错误信息,方便调试throw new IntegerException("年龄不合理");}else{this.age = age;}}public int getAge(){System.out.println("年龄" + age + "合理");return age;}public static void main(String[] args) {People bao = new People();People hu = new People();try{bao.setAge(180);System.out.println(bao.getAge());} catch (IntegerException e) {System.out.println(e.toString());}try{hu.setAge(21);System.out.println(hu.getAge());} catch (IntegerException e) {System.out.println(e.toString());}}}
// 输出结果com.lh.exception.IntegerException: 年龄不合理
年龄21合理
21
也可以重写 toString()
方法,编写自己想要的提示格式
@Override
public String toString() {return "IntegerException{" +"message='" + message + '\'' +"} " + super.toString();
}
// 其中 super.toString() 格式是 类路径:
// com.lh.exception.IntegerException:
信息查看
try{bao.setAge(180);System.out.println(bao.getAge());
} catch (IntegerException e) {System.out.println(e.getCause());System.out.println(e.getMessage());System.out.println(e.getClass().getName());System.out.println(e.toString());
}// null
// age e.getMessage()
// com.lh.exception.IntegerException
// com.lh.exception.IntegerException: age
7.2 捕获异常
7.2.2 捕获多个异常
int a = 2;
int b = 0;
try{int c = a / b;
}catch (Exception e){System.out.println(e.getCause());System.out.println(e.getClass().getName());System.out.println(e.getMessage());System.out.println(e);
}// null
// java.lang.ArithmeticException
// / by zero
// java.lang.ArithmeticException: / by zero
7.5 日志
7.5.2 高级日志
public class LogJDKTest {private static Logger log = Logger.getLogger(LogJDKTest.class.toString());public static void main(String[] args) {// severe warning info config fine finer finest// INFO及更高级别的可以显示log.setLevel(Level.INFO);log.finest("finest");log.finer("finer");log.fine("fine");log.config("config");log.info("info");log.warning("warning");log.severe("severe");}
}
输出结果
🐯 Logger.getLogger(String)
参数为一个String类型(理解为起一个名字),如果是 com.lh
。那么可以理解为 com.lh.hu
是 com.lh
的子,如果 com.lh.hu
没设置日志级别,将继承 com.lh
的日志级别
🐯 修改默认级别
private static final Logger log1 = Logger.getLogger("com.lh");
private static final Logger log = Logger.getLogger("com.lh.hu");public static void main(String[] args) { 修改日志级别// 关闭系统配置log1.setUseParentHandlers(false);// 创建ConsoleHandlerConsoleHandler con = new ConsoleHandler();log1.addHandler(con);// 配置日志级别// severe warning info config fine finer finest// INFO及更高级别的可以显示log1.setLevel(Level.FINEST);con.setLevel(Level.FINEST);log.finest("finest");log.finer("finer");log.fine("fine");log.config("config");log.info("info");log.warning("warning");log.severe("severe");
}
修改 log1
的日志级别,可以影响到子 log
的日志级别
可以为一个 log
设置多个级别不同的 handler
private static final Logger log = Logger.getLogger("com.lh.hu");public static void main(String[] args) {// 关闭系统配置log.setUseParentHandlers(false);// 创建ConsoleHandlerConsoleHandler con = new ConsoleHandler();con.setLevel(Level.SEVERE);log.addHandler(con);ConsoleHandler con1 = new ConsoleHandler();con1.setLevel(Level.INFO);log.addHandler(con1);// 配置日志级别// severe warning info config fine finer finest// INFO及更高级别的可以显示log.setLevel(Level.FINEST); // 这个设置貌似没什么用log.finest("finest");log.finer("finer");log.fine("fine");log.config("config");log.info("info");log.warning("warning");log.severe("severe");
}
8. 泛型程序设计
8.4 类型变量的限定
&
表示这多个接口都要继承(如果有一个类必须实现)。
// T,U 作为两个类型变量
// 有多个限定时,最多有一个可以为类,但必须是限定列表中的第一个
public static <T extends Pair & Comparable, U extends Serializable> T min(T[] a){if(a == null || a.length == 0) return null;T smallest = a[0];for(int i = 1; i < a.length; i++)if(smallest.compareTo(a[i]) > 0) smallest = a[i];return smallest;
}
为了兼容,对于泛型对象如 new ArrayList<String>()
和 new ArrayList()
都是允许的,不同的是, new ArrayList()
无视泛型,类中的参数 T
都使用的默认的原始类型 Object
,而 new ArrayList<String>()
则再调用方法时,编译器直接可以推断出参数类型必须为 String
,其他类型的对象不可以,使用起来更安全。
8.5 泛型代码和虚拟机
8.5.3 转换泛型方法
🐯 桥方法解释
public class A<T> {public static void main(String[] args) {A<String> a = new A<>();B b = new B();}private T value;public T getValue(){return value;}public void setValue(T value){this.value = value;}
}class B extends A<String>{@Overridepublic void setValue(String value) {super.setValue(value);}@Overridepublic String getValue(){return value;}
}
在A类型擦除后,会生成 public void setValue(Object value)
的方法,B也会继承该方法。这个时候,A中会有两个方法,一个 public void setValue(String)
,一个 public void setValue(Object)
。但是我们既然在A中重写了,我希望调用时是B的 setValue
。所以擦写后的方法会被修改如下,成为桥方法。
// 桥方法
public void setValue(Object value){setValue((String)value);
}
同理,对于 getValue
方法,擦除后也有如下两种
String getValue();
Object getValue();
这种只有返回类型不同的方法是不能编写的,但是在虚拟机中是可以存在的。虚拟机可以正常处理。结果如下
String getValue(){return value;
}
Object getValue(){return getValue();
}
解释
- 方法签名确实是方法名 + 参数列表。
- JVM会用方法名、参数类型和返回类型来确定一个方法,所以针对方法前面相同的两个方法,返回值类型不相同的时候,JVM是可以分辨的。
桥方法的另一个场景在336页注释中
擦除,强制转换,桥方法
全部都是编译器帮我们做的类型擦除(P333 在下面的小节你会看到编译器如何擦除类型参数)、包括强制转换(P335编译器自动插入转换到Employee的强制类型转换)和桥方法生成(P336 编译器在DateInterval类中生成一个桥方法)。操作完之后给虚拟机,虚拟机直接就来用了。
8.6 限制与局限性
8.6.3 不能创建参数化类型的数组
var table = new Pair<String>[10]; // error java: 创建泛型数组
以下使用也会存在问题
@SafeVarargs
static <E> E[] array(E...array){return array;
}@SuppressWarnings("unchecked")
public static void main(String[] args) {// var table = new Pair<String>[10]; // java: 创建泛型数组Pair<String> pair1 = new Pair<>();Pair<String> pair2 = new Pair<>();// Pair<String>[] 的声明是合法的Pair<String>[] table = array(pair1,pair2);System.out.println(table[0].getClass()); // class com.lh.generic.Pair// 问题暴露// com.lh.generic.Pair 可为 ObjectObject[] objArray = table;System.out.println(objArray.getClass()); // class [Lcom.lh.generic.Pair;objArray[0] = new Pair<Employee>(); // 是可以的,因为数组是Pair类型,可以接受泛型参数为任意类型的PairPair<Employee> obj = (Pair<Employee>) objArray[0];obj.setFirst(new Employee()); // 设置一下 objArray[0] 的内容System.out.println(table[0].getFirst()); // ERROR: class com.lh.clone.Employee cannot be cast to class java.lang.String
}
8.6.4 Varargs警告
// Possible heap pollution from parameterized vararg type
// 参数化可变参数类型可能造成堆污染
@SafeVarargs // 加上这个警告消除
public static <T> void addAll(Collection<T> coll, T...ts){for (T t : ts) {coll.add(t);}
}public static void main(String[] args) {Collection<Pair<String>> table = new ArrayList<>();Pair<String> pair1 = new Pair<>();Pair<String> pair2 = new Pair<>();addAll(table,pair1,pair2);
}
8.6.6 不能构造泛型数组
T[] mm = new T[2]; // ERROR
有两种方法创建泛型数组
// 函数式接口的方法
public static <T extends Comparable> T[] minmax1(IntFunction<T[]> constr, T...a){// IntFunction<T[]> apply(int) 给定一个Int值,int值指示数组大小,返回一个T类型的数组T[] result = constr.apply(2);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];}result[0] = min; result[1] = max;return result;
}
// 反射方法
public static <T extends Comparable> T[] minmax2(T...a){var result = (T[]) Array.newInstance(a.getClass().getComponentType(),2);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];}result[0] = min; result[1] = max;return result;
}
public static void Test(){System.out.println(Arrays.toString(minmax1(String[]::new, "Tom", "Dick", "Harry")));System.out.println(Arrays.toString(minmax2("Tom", "Dick", "Harry")));// [Dick, Tom]
}
8.7 泛型的继承规则
class A<T> {protected T t;public T getT() {return t;}
}class B1 extends A {} // 子类无视泛型 , 导致泛型类型永远是Object, 类型模糊
class B2 extends A<String> {} // 子类直接把泛型父类的泛型类写死, 子类中继承的T类型永远是固定的
class B3 extends A<Boolean> {}
class C<T> extends A<T> {} // 子类也泛型, 在子类对象创建时再动态决定泛型类型, 这是最灵活的做法.
8.8 通配符类型
8.8.1 通配符概念
var managerBuddies = new Pair<Manager>(new Manager(),new Manager());
Pair<? extends Employee> wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(e); // ERROR
Employee first = wildcardBuddies.getFirst();
可以理解为:
? extends Employee 表示为 Employee 或 Employee 的子类型,可进行 get ,返回值可用 Employee 来接收,而在调用 set 方法时无法确定参数的具体类型(拥有多个子类型,不知道具体哪一个子类型)。如此意味着我们可以进行读取,但是不能写入。
9. 集合
9.1 Java集合框架
9.1.4 泛型实用方法
java.util.Collection
中的方法 <T> T[] toArray(T[] arrayToFill)
Collection<String> c = new ArrayList<>();
c.add("a");int stringSize = 0;
String[] a = new String[stringSize];
String[] array = c.toArray(a);
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(array));// stringSize == 0 时
[]
[a]
// stringSize == 1 时
[a]
[a]
// stringSize == 2 时
[a,null]
[a,null]
9.3 具体集合
9.3.1 链表
p380 有一个简单的方式可以检测到并发…段落解释
测试程序:
List<String> list = new ArrayList<>();
list.add("JavaScript");
list.add("Java");
list.add("Java");
list.add("HTML");
list.add("CSS");Iterator<String> it = list.iterator();
// 删除Java
while(it.hasNext()){String ele = it.next();if(ele.equals("Java")){// remove只删除一个元素list.remove(ele);System.out.println(Arrays.toString(list.toArray()));}
}
这里输出list时使用的是
System.out.println(Arrays.toString(list.toArray()))
其实直接System.out.println(Arrays.toString(list.toArray()))
即可,因为在AbstractCollection
中已经重写了toString()
方法
结果如下:
错误原因:
迭代器的更改操作数与集合的更改操作数不相等,所以认为不安全,抛出异常,但是书中一个地方并不准确,即每个迭代器方法的开始处去判断,其实这里抛出异常的地方并不是从 it.hasNext()
处,而是 it.next()
。
查看源码:
hasNext()
方法并没有检查,而 next()
有检查
所以我们将原本使用 list
删除改为使用迭代器删除就不会出现错误
while(it.hasNext()){String ele = it.next();if(ele.equals("Java")){// remove只删除一个元素it.remove();System.out.println(Arrays.toString(list.toArray()));}
}
// 输出结果:
// [JavaScript, Java, HTML, CSS]
// [JavaScript, HTML, CSS]
9.3.4 树集
树集中的迭代是按照排序后的有序顺序来访问元素。
var parts = new TreeSet<Item>();
parts.add(new Item("Toaster", 1234));
parts.add(new Item("Widget", 4562));
parts.add(new Item("Modem", 9912));for (Item part : parts) {System.out.println(part);
}
// Item{88description='Modem', partNumber=9912}
// Item{description='Widget', partNumber=4562}
// Item{description='Toaster', partNumber=1234}
9.3.6 优先队列
优先队列的迭代不是按有序顺序访问的,因为它是基于堆的,每次只知道最小的值,只有最小值被移除后,才知道下一个最小值。但是如果只迭代是无法知道顺序的。
var pq = new PriorityQueue<LocalDate>();
pq.add(LocalDate.of(1906,12,9));
pq.add(LocalDate.of(1815,12,10));
pq.add(LocalDate.of(1903,12,3));
pq.add(LocalDate.of(1910,6,22));System.out.println("Iterating over elements . . .");
for (LocalDate date : pq) {System.out.println(date);
}
System.out.println("Removing elements . . .");
while(!pq.isEmpty()){System.out.println(pq.remove());
}// Iterating over elements . . .
1815-12-10
1906-12-09
1903-12-03
1910-06-22
// Removing elements . . .
1815-12-10
1903-12-03
1906-12-09
1910-06-22
9.4 映射
9.4.6 枚举集与映射
EnumMap
//初始化直接放入键值
Map<Weekday,String> map = new EnumMap<>(Weekday.class){{put(Weekday.MONDAY,"星期一");put(Weekday.TUESDAY,"星期二");put(Weekday.WEDNESDAY,"星期三");put(Weekday.THURSDAY,"星期四");put(Weekday.FRIDAY,"星期五");put(Weekday.SATURDAY,"星期六");put(Weekday.SUNDAY,"星期日");
}};
// 方式二
//先创建枚举映射
Map<Weekday,String> map = new EnumMap<>(Weekday.class);
//后放入键值
map.put(Weekday.MONDAY,"星期一");
map.put(Weekday.TUESDAY,"星期二");
map.put(Weekday.WEDNESDAY,"星期三");
map.put(Weekday.THURSDAY,"星期四");
map.put(Weekday.FRIDAY,"星期五");
map.put(Weekday.SATURDAY,"星期六");
map.put(Weekday.SUNDAY,"星期日");
双层花括号的意义?
- 外层花括号实际是定义了一个匿名内部类 (Anonymous Inner Class)。
- 内层花括号实际上是一个实例初始化块 ,这个块在内部匿名类构造时被执行。
使用场景:可以动态构造或修改枚举集。
9.5 视图与包装器
9.5.1 小集合
Map<String, Integer> peter = Map.ofEntries(Map.entry("Peter", 2));
Integer dog1 = peter.put("dog", 2); // UnsupportedOperationException
// Java中用来替代 Pair
Map.Entry<String, Integer> dog = Map.entry("Dog", 2);
9.5.5 检查型视图
var strings = new ArrayList<String>();
ArrayList rawList = strings;
// rawList没有指定类型,所以可以插入任意类型的对象,只有运行起来的时候才会发现强制转换时出现错误
rawList.add(new Date());
System.out.println(strings.get(0));
// class java.util.Date cannot be cast to class java.lang.StringList<String> safeStrings = Collections.checkedList(strings,String.class);
safeStrings.add(new Date()); // ERROR: Required type:String Provided: Date
9.6 算法
9.6.6 集合与数组的转化
🐯 集合变数组
var strings = new HashSet<String>();
strings.add("二狗");
strings.add("是");
strings.add("我弟");
// java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
// String[] array = (String[])strings.toArray();
String[] a = new String[0];
String[] b = new String[3];
// 参数为 new String[0] 观察 a 与 arrayA 的关系
String[] arrayA = strings.toArray(a);
System.out.println(Arrays.toString(arrayA) + " " + arrayA.length + " " + (arrayA == a));
// 参数为 new String[3] 观察 b 与 arrayB 的关系
String[] arrayB = strings.toArray(b);
System.out.println(Arrays.toString(arrayB) + " " + arrayB.length + " " + (arrayB == b));System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
结果输出:
[二狗, 我弟, 是] 3 false
[二狗, 我弟, 是] 3 true
[]
[二狗, 我弟, 是]
我们可以观察源码,当传入的参数数组的大小小于集合的大小时,函数会创建新创建一个数组并返回它,并且 a[]
保持不变。如果传入的参数数组大小合适,则会复制到该数组并返回,就不需要额外创建了。
9.7.3 映射
String userDir = System.getProperty("user.home");
String javaHome = System.getProperty("java.home");
String javaVersion = System.getProperty("java.version");
System.out.println(userDir + " " + javaHome + " " + javaVersion);
// C:\Users\16331 D:\configuration\Java\jdk-11.0.21 11.0.21
9.7.5 位集
Java使用埃式筛法实现查找素数
public static BitSet getPrim(int n){var bitSet = new BitSet(n + 1);int count = 0;int i;for(i = 2; i <= n; i++)bitSet.set(i);i = 2;while(i * i <= n){if(bitSet.get(i)){count++;int k = 2 * i;while(k <= n){bitSet.clear(k);k += i;}}i++;}while(i <= n){if(bitSet.get(i))count++;i++;}return bitSet;
}
测试
System.out.println(getPrim(100));
// {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}
BitSet
重写了 toString()
,对位上为开状态的将输出下标。
BitSet
内部函数测试:
BitSet prim = getPrim(100);// and
BitSet bsAnd = new BitSet(100 + 1);
bsAnd.and(prim);
// 默认全为0,即关状态
System.out.println(bsAnd); // {}// or
BitSet bsOr = new BitSet(100 + 1);
bsOr.or(prim);
System.out.println(bsOr); // {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}// xor
BitSet bsXOr = new BitSet(100 + 1);
bsXOr.xor(prim);
System.out.println(bsXOr); // {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}// andNot 获取 0 ~ 100的非素数
BitSet bsAndNot = new BitSet(100 + 1);
for(int i = 0; i <= 100; i++)bsAndNot.set(i);
bsAndNot.andNot(prim);
System.out.println(bsAndNot);
// {0, 1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25, 26, 27, 28, 30, 32, 33, 34, 35, 36, 38, 39, 40, 42, 44, 45, 46, 48, 49, 50, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 65, 66, 68, 69, 70, 72, 74, 75, 76, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 96, 98, 99, 100}
12. 并发
12.1 什么是线程
bank
类的定义
/*** A bank with a number of bank accounts.*/
public class Bank
{private final double[] accounts;/*** Constructs the bank.* @param n the number of accounts* @param initialBalance the initial balance for each account*/public Bank(int n, double initialBalance){accounts = new double[n];Arrays.fill(accounts, initialBalance);}/*** Transfers money from one account to another.* @param from the account to transfer from* @param to the account to transfer to* @param amount the amount to transfer*/public void transfer(int from, int to, double amount){if (accounts[from] < amount) return;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());}/*** Gets the sum of all account balances.* @return the total balance*/public double getTotalBalance(){double sum = 0;for (double a : accounts)sum += a;return sum;}/*** Gets the number of accounts in the bank.* @return the number of accounts*/public int size(){return accounts.length;}
}
12.3 线程属性
12.3.1 中断线程
如果线程被阻塞,就无法检查中断状态。当在一个被
sleep
或wait
调用阻塞的线程上调用interrupt
方法时,那个阻塞调用将被一个InterruptedException
异常中断
🐯测试代码 main
:
final Thread thread = new Thread(() -> {try {// System.out.println(Thread.currentThread()); 一个普通的线程System.out.println("线程开始执行...");Thread.sleep(5000); // 休眠5sSystem.out.println("线程继续执行...");} catch (InterruptedException e) {System.out.println("线程被中断...");}
});
// 启动线程
thread.start();try{// System.out.println(Thread.currentThread()); main 线程System.out.println("睡两秒...");Thread.sleep(2000);System.out.println("睡醒了...");
} catch (InterruptedException e) {e.printStackTrace();
}System.out.println("尝试中断线程...");
thread.interrupt(); // 中断线程
System.out.println("主线程结束...");
🐯 运行结果:
睡两秒...
线程开始执行...
睡醒了...
尝试中断线程...
主线程结束...
线程被中断...
🐯 提示:主函数本身就是一个线程,为 main
线程。
🐯 分析:
主程序开始执行,先启动 thread
线程,thread
线程启动过程中,主线程继续向下执行,输出 睡两秒...
然后此时主线程要阻塞两秒,这个时候,thread
启动了,然后输出 线程开始执行...
,之后要睡 5 秒,那么主线程就会在这 5 秒重新进入可运行状态,然后输出 睡醒了...
,然后继续向下执行,输出 尝试中断线程...
。由于此时 thread
线程的 5 秒还没有结束,所以 thread
线程还处于阻塞态,此时被调用了中断,所以会触发异常。此时主程序会继续执行,然后处理异常。
🐯 问题抛出
- 为什么先输出
睡两秒...
而不是线程开始执行
? - 我们中断线程后为什么不是先输出
线程被中断
,再执行主线程结束
- 触发异常后,线程是先被回收还是先处理异常
🐯 问题解决
-
测试程序如下:
将睡两秒这句话多输出几遍,观察效果for(int i = 0; i < 100; i++)System.out.println("睡两秒...");
差不多在输出80多次的睡两秒之后输出了线程开始执行。我们知道,输出消耗的时间其实很少。在这里,其实也验证了
main
程序在启动一个线程后,并不会影响自身的运行,而线程的启动也不是瞬间启动的,也需要一段时间。 -
测试程序如下:
将主线程结束这句话多输出几遍,观察效果for(int i = 0; i < 100; i++)System.out.println("主线程结束...");
差不多输出30多次主线程结束后就输出了线程被中断。原因应该是和第一个差不多。
-
在触发异常后,异常会先向上抛出,被捕获到后就会进行处理,处理结束后(一般手动,在
finally
)回收资源,否则是不会自动回收的。
测试程序如下:在捕获异常的地方对线程进行一些操作(测试资源还未被回收)。
System.out.println(Thread.currentThread().getName()); // Thread-0 System.out.println(Thread.currentThread().isInterrupted()); // false Thread.currentThread().interrupt(); System.out.println(Thread.currentThread().isInterrupted()); // true System.out.println("线程被中断...");
还能输出信息,改变状态等。
如何回收线程?
-
手动线程中断:线程中断是一种机制,当线程被中断时,它并不会自动终止或释放资源。需要程序员在适当的时机手动处理中断状态,做出相应的清理工作并结束线程执行,以便释放线程所占用的资源。
测试程序如下:final Thread thread = new Thread(() -> {try {Thread.sleep(5000); // 休眠5s} catch (InterruptedException e) {System.out.println(Thread.currentThread().getState()); // RUNABLESystem.out.println("线程被中断..."); // 没有手动中断程序} }); // 启动线程 thread.start();try{Thread.sleep(2000); } catch (InterruptedException e) {e.printStackTrace(); }System.out.println("尝试中断线程..."); thread.interrupt(); // 中断线程Thread.sleep(5_000); System.out.println(thread.getState()); // TERMINATED
解释:在给定的代码中,当线程被中断后,最后输出线程的状态将是
TERMINATED
。原因是线程在休眠期间(Thread.sleep(5000)
),它被中断(调用了thread.interrupt()
方法),这时会抛出InterruptedException
并进入catch
块处理异常。在
catch
块中,虽然没有手动中断线程(没有显式调用return
或break
等语句),但是在捕获到InterruptedException
后,线程继续执行catch
块内的代码。因此,最后在catch
块中输出线程状态时,线程处于RUNNABLE
状态,这是因为catch
块仍在执行,尚未结束。但在整个代码执行完成后,主线程会继续执行。最后的
Thread.sleep(5_000);
会使主线程休眠 5 秒钟。然后,当主线程执行完成后,整个程序终止,同时 Java 虚拟机也会回收已经结束的线程,这时候线程的状态会变为TERMINATED
。因此,最后输出线程状态时,会输出
TERMINATED
,这表示线程已经完全结束并被回收了。 -
自然结束:线程自然终止后,它所占用的资源将会被回收。这时线程将会进入终止状态,不再继续执行。
Runnable r = () -> {System.out.println("r 线程执行结束..."); }; Thread rThread = new Thread(r); rThread.start(); rThread.join(); System.out.println(rThread.getState()); // TERMINATED
线程结束后,是所有信息完全没有了吗?
不是!!
当线程执行结束后,一些信息仍然可以被访问,而另一些信息则可能不再可用或已经不再具有意义。以下是线程执行结束后可以访问和获取的一些信息:
可以访问的信息:
- 线程名称(Name): 即使线程执行结束,可以使用
getName()
方法获取线程的名称。 - 线程状态(State): 线程状态是枚举类型
Thread.State
的一个实例,表示线程的状态。在线程结束后,状态通常会变为TERMINATED
,可以使用getState()
方法获取线程状态。
不再可用或没有意义的信息:
- 存储在线程栈中的局部变量: 当线程执行结束后,线程的栈帧和局部变量表等信息会被释放,因此线程栈中的局部变量等信息不再可用。
- 线程执行期间产生的临时变量或对象引用: 在线程执行期间创建的对象引用、临时变量等,如果没有被其他引用持有,则在线程结束后会被 JVM 的垃圾回收器回收。
12.3.4 未捕获异常的处理器
解释:线程的
run
方法不能抛出任何检查型异常
Runnable
接口中的 run()
方法不允许抛出任何已检查异常(Checked Exception),这是因为 Runnable
接口的 run()
方法签名是不允许抛出已检查异常的。
Runnable
接口中的 run()
方法定义如下:
void run();
这意味着 run()
方法是一个没有声明任何异常的方法。如果在实现 Runnable
接口时,要在 run()
方法中抛出已检查异常,编译器会报错,因为接口的方法中不允许有已检查异常的声明。
这样的设计是为了确保在多线程环境下,线程的运行不会受到已检查异常的影响。如果 run()
方法允许抛出已检查异常,就需要在每次使用 Runnable
的地方捕获这些异常,这对于多线程场景来说会增加代码的复杂性和错误处理的困难。
相反,如果在 run()
方法中有异常需要处理,可以在 run()
方法内部使用 try-catch
块来捕获异常,并进行相应的处理,或者将异常转换为非检查异常(Unchecked Exception)进行抛出。
卷II
1. Java8的流库
1.1 从迭代到流的操作
List<String> words = List.of(contents.split("\\PL+"));
举例:java.txt
Streams do not store their elements. These elements may be stored in an underlying collection or generated on demand.
分割后:
[Streams, do, not, store, their, elements, These, elements, may, be, stored, in, an, underlying, collection, or, generated, on, demand]
官方解释:
\\PL+
意思是任意多的非字母组成的内容,即将内容以非字母分割,得到的结果就是一个个单词。\\PL
是指非字母分割,但是仅仅只是一个非字母(会出现分割错误,如在 elements
文本后有一个 .
和一个空格两个非字母 )。+
代表1到多个。 \\pL
是指单个字母。(这里的字母是指Unicode集中的字符,官网也有解释)
初次之外,还有其他一些用法:
官方链接
1.2 流的创建
P5 警告部分
分析如下代码错误原因:
List<String> wordList = new ArrayList<>();
wordList.add("a");
wordList.add("b");
Stream<String> words = wordList.stream();words.forEach(s -> {if(s.length() < 12) // ERROR: java.lang.NullPointerExceptionwordList.remove(s);
});
错误的根源是在使用 wordList.remove(s)
时,尝试删除元素时发生了问题。wordList.remove(s)
这个操作实际上会在迭代中修改集合,在多线程或者并发操作中可能会导致 ConcurrentModificationException
。但是,实际上在这种情况下,它会引发 java.lang.NullPointerException
,因为在 lambda 表达式中,存在一个隐藏的问题。
当你在 forEach
循环中使用 wordList.remove(s)
时,如果 s.length() < 12
不成立,wordList.remove(s)
操作就不会执行。这意味着 s
的值仍然会在 wordList
中存在,但在某些情况下,lambda 表达式可能会将 s
设置为 null
。当 s
是 null
时,s.length()
就会触发 NullPointerException
。
那为什么会出现
s
为null
的情况呢?
在Java中,Lambda表达式中使用的外部变量必须是最终(final)或事实上最终的(effectively final)。
在这种情况下,当在lambda表达式中使用了非最终的或非事实上最终的变量(即非final或者非effectively final的变量),它必须是“等效最终的”,即不能在lambda表达式内部被修改。
在这个例子中,虽然 s
是一个局部变量,但它被用在了 lambda 表达式内部,并且 s
可能被更改(在 wordList.remove(s)
这个语句中),这与 Lambda 表达式的要求相冲突。虽然 s
的作用域只在 lambda 表达式中,但由于其值在 lambda 中被修改,这可能导致一些不可预测的结果。
在某些情况下,Java编译器会强制要求在lambda表达式中使用的变量必须是事实上最终的。这可能是因为编译器将变量 s
视为事实上的最终变量,所以在运行时不允许对它进行修改。这可能导致 s
在某些条件下成为 null
,从而引发 NullPointerException
。
使用迭代器的方式可以避免这种情况,因为在使用迭代器时,直接操作的是迭代器本身,而不是集合内部的变量。这样就不会存在直接操作变量并导致非预期行为的情况。
Iterator<String> iterator = wordList.iterator();
while (iterator.hasNext()) {String s = iterator.next();if (s.length() < 12) {iterator.remove(); // 使用迭代器安全删除元素}
} // 第九章集合中说了为什么使用 wordList 删除会出现错误。而迭代器不会
示例代码运行结果:
words: Streams, do, not, store, their, elements, These, elements, may, be, ...
song: gently, down, the, stream
silence:
echos: Echo, Echo, Echo, Echo, Echo, Echo, Echo, Echo, Echo, Echo, ...
randoms: 0.15857816475249664, 0.04915017647536335, 0.28835609579397903, 0.6797328166343386, 0.7411366892152276, 0.21259115522182692, 0.8276498659671652, 0.6643921791539643, 0.5221171259974529, 0.6811019944501525, ...
integers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
wordsAnotherWay: Streams, do, not, store, their, elements, These, elements, may, be, ...
lines: Streams do not store their elements. These elements may be stored in an underlying collection or generated on demand.
rootDirectories: C:\, D:\
pathComponents: usr, share, dict, words
1.3 filter、map和flatMap方法
测试:
public static void filterTest(){List<String> words = List.of("2","dog","is","big","sb");Stream<String> longWords = words.stream().filter(w -> w.length() > 2);longWords.iterator().forEachRemaining(System.out::print);System.out.println();
}public static void mapTest(){List<String> words = List.of("2","dog","is","big","sb");Stream<String> uppercaseWords = words.stream().map(String::toUpperCase);uppercaseWords.iterator().forEachRemaining(System.out::print);System.out.println();
}public static Stream<String> codePoints(String s){var result = new ArrayList<String>();int i = 0;while(i < s.length()){// 对于Unicode集应基于代码点而不是字节或字符来操作字符串的位置int j = s.offsetByCodePoints(i, 1);result.add(s.substring(i, j));i = j;}return result.stream();
}public static void flatMapTest(){List<String> words = List.of("2","dog","is","big","sb");Stream<String> flatMapResult = words.stream().flatMap(StreamMethodsTest::codePoints);flatMapResult.iterator().forEachRemaining(System.out::print);System.out.println();Stream<Stream<String>> mapResult = words.stream().map(StreamMethodsTest::codePoints);// 需要双层才能输出完整mapResult.iterator().forEachRemaining(x -> x.iterator().forEachRemaining(System.out::print));System.out.println();
}public static void main(String[] args) {filterTest();mapTest();flatMapTest();
}
结果:
dogbig
2DOGISBIGSB
2dogisbigsb
2dogisbigsb
小结论:
- 可以获取流的迭代器
iterator()
对内容依次输出。 - 也可以使用
stream.collect(Collectors.toList())
将流转化为List
Stream
流继承了AutoCloseable
接口,会自动关闭哦(try-with-resource也会自动关闭继承了AutoCloseable
的资源)。
1.4 抽取子流和组合流
limit方法
测试:
public static void limitTest(){Stream<Double> randoms = Stream.generate(Math::random).limit(4).map(x ->Double.parseDouble(Double.toString(x).substring(0,4)));System.out.println(randoms.collect(Collectors.toList()));
}
结果:
[0.09, 0.88, 0.95, 0.66]
skip方法
测试:
public static void skipTest(){// skip是丢弃而不是跳过Stream<Double> randoms = Stream.generate(Math::random).limit(4).skip(1).map(x ->Double.parseDouble(Double.toString(x).substring(0,4)));System.out.println(randoms.collect(Collectors.toList()));
}
结果:
[0.86, 0.13, 0.82] // 输出结果只有三个,说明第一个是丢弃了,而不是跳过map处理
takeWhile()方法
takeWhile
方法的作用类似于对流进行过滤操作,但不同之处在于它在遇到第一个不满足条件的元素时就停止了,不会继续检查后面的元素。
举个例子,假设我们有一个包含整数的流,我们希望获取流中小于 5 的元素,一旦遇到第一个大于或等于 5 的元素,就停止获取。
import java.util.stream.Stream;public class TakeWhileExample {public static void main(String[] args) {Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);Stream<Integer> result = stream.takeWhile(x -> x < 5);result.forEach(System.out::println); // 输出结果为 1, 2, 3, 4}
}
在上面的例子中,我们创建了一个包含整数的流,然后使用 takeWhile
方法获取小于 5 的元素。takeWhile(x -> x < 5)
表达式表示只要元素的值小于 5,就将其包含在新的流中。当遇到第一个大于或等于 5 的元素(这里是 5),takeWhile
方法就停止了,不再获取后续的元素。
这样,takeWhile
方法提供了一种便捷的方式来获取流中满足特定条件的元素,并且在遇到第一个不满足条件的元素时停止处理,避免了对整个流进行过滤操作的性能开销。
dropWhile方法
测试:
// 去除了第一个空格,第二个为假,不再继续
// Stream<String> initialDigits = codePoints(" 12 ql sfj!?").dropWhile(s -> s.trim().isEmpty());
Stream<String> initialDigits = codePoints(" 12 ql sfj!?").dropWhile(s -> {System.out.println(",,,");return s.trim().isEmpty();
});
System.out.println(initialDigits.collect(Collectors.toList()));
结果:
,,, // 第一次匹配空格时输出
,,, // 第二次需要判断1是否满足条件,也会输出一次,之后就不会再有了
[1, 2, , q, l, , s, f, j, !, ?]
concat方法
测试:
public static void concatTest(){Stream<String> concat = Stream.concat(codePoints("Hello"),codePoints("Dog"));System.out.println(concat.collect(Collectors.toList()));
}
结果:
[H, e, l, l, o, D, o, g]
1.5 其他流的转换
peek方法
验证惰性处理
var array = Stream.iterate(1.0, p -> p * 2).peek(e -> System.out.println("Fetching " + e)).limit(20);
此时是没有输出的
var array = Stream.iterate(1.0, p -> p * 2).peek(e -> System.out.println("Fetching " + e)).limit(20).toArray();
这个时候就会有输出。
Fetching 1.0
Fetching 2.0
Fetching 4.0
Fetching 8.0
......
peek,limit都是中间操作,不会触发最终结果。
🐯 流的中间操作包括:
- map: 将流中的每个元素映射为另一个元素。
- filter: 根据指定条件过滤流中的元素。
- distinct: 去除流中的重复元素。
- sorted: 对流中的元素进行排序。
- limit: 截断流,限制流中元素的数量。
- skip: 跳过指定数量的元素,返回剩余的元素组成的新流。
- flatMap: 将流中的每个元素转换为流,然后将这些流合并为一个流。
- peek: 对流中的每个元素执行操作,同时保留流的原始结构。
而 toArray 会终止流的操作并处理其中的元素(将之前的中间操作一并处理,流会使用多线程处理,并将结果合并)
🐯 流的终止操作包括:
- forEach: 对流中的每个元素执行指定的操作。
- toArray: 将流中的元素收集到数组中。
- reduce: 对流中的元素进行归约操作,生成一个结果。
- collect: 将流中的元素收集到集合或其他数据结构中。
- count: 计算流中的元素数量。
- min / max: 找到流中的最小值或最大值。
- anyMatch / allMatch / noneMatch: 检查流中是否存在满足条件的元素。
- findFirst / findAny: 获取流中的第一个元素或任意元素。
- toArray: 将流中的元素收集到数组中。
- forEachOrdered: 保证元素以流中的顺序被处理,尤其在并行流中有用。
- toList / toSet / toMap: 将流中的元素收集到 List、Set 或 Map 中。
1.7 Optional类型
可以回顾 lambda
表达式中介绍的函数接口以及三种形式的方法引用,本章节会多次出现,注意理解。
Optional在本章中也被作者在一些地方成为 可选值,Optional类型的变量都是具有 0 个或 1 个值。
1.7.4 不适合使用 Optional 值的方式
P15 正确用法的提示的解释
- Optional设计目的是作为方法返回类型来表示可能为
null
的值。如果该类型的变量让其结果为null
,那这个类的设计将毫无意义。 - 不要使用Optional作为类字段或属性。比如 我本来要使用
String
,包装后变为了Optional<String>
。多了一层包装对象,无意义且会使代码更复杂。
总结:避免将 Optional
类型用于类的字段或属性,而是将其保留用于方法返回值,以帮助更清晰地表达可能为空的情况。
1.7.6 用flatMap构建Optional值的函数
测试程序:
public static Optional<Double> inverse(Double x){return x == 0 ? Optional.empty() : Optional.of(1 / x);
}public static Optional<Double> squareRoot(Double x){System.out.println(x);return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}public static void main(String[] args) {Optional<Double> v = inverse(0d).flatMap(CreateOptionalTest::squareRoot);Optional<Double> v2 = inverse(-4.0).flatMap(CreateOptionalTest::squareRoot);
}
分析两个变量的取值过程
- 传入了参数
0d
,所以在inverse(Double)
方法中返回了Optional.empty()
。则不会再调用后面的flatMap
方法,所以squareRoot
是不会执行的,结果v
为Optional.empty
- 传入了参数
-4.0
,所以inverse(Double)
正常执行,返回的Optional<Double>
包装了值为-0.25
的Double
类,值不为空,所以调用了flatMap
方法,执行了squareRoot
方法,但由于值小于0,v2
结果为Optional.empty
。
1.7.7 将Optional转换为流
User类:
public class User {private String id;public User(String id) {this.id = id;}// 假设 id 为2的User不存在,使用该方法代替数据库查询public static Optional<User> lookup(String id){if(id.equals("2"))return Optional.empty();return Optional.of(new User(id));}public static User classicLookup(String id){if(id.equals("2"))return null;return new User(id);}@Overridepublic String toString() {return "User{" +"id='" + id + '\'' +'}';}
}
测试程序:
Stream<String> ids = Stream.of("1","2","3");Stream<User> users = ids.map(User::lookup) // Stream<Optional<User>>.filter(Optional::isPresent) // Stream<Optional<User>>.map(Optional::get); // Stream<User>
System.out.println(Arrays.toString(users.toArray()));// 上面的流 ids 已经使用过并关闭了,下面如果要再使用就要重新赋值
ids = Stream.of("1","2","3");
Stream<User> user2 = ids.map(User::lookup) // Stream<Optional<User>>.flatMap(Optional::stream); // 流中的flatMap
System.out.println(Arrays.toString(user2.toArray()));
// flatMap对每个 Optional<User> 执行 stream 方法转为流并合并,空的Optional会转为大小为0的流,就相当于没有// 传统方法
ids = Stream.of("1","2","3");
Stream<User> users3 = ids.map(User::classicLookup).filter(Objects::nonNull);
System.out.println(Arrays.toString(users3.toArray()));ids = Stream.of("1","2","3");
Stream<User> users4 = ids.flatMap(id -> Stream.ofNullable(User.classicLookup(id)));
System.out.println(Arrays.toString(users4.toArray()));ids = Stream.of("1","2","3");
Stream<User> users5 = ids.map(User::classicLookup).flatMap(Stream::ofNullable);
System.out.println(Arrays.toString(users5.toArray()));
输出结果:
[User{id='1'}, User{id='3'}]
[User{id='1'}, User{id='3'}]
[User{id='1'}, User{id='3'}]
[User{id='1'}, User{id='3'}]
[User{id='1'}, User{id='3'}]
1.8 收集结果
iterator
Iterator<Integer> iter = Stream.iterate(0, n -> n + 1).limit(10).iterator();
while(iter.hasNext()){System.out.print(iter.next());
}
// 0123456789
toArray
情形一:获取 Object[]
数组
Object[] numbers = Stream.iterate(0, n -> n + 1).limit(10).toArray();
System.out.println("Object array:" + Arrays.toString(numbers));
// Object array:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
情形二:对获得的数组中单个元素可以强制转换,但是整个数组是无法强制转换的(Integer[]
可转为 Object[]
,反过来不行哦)
try{var number = (Integer) numbers[0];System.out.println("number: " + number);System.out.println("The following statement throws an exception:");Integer[] numbers2 = (Integer[]) numbers; // ERROR
}catch (ClassCastException e){System.out.println(e);
}
也可以使用如下方式进行数组转换
Integer[] numbers2 = Arrays.copyOf(numbers, numbers.length, Integer[].class);
情形三:指定构造器
// toArray(Constructor)
Integer[] numbers3 = Stream.iterate(0, n -> n + 1).limit(10).toArray(Integer[]::new);
System.out.println("Integer array: " + Arrays.toString(numbers3));
collect
// Set
Set<String> noVowelSet = noVowels().collect(Collectors.toSet());
show("noVowelSet", noVowelSet);// 结果
// noVowelSet:java.util.HashSet[b, d, gnrtd, strd, nt, lmnts, ndrlyng, my, n, cllctn]
方法定义
public static Stream<String> noVowels() throws IOException {// 该方法可以代替书上读取后转为字符串的操作var contents = Files.readString(Paths.get("java.txt"));List<String> wordList = List.of(contents.split("\\PL+"));Stream<String> words = wordList.stream();// 正则表达式进行字符串的处理return words.map(s -> s.replaceAll("[aeiouAEIOU]",""));
}public static <T> void show(String label, Set<T> set){System.out.print(label + ":" + set.getClass().getName());System.out.println("["+ set.stream().limit(10).map(Object::toString).collect(Collectors.joining(", "))+ "]");
}
joining
// joining()
String result = noVowels().limit(10).collect(Collectors.joining());
System.out.println("Joining: " + result);
result = noVowels().limit(10).collect(Collectors.joining(", "));
System.out.println("Joining with commas: " + result);
结果:
Joining: StrmsdntstrthrlmntsThslmntsmyb
Joining with commas: Strms, d, nt, str, thr, lmnts, Ths, lmnts, my, b
Collections.summarizing(Int|Double|Long)
//Collectors.summarizingInt
IntSummaryStatistics summary = noVowels().collect(Collectors.summarizingInt(String::length));
double averageWordLength = summary.getAverage();
int maxWordLength = summary.getMax();
System.out.println("Average word length: " + averageWordLength);
System.out.println("Max word length: " + maxWordLength);// 结果Average word length: 3.1578947368421053
Max word length: 7
forEach
// forEach()
noVowels().limit(10).forEach(System.out::print);// 结果
StrmsdntstrthrlmntsThslmntsmyb
1.9 收集到映射表中
Locale[] availableLocales = Locale.getAvailableLocales(); // 一个数组
System.out.println("[1]" + availableLocales[1].getDisplayCountry() + availableLocales[1].getDisplayLanguage()); // nn 第一个代表挪威,第二个代表尼诺斯克语
// [1]挪威 尼诺斯克语
Collections.singleton
Collections.singleton()
是 Java 中 java.util.Collections
类的静态方法之一。它返回一个只包含单个指定元素的不可变集合,这个集合无法修改(不可添加、删除或修改元素)。
1.10 约简操作
reduce(BinaryOperator)
List<Integer> values = List.of(1,2,3,4);
Optional<Integer> sum = values.stream().reduce((x,y) -> {System.out.println("x=" + x + " y=" + y); return x + y;});
System.out.println(sum.orElse(0));
结果:
x=1 y=2
x=3 y=3
x=6 y=4
10
T reduce(T identity, BinaryOperator<T> accumulator)
等价于如下程序(注意泛型类型)
T result = identity;
for (T element : this stream) result = accumulator.apply(result, element)
return result;
测试程序1:计算整数和
List<Integer> values = List.of(1,2,3,4);
Integer sum = values.stream().reduce(0, (x, y) -> x + y);
System.out.println(sum); // 10
测试程序2:计算字符串长度和(这里由于 BinaryOperator
函数接口为 (T,T)->T
类型,所以只得是如下形式,很不理想)
List<String> strings = List.of("dog","tow","2");
// String String String String
String sumLength = strings.stream().reduce("0", (total, word) -> String.valueOf(Integer.parseInt(total) + word.length()));
System.out.println(sumLength); // 返回类型是String,因为集合中的数据类型为String。
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
等价如下程序
R result = supplier.get();
for (T element : this stream) accumulator.accept(result, element);
return result;
测试程序:
strings = List.of("dog", "tow", "2");
// Integer Integer String Integer
Integer sumLength2 = strings.stream().reduce(0, (total, word) -> total + word.length(), Integer::sum);
// U 为Intger,T为String
2. 输入与输出
2.1 输入/输出流
2.1.1 读写字节
read
和write
方法在执行时都将阻塞
比如我要读写一个网络文件,但是网速较慢,信息还没到,那么当前读线程就会阻塞起来,让出来CPU。直到信息到达,该线程得到CPU,read
就会读取信息。
available
使用举例
import java.io.*;public class InputStreamExample {public static void main(String[] args) {try {FileInputStream fileInputStream = new FileInputStream("example.txt");// 检查可读取的字节数量int availableBytes = fileInputStream.available();System.out.println("可读取的字节数量为: " + availableBytes);// 如果有数据可读取,则读取数据if (availableBytes > 0) {byte[] buffer = new byte[availableBytes];int bytesRead = fileInputStream.read(buffer);System.out.println("读取到的内容为: " + new String(buffer, 0, bytesRead));} else {System.out.println("没有可读取的数据.");}fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}
}
有多少读多少,read
方法不会被阻塞。