# 3.3.1 - 无符号数的应用场景
Java 中没有无符号数,暂略。但不排除会有使用场景。
# 3.3.4 - 码点和代码单元
# 码点:
是指与一个编码表中的某个字符对应的代码值。
# 代码单元:
是指在基本多语言平面中的每个字符,每个字符用 16 位表示,通常成为代码单元
# 注:
- 在 Unicode 中,码点采用 16 进制书写,并加上前缀 U+,例如 U+0041 就是拉丁字母 A 的码点。
- UniCode 编码表
**String**类的**length()**** 返回的是码点。**- 从 U+0000 到 U+FFFF 的字符集有时称为基本多语言平面 (BMP) 。码位大于 U+FFFF 的字符称为补充字符 s。 **Java 平台在 char 数组以及 String 和 StringBuffer 类中使用 UTF-16 表示。** 在此表示中,补充字符表示为一对 char 值,第一个来自高代理范围 (\uD800-\uDBFF),第二个来自低代理范围 (\uDC00-\uDFFF)。
- 因此, char 值表示基本多语言平面 (BMP) 代码点,包括代理代码点或 UTF-16 编码的代码单元。一个 int 值表示所有 Unicode 代码点,包括补充代码点。 int 的低(最低)21 位用于表示 Unicode 代码点,高(最高)11 位必须为零。除非另有说明,关于补充字符和代理 char 值的行为如下:
- ** 只接受 char 值的方法不支持补充字符。它们将代理范围中的 char 值视为未定义字符。** 例如,
Character.isLetter('\uD840')返回 false ,即使此特定值后跟字符串中的任何低代理值将表示一个字母。 - ** 接受 int 值的方法支持所有 Unicode 字符,包括补充字符。** 例如,
Character.isLetter(0x2F81A)返回 true ,因为代码点值表示一个字母(CJK 表意文字)。
- ** 只接受 char 值的方法不支持补充字符。它们将代理范围中的 char 值视为未定义字符。** 例如,
# 3.5.1 - 算术运算符
很多 Intel 处理器在计算 x * y /z 时,** 会将结果存储在 80 位寄存器中,再除以 z 并将结果截断位 64 位。** 这样可以得到一个更加精确的结果,并且还能避免产生指数溢出。但是,这个结果可能与始终使用 64 位计算的结果不一样。因此,JVM 最初规范规定所有的中间计算都必须进行截断。(也就是全程使用 64 位的空间进行计算)。这种做法遭到了数字社区的反对。
- 截断可能导致溢出
- 截断操作需要消耗时间
所以,现阶段在默认情况下允许对中间结果使用扩展的精度。但是,对于使用 **strictfp** 关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。
# 3.6.4 - 不区分大小写的 equals ()
public boolean equalsIgnoreCase(String anotherString)
将此 String 与其他 String 比较,忽略案例注意事项。如果两个字符串的长度相同,并且两个字符串中的相应字符等于忽略大小写,则两个字符串被认为是相等的。
如果以下至少一个为真,则两个字符 c1 和 c2 被认为是相同的忽略情况:
- 两个字符相同(与
==操作符相比) - 将方法
[Character.toUpperCase(char)](../../java/lang/Character.html#toUpperCase-char-)应用于每个字符产生相同的结果 - 将方法
[Character.toLowerCase(char)](../../java/lang/Character.html#toLowerCase-char-)应用于每个字符产生相同的结果 - 参数
anotherString-String将此String对比 - 结果
true如果参数不是null,它代表等效的String忽略大小写;false否则
# 3.7.2 - printf () 格式化输出
# 先上源码的解释:
- 使用指定格式字符串和参数将格式化字符串写入此输出流的便捷方法。形式为 out.printf (l, format, args) 的此方法的调用与调用的行为方式完全相同 out.format (l, format, args)
- 参形:l – ** 格式化期间应用的语言环境。如果 l 为空,则不应用本地化。**format –格式字符串语法中描述的格式字符串 args – 格式字符串中的格式说明符引用的参数。如果参数多于格式说明符,则忽略多余的参数。参数的数量是可变的,可能为零。参数的最大数量受 Java™ 虚拟机规范定义的 Java 数组的最大维度限制。 null 参数的行为取决于转换。
- 返回值:这个输出流
- 抛出:
- java.util.IllegalFormatException – 如果格式字符串包含非法语法、与给定参数不兼容的格式说明符、给定格式字符串的参数不足或其他非法条件。有关所有可能的格式错误的规范,请参阅格式化程序类规范的详细信息部分。
- NullPointerException – 如果格式为 null
# 格式:
- 每一个以 % 开头的格式说明符都用相应的参数替换。
- 格式说明符的尾部的转换符指示要格式化的数值类型
| 转换符 | 类型 | 示例 |
|---|---|---|
| d | 十进制整数 | 159 |
| x | 十六进制整数 | 9f |
| o | 八进制整数 | 237 |
| f | 定点浮点数 | 15.9 |
| e | 指数浮点数 | 1.59e+01 |
| g | 通用浮点数(e 和 f 中较短的一个) | — |
| a | 十六进制浮点数 | 0x1.fccdp3 |
| s | 字符串 | hello |
| c | 字符 | H |
| b | 布尔 | true |
| h | 散列码 | 42628b2 |
| tx 或 Tx | 日期时间(T 强制大写) | 已过时,应当使用 java.time 包下的类 |
| % | 百分号 | % |
| n | 与平台有关的行分隔符 | — |
- 另外,还可以指定控制格式化输出外观的各种标志。见下表
| 标志 | 目的 | 示例 |
|---|---|---|
| + | 打印正数和负数的符号 | +3333.33 |
| 空格 | 在正数之前添加空格 | | 3333| (或运算符只是为了使空格明显) |
| 0 | 在数字前面补 0 (%06d 表示数字长度为 6,如果小于 6 就补 0,否则不补) | 003333 |
| - | 左对齐 (不知道有啥用) | |3333| |
| ( | 将负数括在括号内 | (3333) (此处的 args 是 - 3333,括起来之后 - 号省去) |
| , | 添加分组分隔符 | 3,333.33 |
| #(对于 f 格式) | 包含小数点 | 3,333.000000 (看注:) |
| #(对于 x 或 o 格式) | 添加前缀 0x 或 0 | 0xcafe |
| $ | 指定要格式化的索引,例如,%1 x 将以十进制和十六进制打印第一个参数 |
159 9F |
| < | 格式化前面说明的数值。例如:% d%<x 将以十进制和十六进制打印同一个数值 | 159 9F |
# 注:
- 关于 % 的解释:就像 \ 一样,需要两个 \ 反斜杠才能表示一个反斜杠,自己转意自己
- #(对于 f 格式): 代码实现会加 六个 0 在小数点后面,不知道为啥
# 代码实现:
public class PrintfTest { | |
public static void main(String[] args) { | |
System.out.printf("小明今年%d岁\n", 18); | |
System.out.printf("小明今年%+d岁\n", 18); | |
System.out.printf("小明今年% d岁\n", 18); | |
System.out.printf("小明今年%(d岁\n", -18); | |
System.out.printf("小明今年%+d岁\n", -18); | |
System.out.printf("%d的十进制是%<d, 八进制是%<o, 十六进制是%<x\n", 64); | |
System.out.printf("%d的十进制是%1$d, 八进制是%1$o, 十六进制是%1$x;\n" + | |
"%d的十进制是%2$d, 八进制是%2$o, 十六进制是%2$x;\n", 128, 256); | |
System.out.printf("%05d\n", 123); | |
System.out.printf("%#f\n", 12.0); | |
} | |
} |
# 3.7.3 - 相对路径是相对于谁的?
文件相对于 Java 虚拟机启动目录的位置,或者由 IDE 控制。可以通过 System.getProperty("user.dir") 来获取路径。
# 3.8.1 - 块作用域
块(即复合语句)是指由若干条 Java 语句组成的语句,并用一对大括号括起来。块确定了变量的作用域。
# 3.8.4 - for 循环的基本编写准则
for 语句的三个部分应该对同一个计数器变量进行 初始化、检测、更新。
# 3.8.6 - 路程控制中的 break
大多数 break 的使用都是跳出当前循环,但实际上并不止于此。
事实上,还有一种带标签的 break 语句,执行带标签的 **break** 语句会跳转到带标签的语句块末尾。
且标签可以应用到任何语句,甚至可以应用到 if 语句或者 块语句。但标签也会被 块语句 限制其作用域
# 3.10.6 - 数组排序中的算法
-
#
Arrays.sort()使用了优化的快速排序算法。
# 源码解析:画饼充饥
-
#
Arrays.binarySearch()使用二分查找算法。
# 源码解析:没吃饱,再来一个
# 4.1.1 - 类、变量、方法、封装的定义
- ** 类:** 是构造对象的模板或蓝图。
- ** 变量:** 是对象中的数据。
- ** 方法:** 操作数据的过程成为方法。
- ** 封装:** 是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。
# 4.1.2 - 对象的三大特性(存疑)
- 对象的行为
- 可以对对象完成哪些操作,或者可以对对象应用哪些方法?
- 对象的行为通过对象的方法来定义
- 对象的状态
- 当调用哪些方法时,对象会如何响应?
- 对象的状态通过变量来定义
- ** 注:** 如果不经过方法调用就可以改变对象状态,只能说明破坏了封装性
- 对象的标识(因为对象的状态并不能完全描述以一个对象,所以会有对象的标识)
- 如何区分具有相同行为与状态的不同对象?
- 对象的标识是对属性进行赋值(存疑)
# 4.1.3 - 类之间的关系
- 依赖 ------- 例如 service 层得类需要 DAO 层的方法
- 聚合 ------- 例如 Person 类 可以包含 Student 类、Teacher 类
- 继承 ------- 例如 所有类都继承自 Object 类
- 接口实现
- 关联
- 直接关联
详见:直接上大佬的 blog : Java 类之间的关联关系_少主无翼的博客 - CSDN 博客_java 类关联
# 4.3.5 - Java 10 新特性
在 Java 10 中,如果可以从变量的初始值推导出他们的类型,那么可以用 var 关键字声明局部变量,而无需指定类型。
例如 var str = "hello";
# 4.3.6 - null 引用
防止 NPE 的方法:
- 宽容型:把 null 参数转换为一个适当的非 null 值
- Java 9 中 Objects 类提供了一个方法:
requireNonNullElse()
- Java 9 中 Objects 类提供了一个方法:
public Employee(String m, double s, int year, int mouth, int day) { | |
name = Objects.requireNonNullElse(n, "unknown"); | |
} |
-
# 严格型:拒绝 null 参数
public Employee(String m, double s, int year, int mouth, int day) { | |
Objects.requireNonNullElse(n, "unknown"); | |
name = n; | |
} |
**requireNonNullElse()** 详解:
- public static
T requireNonNullElse (T obj, T defaultObj) 如果第一个参数为非 null,则返回该参数,否则返回非第二个参数。 - 类型参数:
- T- 引用的类型
- 参数:
- obj- 一个对象
- defaultObj- 如果第一个参数是 nullnull
- 返回:如果第一个参数是非 null ,则为第二个参数
- 抛出:NullPointerException- 如果两者都为空且为 null
- 源码:
public static <T> T requireNonNullElse(T obj, T defaultObj) { | |
return (obj != null) ? obj : requireNonNull(defaultObj, "defaultObj"); | |
} |
# 4.3.7 - 方法的 隐式参数 和 显式参数
- 隐式参数为出现在方法前面,也被称为 方法调用的目标 或 接收者。
- 如:
student.setName("Jack");student 就是隐式参数; - this 也是隐式参数
- 如:
- 显示参数出现在方法的括号内
# 4.3.8 - 封装的注意点
警告:不要编写返回可变对象引用的访问器方法;
例如下面这个类:
class Employee { | |
private Date hirDay; | |
... | |
public Date getHirDay() { | |
return hirDay; | |
} | |
... | |
} |
LocalData 没有更改器方法,与之不同的是 Date 类有一个更改器方法 setTime () ,可以在这里设置毫秒数!!!
所以 Date 对象是可变的,这一点就破坏了封装性!请看下面这段代码:
Employee harry = ...; | |
Date d = harry.getHirDay(); | |
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000; | |
// 这里没有通过该对象修改该对象的属性,破坏了封装性 | |
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds); | |
// 所以返回可变对象的引用时,应该 clone 一个副本进行返回 | |
// 将上面的类代码修改为 | |
class Employee { | |
private Date hirDay; | |
// ... | |
public Date getHirDay() { | |
return (Date) hirDay.clone(); | |
} | |
// ... | |
} |
# 4.6.7- 初始化单个类时的执行顺序
- 如果构造器的第一行调用类另一个构造器,则基于所提供的参数执行第二个构造器。
- 否则
- 所有数据字段初始化默认值(0,false,null)
- 按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块
- 执行构造器主体代码
# 4.7.2 - import 使用注意事项
- 只能使用 _ 导入一个包,不能使用 import java._ 导入所有以 java 为前缀的包
- 编译为字节码文件后,总是使用完整的包名引用其他类
# 4.7.3 - 静态导入
可以使用 import static 导入静态方法 和 静态字段,而不只是类。
导入之后再使用静态方法和静态字段就不需要加类名前缀。
# 4.7.4 - 编译时不检测目录结构
虽然书上写道:编译器在编译源文件时不检查目录结构。编译通过的程序运行时,虚拟机就会找不到类。
但是 IDEA 却会报错,看来这个是 IDEA 的扩展功能?
# 4.7.5 - 包名为什么不能以 java 命名?
因为 java.awt 包下 Window 类的静态变量 warningString 不是 private!这意味着同一包下的类都可以访问该静态变量。这个问题已经 20 多年了,这个变量仍然存在。不仅如此,这个类还陆续增加了一些新的字段,而器中大约有一半也不是私有的。
这会成为一个问题!只需要用户自定义一个包名以 java.awt 为前缀的 类,那么就可以访问到 java.awt 的内部了,使用这一手段可以轻易的设置警告字符串。
所以从 JDK 1.2 开始,JDK 实现者修改了类加载器,明确禁止了加载包名以 java. 开头的用户自定义的类
# 4.10 - 类设计技巧
- 一定要保证数据私有
- 这是最重要的;绝对不要破坏封装性。
- 一定要对数据进行初始化。
- 最好不要依赖于系统的默认值,而是应该显式的初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值
- 不要在类中使用过多的基本类型
- 这个想法是要用其他的类替换使用多个相关的基本类型。例如:
// 可以使用 Address 类来替换以下的实例字段 | |
private String street; | |
private String city; | |
private String state; | |
private int zip; |
- 不是所有的字段都需要单独的字段访问器和字段更改器
- 例如:员工的入职日期,学生的出生日期等
- 分解有过多职责的类
- 类名和方法名要能够体现他们的职责(阿里开发手册也这样说)
- 优先使用不可变的类
# 5.1.5 - 数组引用可以自动向上转型
在 Java 中,子类引用的数组可以转化为超类引用的数组,而不需要强制类型转换。但是可能会有意想不到的错误。
# 5.1.6 - 方法调用的详细过程
这里假设要调用 x.f(args) ,隐式参数声明为 类 C 的一个对象。(隐式参数详见:4.3.7)
- 编译器查看对象的声明类型和方法名。这里由于重载,不一样只有一个待选项。
- 编译器确定方法中提供的参数类型。称为重载解析
- 如果是 private、static、final 方法,那么此时编译器可以准确的知道应该调用哪个方法。这称为静态绑定。如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行是使用动态绑定。
- 程序运行并且采用动态绑定调用方法时,虚拟机必须调用与 x 所引用对象的实际类型对应的那个方法。
注:
- 方法的名字和参数称为方法的签名。返回类型不是签名的一部分。
- 若某个方法被重写了且有返回值,那么我们说这两个方法有可协变的返回类型。
- 可协变的返回类型 :允许子类将覆盖方法的返回类型定义为原返回类型的子类型。
- 每次调用方法都需要完成上述搜索步骤,时间开销相当大。
- 因此虚拟机预先为每个类计算了一个方法表,其中列出了所有的方法签名和要实际调用的方法。
- 或许每个类都有一个方法表?(存疑)
- 动态绑定有一个非常重要的特性:
- 无需对现有的代码进行修改就可以对程序进行扩展。
- 在覆盖(重写)方法时,子类方法的作用范围不能低于父类方法的作用范围。
# 5.1.8 - 强制类型转换
- 进行强制类型转换的唯一原因是:要暂时忽视对象的实际类型之后使用对象的全部功能。
- 强制类型转换前可以通过 instanceof 关键字进行判断,例:
if (s instanceof Person) { | |
Student = (Student) s; | |
} else { | |
//... | |
} |
- 综上所述:
- 只能在继承层次内进行强制类型转换。
- 在将超类转换为子类之前,应该使用 instanceof 进行检查
# 5.2.3 - 相等性测试与继承
# Java 语言规范要求 equals 方法具有下面的特性:
- ** 自反性:** 对于任何非空引用 x,
x.equals(x)应该返回 true; - ** 对称性:** 对于任何引用 x 和 y,当且仅当
y.equals(x)返回 true 时,x.equals返回 true; - ** 传递性:** 对于任何非空引用 x、y 和 z,如果
x.equals(y)返回 true,y.equals(z)返回 true 时,x.equals(z)也应该返回 true; - ** 一致性:** 如果 x 和 y 引用的对象没有发生变化,反复调用
x.equals(y)应该返回同样的结果。 - 对于任意非空引用 x,
x.equals(null)应该返回 false;
不过,就对称性而言,当参数(隐式参数和显式参数)不属于同一个类的时候会有一些微妙的结果,例如下面这个调用:
e.equals(m)
这里的 e 的确是一个 Employee 对象,m 是一个 Manager 对象(Manager 是 Employee 子类)并且两个对象具有相同的姓名、薪水、雇佣日期 **。如果在 Employee.equals 中 使用 instanceof 进行检测,这个调用将返回 true。这意味着反过来调用 **m.equals(e)** 也需要返回 true**,这就使得 Manager 类收到了束缚。Manager 的 equals 方法必须愿意将自己与任何 Employee 对象进行比较,而不考虑 Manager 类特有的信息。
# 所以现在看来有两种情况:
- 如果子类可以有自己的相等性概念,则对称性需求将强制使用 getClass 检测。
- 如果由超类决定相等性概念,那么就可以使用 instanceof 检测,并且应该将此 equals 方法声明为 final,这样可以在不同子类的对象之间进行相等性比较。
# 下面给出编写一个完美的 equals 方法的建议:
- 显式参数命名为 otherObject ,稍后需要将他强制转换为另一个名为 other 的变量
- 检测 this 与 otherObject 是否相等:
if (this == otherObject) return true;–> 因为检查身份比逐个比较字段开销小。 - 检测 otherObject 是否为 null,如果为 null,返回 false。这项检测是很必要的。
if (otherObject == null) retuen false; - 比较 this 与 otherObject 的类。
- 如果 equals 的语义可以在子类中改变,就使用 getClass 检测:
**if (getClass() != otherObject.getClass()) return false;** - 如果所有的子类都具有相同的相等性语义,可以使用 instanceof 检测:
**if (! (otherObject instanceof ClassName)) return false;**
- 如果 equals 的语义可以在子类中改变,就使用 getClass 检测:
- 将 otherObject 强制转换为相应类类型的变量:
ClassName other = (ClassName) otherObject; - 现在根据相等性概念的要求来比较字段。
- 使用 == 比较基本类型字段.
- 使用 Objects.equals 比较对象字段。如果所有的字段都匹配,就返回 true;否则返回 false。
** 提示:** 对于数组类型的字段,可以使用静态的 **Arrays.equals()** 方法检测相应数组是否相等。
# 5.2.4 - hashCode 方法
注意:
- String 类的 hashCode 方法是根据字符串内容得到的。
// JDK 1.8 | |
public int hashCode() { | |
int h = hash; | |
if (h == 0 && value.length > 0) { | |
char val[] = value; | |
for (int i = 0; i < value.length; i++) { | |
h = 31 * h + val[i]; | |
} | |
hash = h; | |
} | |
return h; | |
} |
- 而 Object 类的默认 hashCode 方法会从对象的存储地址得出 散列码。
- 如果重新定义了 equals 方法,那么就必须为用户可能插入散列表的对象重新定义 hashCode 方法。
- 最好使用 null 安全的
**Objects.hashCode()**,若传入了 null,那么这个方法会返回 0; - 需要多个组合多个散列值时,可以调用
**Object.hash**并提供所有这些参数。 - equals 方法与 hashCode 的定义必须相容:** 如果
**x.equals(y)**返回 true,那么**x.hashCode()**就必须与**y.hashCode()**返回相同的值。** 特别的,数组可以使用Arrays.hashCode()得到散列码,这个散列码由数组元素的散列码组成。
# 5.2.5 - toString () 的小优化
- 最好通过
getClass().getName()获得类名,而不要将类名硬编码到 toString 方法中。这样做的好处是:子类调用 toString 方法时也可以根据动态绑定机制显示出正确的类名。 - 调试程序时更好的解决办法是调用
Logger.global.info(), 这个饼等 7.x 再补。
# 5.4 - 基本类型包装类
- 包装类是不可变的,即一旦构造了包装器,就不允许更改在其中的值。
- 自动装箱 与 自动拆箱 是编译器的工作。编译器会自动的插入一条对象拆箱和对象装箱的指令。
- 自动装箱规范要求 boolean、type、char <= 127,介于 -128 和 127 之间的 short 和 int 被包装到固定的对象中。Integer 中源码如下:
/* | |
返回一个表示指定 int 值的 Integer 实例。如果不需要新的 Integer 实例,则通常应优先使用此方法而不是构造函数 Integer (int) ,因为此方法可能会通过缓存频繁请求的值来显着提高空间和时间性能。此方法将始终缓存 -128 到 127(含)范围内的值,并且可能缓存此范围之外的其他值。 | |
参形:i - 一个 int 值。 | |
返回值:表示 i 的 Integer 实例。 | |
自:1.5 | |
*/ | |
public static Integer valueOf(int i) { | |
// 判断是否在 规范 规定的缓存范围内 | |
if (i >= IntegerCache.low && i <= IntegerCache.high) | |
// 从缓存中取出值 | |
return IntegerCache.cache[i + (-IntegerCache.low)]; | |
//new 一个新的对象 | |
return new Integer(i); | |
} |
然而缓存范围的上限是可以通过 ** 添加 JVM 启动参数(-Djava.lang.Integer.IntegerCache.high=256 [当然不一定是 256,可以是其他值])** 进行修改的,原因在 Integer 的内部类 IntegerCache 中。IntegerCache 中静态代码块源码如下:
# 5.8 - 继承的设计技巧
- 将公共操作和字段放在超类中。
- 不要使用受保护 (protected) 的字段。
- protect 机制不能够带来更多的保护。
- 子类集合是无限制的,任何一个人都能够有你的类派生一个子类,然后编写代码直接访问 protect 字段,从而破坏了封装性。
- 在 Java 中,同一个包下的类都可以访问 protect 字段,而不管他们是否为这个类的子类。
- protect 机制不能够带来更多的保护。
- 使用继承实现 "is - a" 关系
- 例如需要定义一个 Contractor(钟点工)的类,钟点工有姓名、雇佣日期,但是没有月薪,他们按小时计薪,且没有奖金。这似乎在引导我们由 Employee 类派生出 Contractor 类,然后增加 hourlyWage 字段。但实际上这样的话,Constractor 类会同时存在 时薪与月薪这两个字段,且钟点工不应该拥有 奖金 字段。这会在实现打印薪水的方法时带来很多问题。
# 除非所有的继承方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而不要使用类型信息。
- 不要滥用反射。
# 6.1.1 - 比较两者之间的大小或者内容的方法都应该遵循对称性原则
归纳:
- compareTo()
- compare()
- equals()
# 6.1.4 - Java 9 新特性
在 Java 9 中,接口的方法可以是 private。
| private | protect | public | static | final | abstract | 默认修饰符 | ||
|---|---|---|---|---|---|---|---|---|
| 接口 | 方法 | √ (Java 9 +) | \ | √ | √ (Java 8 +) | \ | √ | public abstract |
| 变量 | \ | \ | √ | √ | √ | \ | public static final | |
| 抽象类 | 方法 | √ | √ | √ | √ | √ | √ | \ |
| 变量 | √ | √ | √ | √ | √ | \ | \ |
# 6.1.6 - 解决默认方法冲突
- 问题:如果类 A 继承的 B 类中有一个方法 getName (),实现的接口 C 中也有一个 默认方法 getName (),此时类 A 同时含有 B 类的 getName (),C 接口的 getName (),会发生什么情况?
- ** 超类优先。** 接口中的 getName () 会被忽略。
- ** 注:** 千万不要让一个 默认方法 重新定义 Object 类中的方法。
- 问题:如果类 A 实现的 B、C 两个接口中都含有同名的默认方法,会发生什么?
- ** 接口冲突。** 编译器会报告一个错误,让程序员来解决这个二义性问题。解决办法:在 A 中重写此方法。
- ** 注:** 另外的,假设 B 接口中的方法不是默认方法,只是一个 抽象方法与 C 接口重名了,那么编译器也会报告此错误。
# 6.1.9 - 对象克隆
发现大佬写的非常清晰:Java 提高篇 —— 对象克隆(复制) - 萌小 Q - 博客园 (cnblogs.com)
# 6.2.6 - lambda 表达式中的变量作用域
12 章见。
# 6.2.7 - 常用函数式接口
# 常用函数式接口
| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
|---|---|---|---|---|---|
| Runable | \ | void | run | 作为无参数或返回值的动作运行 | |
| Supplier |
\ | T | get | 提供一个 T 类型的值 | |
| Consumer |
T | void | accept | 处理一个 T 类型的值 | andThen |
| BiConsumer<T, U> | 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 |
| UnaryOperate |
T | T | apply | 类型 T 上的一元操作符 | compose, andThen, identity |
| BinaryOperate |
T, T | T | apply | 类型 T 上的二元操作符 | andThen, maxBy, minBy |
| Predicate |
T | boolean | test | 布尔值函数 | and, or, negate, isEqual |
| BiOredicate<T, U> | T, U | boolean | test | 有两个参数的布尔值函数 | and, or, negate |
# 基本类型的函数式接口
| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 |
|---|---|---|---|
| BooleanSupplier | \ | boolean | getAsBoolean |
| PSupplier | \ | p | getAsP |
| PConsumer | p | void | accept |
| ObjPConsumer |
T, p | void | accept |
| PFunction |
p | T | apply |
| PToQFunction | p | q | applyAsQ |
| ToPFunction |
T | p | applyAsP |
| ToPBiFunction<T, U> | T, U | p | applyAsP |
| PUnaryOperate | p | p | applyAsP |
| PBinaryOperate | p, p | p | applyAsP |
| PPredicate | p | boolean | test |
# 注:
- p、q 是 int、long、double;P、Q 是 Int、Long、Double
- @FunctionalInterface 注解标记函数式接口
# 6.3.1 ~ 6.3.4 为什么内部类可以访问外部类的变量?
- 内部类的对象中总有一个隐式引用,指向创建它的外部类对象。
- 外部类的引用在构造器中设置。编译器会修改所有内部类构造器,添加一个对应外部类引用的参数。生成的代码如下所示:
public Inner(Outer outer) { | |
this.outer = outer; | |
} |
# 测试代码:
# OuterTest01.java
package inner_outer; | |
/** | |
* @author: Ding | |
* @date: 2022/5/15 | |
* @description: | |
* @modify: | |
*/ | |
public class OuterTest01 { | |
private int num = 100; | |
public static void main(String[] args) { | |
OuterTest01 o = new OuterTest01(); | |
o.invokeInnerMethod(); | |
} | |
public void invokeInnerMethod() { | |
class InnerA { | |
void sout() { | |
System.out.println(num); | |
} | |
} | |
new InnerA().sout(); | |
} | |
} |
# OuterTest01.class
// | |
// Source code recreated from a .class file by IntelliJ IDEA | |
// (powered by FernFlower decompiler) | |
// | |
package inner_outer; | |
public class OuterTest01 { | |
private int num = 100; | |
public OuterTest01() { | |
} | |
public static void main(String[] args) { | |
OuterTest01 o = new OuterTest01(); | |
o.invokeInnerMethod(); | |
} | |
public void invokeInnerMethod() { | |
(new InnerA()).sout(); | |
} | |
class InnerA { | |
InnerA() { | |
} | |
void sout() { | |
System.out.println(OuterTest01.this.num); | |
} | |
} | |
} | |
//--------------------------------------------------------------- | |
// 通过 javap -private OuterTest01 命令也可以查看内部类的情况 | |
public class inner_outer.OuterTest01 { | |
private int num; | |
public inner_outer.OuterTest01(); | |
public static void main(java.lang.String[]); | |
public void invokeInnerMethod(); | |
// 编译器自动生成的一个静态方法,名字可能是 access$0 | |
static int access$000(inner_outer.OuterTest01); | |
} |
# OuterTest01$1InnerA.class
// | |
// Source code recreated from a .class file by IntelliJ IDEA | |
// (powered by FernFlower decompiler) | |
// | |
package inner_outer; | |
class OuterTest01$1InnerA { | |
OuterTest01$1InnerA(OuterTest01 this$0) { | |
this.this$0 = this$0; | |
} | |
void sout() { | |
System.out.println(OuterTest01.access$000(this.this$0)); | |
} | |
} | |
//--------------------------------------------------------------- | |
// 通过 javap -private OuterTest01$1InnerA 命令也可以查看内部类的情况 | |
class inner_outer.OuterTest01$1InnerA { | |
// 编译器创建的指向外部类的实例字段 | |
final inner_outer.OuterTest01 this$0; | |
// 有参构造器 | |
inner_outer.OuterTest01$1InnerA(inner_outer.OuterTest01); | |
// 成员方法 | |
void sout(); | |
} |
# 注:
- 内部类引用外部类的成员变量的语法规则是:
**OuterTest01.this.num** - 外部类的作用域之外引用内部类的语法规则:
**OuterTest01.InnerA** - 内部类声明的所有静态变量都必须是 final
- 内部类会在编译时被转换为一个常规类,并自动命名,类似于
OuterTest01$InnerA,也就是说这是编译器的工作,JVM 并不知道。运行时会将内部类当作一个常规类来处理。
| 那么问题来了,运行时虚拟机如何知道哪个类是内部类?并使其内部类可以直接访问到外部类的私有成员变量而不需要创建对象? |
|---|
—— 再看看我们通过命令查看的 OuterTest01 类的情况,其中有一个编译器生成的静态方法: **static int access$000(inner_outer.OuterTest01);** 这个静态方法会被内部类中的语句这样调用 **OuterTest01.access$000(this.this$0)** ,参数就是编译器生成的有参构造器中传入的外部类对象,返回值就是所需要的变量。所以,** 每调用一个变量就会生成这样一个静态方法。** 只是在名字的’$' 后的数字上有所差别。 |
# 6.3.4 - 局部内部类如何访问到方法中的显式参数?
- 实际上,编译器会自动的生成一个包含所有显式参数的构造器,并通过这个构造器进行赋值。这里是不是似曾相识??对,上一节中的内部类访问外部类的变量时也是通过构造器传入了一个外部类的对象来使内部类中有一个引用指向外部类。这里会把两个构造器合并,一次性的将外部类对象和显式参数都传入构造器以达到赋值。
那么显式参数赋值给谁呢??
- 编译器也会自动生成显式参数所一一对应的成员变量,并且加以 final 修饰。
# 6.3.6 - 关于匿名内部类
所有的类都有构造器?
- 相信很多人的第一想法是:所有的类都是 Object 的子类,所以默认会有一个无参构造器,那么答案很明显是错误的。
- 匿名内部类没有构造器,书上的原话是:
由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器。
- 所以匿名内部类也不会被编译为一个单独存在的类,因为他没有类名。
# 小技巧:
- 在匿名内部类中生成日志或者调试信息时,通常希望包含当前类的类名,所以可以这样:
this.getClass() - 但静态方法没有 this,所以应该使用:
**new Object() {}.getClass().getEnclosing()**,其中getEnclosing()得到外部类,也就是包含这个静态方法的类。
# 6.3.7 - 关于静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类的一个引用。为此,可以把内部类声明为 static,这样就不会生成那个引用。
# 为什么静态内部类不会生成对外部类的引用?
- 静态内部类是 static 修饰的,只能访问外部类的静态变量,而静态变量可以通过
类名.变量名的形式,所以不需要传入外围类的对象来引用变量。
# 注:
- 只要内部类不需要访问外围类对象,就应该使用静态内部类。
- 与常规类不同,静态内部类可以有静态字段和方法。
- 在接口中声明的内部类自动是 static 和 public
# 7.2.4 - try 中的 return 和 finally 中的 return 返回谁?
- try 中的 return 会被 finally 中的 return 覆盖。
# 7.2.5 - try-with-resource 语句 (Java 8 +)
# 语句格式:
try (Resource res = ... , InputStream is = ...) { | |
work with res | |
} catch (...) { | |
... | |
} |
- 当 try 块执行完毕时,会自动调用
res.close()和is.close()。
在 Java 9 中,可以在 try 首部中提供之前声明的事实最终变量。
public static void printAll(String[] lines, PrintWriter out) { | |
try (out) { | |
for (String line : lines) { | |
out.println(line); | |
} | |
} | |
} |
- 如果 try 块抛出一个异常,而且 close 方法也抛出一个异常,这就会带来一个难题。
- try-with-resource 语句会将原来的异常重新抛出,而 close 抛出的异常会 “被抑制 “。并由 addSuppressed 增加到原来的异常,同样的也可以通过 getSuppressed 得到” 被抑制 “的异常。
x 将以十进制和十六进制打印第一个参数