《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

    1. 类与类之间,叫做继承,使用 extends
    1. 类与接口之间,叫做实现,使用 implement
    1. 接口与接口之间,叫做扩展,使用 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,只有 Pair

Java 不支持泛型类型的数组

注意,Class 类本身是泛型。例如,String.class 是一个 Class 的实例(事实上,它是唯一的实例)

不能在静态域或方法中引用类型变量。

不能抛出或捕获泛型类的实例。

泛型类型的继承规则

无论 S 与 T 有什么联系,通常,Pair 与 Pair 没有什么联系。

最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如,ArrayList 类实现 List 接口。

通配符类型

通配符类型中,允许类型参数变化。例如,通配符类型:

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 方法。

每天学习一点点,每天进步一点点。

posted @ 2020-12-06 10:35  爱吃西瓜的番茄酱  阅读(239)  评论(0编辑  收藏  举报