0%

《Java 核心技术(第10版)》读书笔记

《Java 核心技术(第10版)》读书笔记,已大概完结。

资源

勘误表

说明

重看这本书一是为了查漏补缺,二是扫清那些不熟悉的知识点。对于已经非常熟悉的部分,就不做笔记了。另外卷 I 第 10~13 章讲的是 Java GUI 和 applet 的知识,用不到,没有看。卷 II 只需要看 IO 和流式 API 等章节,其余可以用到再查。

目录

卷 I 基础知识

第 1 章 Java 程序设计概述

  • 即时编译(JIT)使得现在的的 Java 代码运行速度与 C++ 相差无几,有些情况下甚至更快。

第 3 章 Java 的基本程序设计结构

数据类型

  • Java 整型范围与运行 Java 代码的机器无关。

  • 长整型数值有一个后缀 L 或 l,例如 4000000000L。十六进制数值有一个前缀 0x 或 0X,例如 0xCAFE。八进制有一个前缀 0,例如 010 对应十进制的 8。八进制表示法比较容易混淆,所以建议最好不要使用八进制常数。

  • 从 Java 7 开始,加上前缀 0b 或 0B 就可以写二进制数,例如 0b1001 是 9。Java 7 开始还可以为数字字面量加下划线,如 1_000_000(或 0b1111_0100_0010_0100_0000)表示一百万。这些下划线只是为了让人更易读。Java 编译器会去除这些下划线。

  • Java 没有任何无符号(unsigned)形式的 int、long、short 或 byte 类型。

  • 没有后缀 F 的浮点数值(如 3.14)默认为 double 类型。当然,也可以在浮点数值后面添加后缀 D 或 d(例如,3.14D)

  • 有三个特殊的浮点数值:正无穷大、负无穷大、NaN(不是一个数字)。例如,一个正浮点数除以 0 的结果为正无穷大。计算 0.0/0 或者负数的平方根结果为 NaN。常量 Double.POSITIVE_INFINITY、Double.NEGATIVE_INFINITY 和 Double.NaN(以及相应的 Float 类型的常量)分别表示这三个特殊的值。判断一个特定值是否等于 Double.NaN 应该用 Double.isNaN() 方法,而不是“==”。

  • Unicode 转义序列会在解析代码之前得到处理。因此需要小心注释中的 \u。注释 // \u000A 会产生一个语法错误,因为读程序时 \u000A 会替换成一个换行符。 类似地,注释 // c:\users 也会产生一个语法错误,因为 \u 后面并未跟着 4 个十六进制数。

  • 我们强烈建议不要在程序中使用 char 类型,除非确实需要处理 UTF-16 代码单元。最好将字符串作为抽象数据类型处理。

    这一条其实我也没太弄懂,关于 Java Unicode、码点、代码单元等相关知识,用到再查。

变量

  • 尽管 $ 是一个合法的 Java 字符,但不要在你自己的代码中使用这个字符。它只用在 Java 编译器或其他工具生成的名字中。

  • 在 Java 中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的程序编写风格。

运算符

  • 如果两个操作数中有一个是 double 类型,另一个操作数就会转换为 double 类型。否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型。否则,如果其中一个操作数是 long 类型,另一个操作数将会转换为 long 类型。否则,两个操作数都将被转换为 int 类型。

    注意:自动类型转换规则包括三目运算!!

  • 如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果 x 是一个 int,则 x += 3.5; 是合法的,将把 x 设置为 (int)(x + 3.5)

  • >>> 运算符会用 0 填充高位,这与 >> 不同,它会用符号位填充高位。不存在 <<< 运算符。移位运算符的右操作数要完成模 32 的运算(除非左操作数是 long 类型,在这种情况下需要对右操作数模 64)。例如,1 << 35 的值等同于 1 << 3 ,结果为 8。

字符串

P49 讲了码点和代码单元的知识,但基本用不到,不要踩这个坑就行。

控制流程

  • 在循环中,检测两个浮点数是否相等需要格外小心。循环:for(double x = 0; x != 10; x += 0.1) 可能永远不会结束。由于舍入的误差,最终可能得不到精确值。因为 0.1 无法精确地用二进制表示,所以,x 将从 9.999 999 999 999 98 跳到 10.099 999 999 999 98

  • 许多程序员容易混淆 break 和 continue 语句。这些语句完全是可选的,即不使用它们也可以表达同样的逻辑含义。

数组

  • 数组长度不要求是常量:new int[n] 会创建一个长度为 n 的数组。

  • 在 Java 中,允许数组长度为 0。在编写一个结果为数组的方法时,如果碰巧结果为空,则这种语法形式就显得非常有用。此时可以创建一个长度为 0 的数组:new elementType[0]。注意,数组长度为 0 与 null 不同。

P83 讲了如何从 m 个数中随机取 n 个的实现。

第 4 章 对象与类

使用预定义类

  • 标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。

更改器方法(mutator method)和访问器方法(accessor method)区别。

用户自定义类

尽量不要返回类中可变的成员变量,这样会破坏类的封装性。如果需要返回一个可变数据域的拷贝,就应该使用 clone。

  • 如果类中的每个方法都不会改变其对象,这种类就是不可变的类。

第 5 章 继承

类、超类和子类

  • 有些人认为 super 与 this 引用的是类似的概念,实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

  • 一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。

Object:所有类的超类

为了防备 equals() 方法调用者可能为 null 的情况,需要使用 Objects.equals() 方法。
同样,还有 Objects.hashCode() 方法

这里还讲了重写 equals() 方法需要注意的细节,比如该用 instanceof 还是 getClass() 的问题。《Effective Java》里有关于重写 equals() 方法和 hashCode() 方法的详细介绍。

泛型数组列表

  • 一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
  • 一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用 trimToSize。

对象包装器与自动装箱

  • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是 final,因此不能定义它们的子类。

  • 如果在一个条件表达式中混合使用 Integer 和 Double 类型,Integer 值就会拆箱,提升为 double,再装箱为 Double:

    1
    2
    3
    Integer n = 1;
    Double x = 2.0;
    System.out.println(true ? n : x); // 输出 1.0

反射

  • 能够分析类能力的程序称为反射(reflective)。
  • 使用反射的主要人员是工具构造者,而不是应用程序员。

第 6 章 接口、lambda 表达式与内部类

接口

接口声明方法不必声明 public,因为接口的所有方法都自动地是 public,但在接口的实现类里必须将方法声明为 public。

compareTo() 方法如果使用两整数相减的实现,要确保减法运算不会溢出,否则,调用静态 Integer.compare() 方法。而如果是两个很接近的浮点数,它们的差经过四舍五入后有可能变成 0,应该调用 Double.compare() 方法。

关于父类 x 和子类 y ,如果它们各自实现了自己的 compareTo() 方法,调用 x.compareTo(y) 没问题,但调用 y.compareTo(x) 时,若在方法内将 x 转换为 y 类型可能会报 ClassCastException 所以,这种情况有两种解决方案:第一种是利用 getClass() 判断两者是否是同一类型,不同的话属于非法比较,直接抛出异常;第二种是在 x 中实现一个子类也通用的 compareTo() 方法,并声明为 final 类型。

Java 语言规范建议不要书写接口中 publi static final 这样的多余关键字。

  • 实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

  • 在 Java API 中,,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接口的部分或所有方法,如 Collection/AbstractCollection 或 MouseListener/MouseAdapter。在 Java SE 8 中,这个技术已经过时。现在可以直接在接口中实现方法。

另外,默认方法也可以使得在 Java 老接口里定义新的默认方法,这样老的接口实现代码不必改动。

解决默认方法冲突:
(1) 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
(2) 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,则子接口或实现类必须覆盖这个方法来解决冲突。

接口示例

要对一个 String 数组按 String 长度排序,不能利用 String 自己实现的 compareTo() 方法,需要自己创建一个 Comparator 对象实现按字符串长度的比较方法,然后传入 Arrays.sort()。

Cloneable 接口只是一个标记接口(标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用 instanceof)

内部类

  • 内部类中声明的所有静态域都必须是 final,并且用一个编译时常量来初始化。原因很简单,我们希望一个静态域只能有一个实例,不过对于每个外部对象,会分别有一个单独的内部实例。如果这个域不是常量,它可能不是唯一的。内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。(按理来说)也可以允许有静态方法,(让它)访问外围类的静态域和方法。(但是)显然,Java 设计者认为相对于这种复杂性来说,它带来的好处有些得不偿失。(注:括号内容和删除线是我加的,原文翻译不通顺)

  • 多年来,Java 程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用 lambda 表达式。

  • 在内部类不需要访问外围类对象的时候,应该使用静态内部类。

代理

  • 克隆和代理是库设计者和工具构造者感兴趣的高级技术,对应用程序员来说,它们并不十分重要。

第 7 章 异常、断言和日志

处理错误

  • Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。这种情况很少出现。

  • 子类方法中声明的受检查异常不能比超类方法中声明的异常更通用。

  • 如果超类方法没有抛出任何受检查异常,子类也不能抛出任何受检查异常。

捕获异常

  • 捕获多个异常时,异常变量隐含为 final 变量。例如,不能在以下子句体中为 e 赋不同的值:catch (FileNotFoundException | UnknownHostException e) { ... }

7.2.3 节还讲了异常链,用到可以一看。

  • try 语句可以只有 finally 子句,而没有 catch 子句。

  • 当 finally 子句包含 return 语句时,将会出现一种意想不到的结果。假设利用 return 语句从 try 语句块中退出。在方法返回前,finally 子句的内容将被执行。如果 finally 子句中也有一个 return 语句,这个返回值将会覆盖原始的返回值。

另外当在 finally 子句中抛出异常时,会把 try 子句和 catch 子句中抛出的异常覆盖掉。

使用异常机制的技巧

捕获异常比判定合法条件再运行要花费更多的时间,因此使用异常的基本规则是:只在异常情况下使用异常机制。

  • 不要过分地细化异常

可以把一个逻辑放在一个 try 语句里,然后 catch 多个可能发生的异常。

异常要“早抛出,晚捕获”。

记录日志

这节没仔细看,不重要,用到可以再查。

使用Java标准库内置的Logging有以下局限:
Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改配置;
配置不太方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=< config-file-name >。
因此,Java标准库内置的Logging使用并不是非常广泛。
——廖雪峰教程

第 8 章 泛型程序设计

泛型方法

  • 类型变量放在修饰符(例如:public static)的后面,返回类型的前面

类型变量的限定

  • 一个类型变量或通配符可以有多个限定,例如:T, E extends Comparable & Serializable。限定类型用 & 分隔,而 , 用来分隔类型变量

  • 可以根据需要有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

泛型代码和虚拟机

  • 原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。

  • class Interval< T extends Serializable & Comparable > 中,原始类型用 Serializable 替换 T,而编译器在必要时要向 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在限定列表的末尾。

约束与局限性

  • 不能创建参数化类型的数组,但声明参数化类型的数组是允许的。也可以创建数组后强制转换为参数化类型。

  • 不能抛出或捕获泛型类的实例,泛型类扩展 Throwable 都是不合法的。

通配符类型

  • 带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

  • 无限定通配符类型的 get 方法只能赋值给一个 Object,set 方法不能被调用(除非传入 null)。通常它被用来判定是否为 null。

本章泛型相关跳着看的,后面比较复杂,实际应该很少遇到,遇到再来查。

第 9 章 集合

Java 集合框架

  • 查找一个元素的唯一方法是调用 next,而在执行查找操作的同时,迭代器的位置随之向前移动。因此,应该将 Java 迭代器认为是位于两个元素之间。当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

具体的集合

  • 在 Java 程序设计语言中,所有链表实际上都是双向链接的——即每个结点还存放着指向前驱结点的引用。

  • 只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,set 类型中的元素完全无序,因此,在 Iterator 接口中就没有 add 方法,相反地,集合类库提供了子接口 ListIterator,其中包含 add 方法。

  • 在调用 next 之后,remove 方法删除迭代器左侧的元素。但是,如果调用 previous 就会将右侧的元素删除掉,并且不能连续调用两次 remove。add 方法只依赖于迭代器的位置,而 remove 方法依赖于迭代器的状态。

  • LinkedList 的 get 方法每次查找一个元素都要从列表的头部重新开始搜索。LinkedList 对象根本不做任何缓存位置信息的操作。get 方法做了微小的优化:如果索引大于 size()/2 就从列表尾端开始搜索元素。

  • 标准类库散列表使用的桶数是 2 的幂,默认值为 16(为表大小提供的任何值都将被自动地转换为 2 的下一个幂)。

映射

  • putIfAbsent 方法只有当键原先不存在或键对应的值为 null 时才会放入一个值。

视图与包装器

这节讲了 Java 集合的视图及其应用,大概扫了一遍,用到再查。

第 14 章 并发

线程

卷 II 高级特性

第 1 章 Java SE 8 的流库

本章讲了 Java 8 的流式 API,其中几个和 python 里的 map、reduce、filter 函数类似,能很方便地对数据集进行处理。相当于把自己的迭代实现提供了现成的接口调用。

第 2 章 输入与输出

这本书这章感觉讲的不太好,用到再查。

觉得文章有帮助,打赏1元鼓励一下作者