《Java 核心技术第 10 版》-- 读书笔记
前言
Java 开发工具包(JavaDevelopment Kit)
第四章 对象与类
4.1 面向对象程序设计概述
事实上,在 Java 中,所有的类都源自于一个“神通广大的超类”,它就是 Object。
首先从设计类开始,然后再往每个类中添加方法。
使用预定义类
构造器:
-
在 Java 程序设计语言中,使用构造器(constructor)构造新实例。
-
构造器的名字应该与类名相同。
在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。
可以将 Java 的对象变量看作 C++ 的对象指针。
在 Java 中的 null 引用对应 C++ 中的 NULL 指针。
所有的 Java 对象都存储在堆中。
UTC,称为协调世界时。
时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是所谓的纪元(epoch),它是 UTC 时间 1970 年 1 月 1 日 00:00:00。UTC 是Coordinated Universal Time 的缩写
4.3 用户自定义类
在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
强烈建议将实例域标记为 private。
在 Java 中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是 Java 虚拟机的任务。
应该将所有的数据域都设置为私有的。
可以将实例域定义为final。构建对象时必须初始化这样的域,并且在后面的操作中,不能够再对它进行修改。
4.4 静态域与静态方法
静态域 nextId 也存在。它属于类,而不属于任何独立的对象。
静态方法是一种不能向对象实施操作的方法。
在下面两种情况下使用静态方法:
-
一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
-
一个方法只需要访问类的静态域(例如:Employee.getNextId)。
4.5 方法参数
Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
方法参数共有两种类型:
-
基本数据类型(数字、布尔值)。
-
对象引用。
4.6 对象构造
如果多个方法(比如,StringBuilder 构造器方法)有相同的名字、不同的参数,便产生了重载。
Java 允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
在 Java 中,this 引用等价于 C++ 的 this 指针。
由于 Java 有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。
4.7 包
Java 允许使用包(package)将类组织起来。
标准的 Java 类库分布在多个包中,包括 java.lang、java.util 和 java.net 等。标准的 Java 包具有一个层次结构。
所有标准的 Java 包都处于 java 和 javax 包层次中。
使用包的主要原因是确保类名的唯一性。
事实上,为了保证包名的绝对唯一性,Sun公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。
从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util 包与 java.util.jar 包毫无关系。
一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)。
在 C++ 中,与包机制类似的是命名空间(namespace)。在 Java 中,package 与 import 语句类似于 C++ 中的 namespace 和 using 指令。
如果没有在源文件中放置 package 语句,这个源文件中的类就被放置在一个默认包(defaulf package)中。默认包是一个没有名字的包。在此之前,我们定义的所有类都在默认包中。
如果没有指定 public 或 private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
从 1.2 版开始,JDK 的实现者修改了类加载器,明确地禁止加载用户自定义的、包名以 “java.” 开始的类。
4.9 文档注释
JDK 包含一个很有用的工具,叫做 javadoc,它可以由源文件生成一个 HTML 文档。
每个 /**...*/ 文 档注释在标记之后紧跟着自由格式文本(free-form text)。标记由 @ 开始,如 @author 或 @param。自由格式文本的第一句应该是一个概要性的句子。javadoc 实用程序自动地将这些句子抽取出来形成概要页。
在自由格式文本中,可以使用 HTML 修饰符,例如,用于强调的 ...、用于着重强调的 ... 以及包含图像的 <img...> 等。不过,一定不要使用 <h1>
或 <hr>
,因为它们会与文档的格式产生冲突。若要键入等宽代码,需使用 {@code...} 而不是 <code>...</code>
这样一来,就不用操心对代码中的 < 字符转义了。
-
@param变量描述
-
@return描述
-
@throws类描述
只需要对公有域(通常指的是静态常量)建立文档。
下面的标记可以用在类文档的注释中。
-
@author 姓名
-
@version 文本
可以直接将类、方法和变量的注释放置在 Java 源文件中,只要用 /**...*/ 文档注释界定就可以了。但是,要想产生包注释,就需要在每一个包目录中添加一个单独的文件。
可以有如下两个选择:
-
1、提供一个以 package.html 命名的 HTML 文件。在标记 ... 之间的所有文本都会被抽取出来。
-
2、提供一个以 package-info.java 命名的 Java 文件。这个文件必须包含一个初始的以 /* *和 */ 界定的 Javadoc 注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
4.10 类设计技巧
1.一定要保证数据私有这是最重要的;绝对不要破坏封装性。有时候,需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。
2.一定要对数据初始化
3.不要在类中使用过多的基本类型,就是说,用其他的类代替多个相关的基本类型的使用。
4.不是所有的域都需要独立的域访问器和域更改器
5.将职责过多的类进行分解
6.类名和方法名要能够体现它们的职责
7.优先使用不可变的类,如果类是不可变的,就可以安全地在多个线程间共享其对象。
第五章 继承
5.1 类、超类和子类
Java 与 C++ 定义继承类的方式十分相似。Java 用关键字 extends 代替了 C++ 中的冒号(:)。在 Java 中,所有的继承都是公有继承,而没有 C++ 中的私有继承和保护继承。
在 Java 中使用关键字 super 调用超类的方法
使用 super 调用构造器的语句必须是子类构造器的第一条语句。
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是 public,子类方法一定要声明为 public。
不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。
类中的特定方法也可以被声明为 final。如果这样做,子类就不能覆盖这个方法( final 类中的所有方法自动地成为 final 方法)。
如果将一个类声明为 final,只有其中的方法自动地成为 final,而不包括域。
将方法或类声明为 final 主要目的是:确保它们不会在子类中改变语义。
将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查。
只能在继承层次内进行类型转换。在将超类转换成子类之前,应该使用 instanceof 进行检查。
在一般情况下,应该尽量少用类型转换和 instanceof 运算符。
包含一个或多个抽象方法的类本身必须被声明为抽象的。
除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
类即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能被实例化,只能被继承。
大家都知道,最好将类中的域标记为 private,而方法标记为 public。
在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。
事实上,Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。
归纳一下 Java 用于控制可见性的4个访问修饰符:
-
1)仅对本类可见——private。
-
2)对所有类可见——public。
-
3)对本包和所有子类可见——protected。
-
4)对本包可见——默认(很遗憾),不需要修饰符。
5.2 Object:所有类的超类
Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的。
可以使用 Object 类型的变量引用任何类型的对象。
在 Java 中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。
equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。
在标准 Java 库中包含 150 多个 equals 方法的实现,包括使用 instanceof 检测、调用 getClass 检测、捕获 ClassCastException 或者什么也不做。
-
使用 == 比较基本类型域,如数值、字符、布尔
-
使用 equals 比较对象域,如数组等。
对于数组类型的域,可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。
hashCode 方法
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。
hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
toString 方法
toString 方法,它用于返回表示对象值的字符串。
强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
枚举类
在比较两个枚举类型的值时,永远不需要调用 equals,而直接使用 “==” 就可以了。
toString 的逆方法是静态方法 valueOf。
反射
能够分析类能力的程序称为反射(reflective)
反射机制最重要的内容——检查类的结构。在 java.lang.reflect 包中有三个类
-
Field,描述类的域
-
Method,描述类的方法
-
Constructor,描述类的构造器。
Class 类中的 getFields、getMethods 和 getConstructors 方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。
Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
继承的设计技巧
1.将公共操作和域放在超类
2.不要使用受保护的域
3.使用继承实现“is-a”关系
4.除非所有继承的方法都有意义,否则不要使用继承
5.在覆盖方法时,不要改变预期的行为
6.使用多态,而非类型信息
7.不要过多地使用反射。
第六章 接口、lambda 表达式和内部类
接口
在 Java 程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
接口中的所有方法自动地属于public。
要将类声明为实现某个接口,需要使用关键字 implements
-
- 类与类之间,叫做继承,使用 extends
-
- 类与接口之间,叫做实现,使用 implement
-
- 接口与接口之间,叫做扩展,使用 extends
虽然在接口中不能包含实例域或静态方法,但却可以包含常量
每个类只能够拥有一个超类,但却可以实现多个接口。
为什么 Java 程序设计语言还要不辞辛苦地引入接口?
答:接口可以多继承,抽象类只能单继承。
若一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法,应如何处理?
答:类优先”规则,优先使用超类中的方法。
Cloneable 接口
这个接口指示一个类提供了一个安全的 clone 方法。
Cloneable 接口是 Java 提供的一组标记接口(tagginginterface)之一,还有 serializable 接口也是标记接口。
lambda表达式
lambda 表达式就是一个代码块,以及必须传入代码的变量规范。
实际上,权威的《数学原理》一书中就使用重音符 ^ 来表示自由变量,受此启发,Church 使用大写 lambda(Λ)表示参数。不过,最后他还是改为使用小写的lambda(λ)。从那以后,带参数变量的表达式就被称为 lambda 表达式。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口(functional interface)。
实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口。
在 Java 中,lambda 表达式就是闭包。
使用 lambda 表达式的重点是延迟执行(deferredexecution)。
内部类
内部类(inner class)是定义在另一个类中的类。为什么需要使用内部类呢?其主要原因有以下三点:
-
内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
-
内部类可以对同一个包中的其他类隐藏起来。
-
当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
-
内部类中声明的所有静态域都必须是final。
-
内部类不能有static方法。
-
内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用 $(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。
代理
要想创建一个代理对象,需要使用 Proxy 类的 newProxyInstance 方法。这个方法有三个参数:
-
一个类加载器(class loader)
-
一个Class对象数组,每个元素都是需要实现的接口。
-
一个调用处理器
代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。
-
所有的代理类都扩展于 Proxy 类。
-
一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中
-
所有的代理类都覆盖了 Object 类中的方法 toString、equals和 hashCode
第 7 章 异常、断言和日志
Java 使用一种称为异常处理(exception handing)的错误捕获机制处理。
处理错误
在 Java 程序设计语言中,异常对象都是派生于 Throwable 类的一个实例。
所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支:Error 和 Exception
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。
在设计 Java 程序时,需要关注 Exception 层次结构。这个层次结构又分解为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。
派生于 RuntimeException 的异常包含下面几种情况:
-
错误的类型转换。
-
数组访问越界。
-
访问null指针。
不是派生于 RuntimeException 的异常包括:
-
试图在文件尾部后面读取数据。
-
试图打开一个不存在的文件。
-
试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在。
不需要声明 Java 的内部错误,即从 Error 继承的错误。也不应该声明从 RuntimeException 继承的那些非受查异常。
捕获异常
如果想传递一个异常,就必须在方法的首部添加一个 throws 说明符,以便告知调用者这个方法可能会抛出异常。
记录日志
要生成简单的日志记录,可以使用全局日志记录器(globallogger)并调用其 info 方法
通常,有以下 7 个日志记录器级别:
- SEVERE
- WARNING
- INFO·CONFIG
- FINE
- FINER
- FINEST
在默认情况下,只记录前三个级别。也可以设置其他的级别。可以使用 Level.ALL 开启所有级别的记录,或者使用 Level.OFF 关闭所有级别的记录。
第八章 泛型程序设计
从 Java 程序设计语言 1.0 版发布以来,变化最大的部分就是泛型。
使用泛型机制编写的程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。
泛型对于集合类尤其有用。
为什么要使用泛型程序设计?
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
在Java SE 7及以后的版本中,构造函数中可以省略泛型类型,省略的类型可以从变量的类型推断得出。
定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。
在 Java 库中,使用变量 E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型。T 表示“任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型
换句话说,泛型类可看作普通类的工厂。
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
记住有关 Java 泛型转换的事实:
-
虚拟机中没有泛型,只有普通的类和方法。
-
所有的类型参数都用它们的限定类型替换。
-
桥方法被合成来保持多态。
-
为保持类型安全性,必要时插入强制类型转换。
约束与局限性
不能用类型参数代替基本类型。因此,没有 Pair
Java 不支持泛型类型的数组
注意,Class 类本身是泛型。例如,String.class 是一个 Class
不能在静态域或方法中引用类型变量。
不能抛出或捕获泛型类的实例。
泛型类型的继承规则
无论 S 与 T 有什么联系,通常,Pair 与 Pair
最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如,ArrayList
通配符类型
通配符类型中,允许类型参数变化。例如,通配符类型:
Pair<? extends Employee>
表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类
还可以使用无限定的通配符,例如,Pair<?>。
反射和泛型
反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。
Class 类是泛型的。例如,String.class 实际上是一个 Class
Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。
第九章 Java 集合
Java 集合框架
与现代的数据结构类库的常见情况一样,Java 集合类库也将接口(interface)与实现(implementation)分离。
队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表
在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法:
-
add 方法,用于向集合中添加元素。如果添加元素确实改变了集合就返回 true,如果集合没有发生变化就返回 false。
-
iterator 方法,用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访问集合中的元素
集合有两个基本接口:Collection 和 Map。
要从集合读取元素,可以用迭代器访问元素。不过,从映射中读取值则要使用 get 方法
List 是一个有序集合
集(set)的 add 方法不允许增加重复的元素。
具体的集合
在 Java 程序设计语言中,所有链表实际上都是双向链接的(doubly linked)
List 接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:
-
一种是用迭代器
-
另一种是用 get 和 set 方法随机地访问每个元素。
后者不适用于链表,但对数组却很有用。集合类库提供了一种大家熟悉的 ArrayList 类,这个类也实现了 List 接口。ArrayList 封装了一个动态再分配的对象数组。
HashSet
有一种众所周知的数据结构,可以快速地查找所需要的对象,这就是散列表(hash table)。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。
在 Java 中,散列表用链表数组实现。每个列表被称为桶(bucket)
有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。
Java 集合类库提供了一个 HashSet 类,它实现了基于散列表的集。
TreeSet
TreeSet 类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
正如 TreeSet 类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree)
优先级队列
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。
优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remore)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
映射
Java 类库为映射提供了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。
键必须是唯一的。不能对同一个键存放两个值。
算法
泛型集合接口有一个很大的优点,即算法只需要实现一次。
Collections 类中的 sort 方法可以对实现了 List 接口的集合进行排序。
Java 程序设计语言并不是这样实现的。它直接将所有元素转入一个数组,对数组进行排序,然后,再将排序后的序列复制回列表。
Collections 类的 binarySearch 方法实现了这个算法。注意,集合必须是排好序的,否则算法将返回错误的答案。
如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。
从集合得到数组会更困难一些。当然,可以使用 toArray 方法。
每天学习一点点,每天进步一点点。