Java核心技术第10版(套装共2册)

凯S.霍斯特曼 霍斯特曼 科内尔

1.1 Java程序设计平台

  • Java是一个完整的平台,有一个庞大的库,其中包含了很多可重用的代码和一个提供诸如安全性、跨操作系统的可移植性以及自动垃圾收集等服务的执行环境。

1.2 Java“白皮书”的关键术语

  • 1)简单性 2)面向对象 3)分布式 4)健壮性 5)安全性 6)体系结构中立 7)可移植性 8)解释型 9)高性能 10)多线程 11)动态性
  • 面向对象设计是一种程序设计技术。它将重点放在数据(即对象)和对象的接口上。
  • Java与C++的主要不同点在于多重继承,在Java中,取而代之的是更简单的接口概念。与C++相比,Java提供了更丰富的运行时自省功能
  • Java和C++最大的不同在于Java采用的指针模型可以消除重写内存和损坏数据的可能性。
  • Java编译器能够检测许多在其他语言中仅在运行时才能够检测出来的问题。
  • 不可信代码在一个沙箱环境中执行,在这里它不会影响主系统
  • 虚拟机有一个选项,可以将执行最频繁的字节码序列翻译成机器码,这一过程被称为即时编译。
  • 字符串是用标准的Unicode格式存储的。
  • 字节码可以(在运行时刻)动态地翻译成对应运行这个应用的特定CPU的机器码。

1.3 Java applet与Internet

  • 在网页中运行的Java程序称为applet。

1.5 关于Java的常见误解

  • 早期的Java是解释型的。现在Java虚拟机使用了即时编译器,因此采用Java编写的“热点”代码其运行速度与C++相差无几,有些情况下甚至更快。

2.2 使用命令行工具

  • 编译器需要一个文件名(Welcome.java),而运行程序时,只需要指定类名(Welcome),不要带扩展名.java或.class。

3.1 一个简单的Java应用程序

  • 关键字public称为访问修饰符(access modifier)
  • 关键字class表明Java程序中的全部内容都包含在类中
  • Java应用程序中的全部内容都必须放置在类中
  • 源代码的文件名必须与公共类的名字相同,并用.java作为扩展名。
  • 根据Java语言规范,main方法必须声明为public
  • Java中任何方法的代码都用“{”开始,用“}”结束。
  • Java中的main方法必须是静态的
  • 点号(·)用于调用方法

3.2 注释

  • 最后,第3种注释可以用来自动地生成文档。这种注释以/**开始,以*/结束。
  • 在Java中,/**/注释不能嵌套。也就是说,不能简单地把代码用/*和*/括起来作为注释,因为这段代码本身可能也包含一个*/。

3.3 数据类型

  • 在Java中,一共有8种基本类型(primitive type),其中有4种整型、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char(请参见论述char类型的章节)和1种用于表示真值的boolean类型。
  • byte和short类型主要用于特定的应用场合,例如,底层的文件处理或者需要控制占用存储空间量的大数组。
  • 从Java 7开始,加上前缀0b或0B就可以写二进制数。
  • Java没有任何无符号(unsigned)形式的int、long、short或byte类型。
  • float类型的数值有一个后缀F或f(例如,3.14F)。没有后缀F的浮点数值(如3.14)默认为double类型。当然,也可以在浮点数值后面添加后缀D或d(例如,3.14D)。
  • 所有“非数值”的值都认为是不相同的。然而,可以使用Double.isNaN方法:
  • 如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal类
  • 有些Unicode字符可以用一个char值描述,另外一些Unicode字符则需要两个char值
  • char类型的值可以表示为十六进制值,其范围从\u0000到\Uffff
  • Unicode转义序列会在解析代码之前得到处理。例如,"\u0022+\u0022"并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022会在解析之前转换为",这会得到""+"",也就是一个空串。
  • 我们强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元。最好将字符串作为抽象数据类型处理
  • 整型值和布尔值之间不能进行相互转换。

3.4 变量

  • 变量名必须是一个以字母开头并由字母或数字构成的序列。
  • 尽管$是一个合法的Java字符,但不要在你自己的代码中使用这个字符。它只用在Java编译器或其他工具生成的名字中。
  • 不过,不提倡使用这种风格。逐一声明每一个变量可以提高程序的可读性。
  • 声明一个变量之后,必须用赋值语句对变量进行显式初始化,千万不要使用未初始化的变量。
  • 在Java中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的程序编写风格。
  • 在Java中,不区分变量的声明与定义。
  • 关键字final表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。习惯上,常量名使用全大写。
  • const是Java保留的关键字,但目前并没有使用。在Java中,必须使用final定义常量。

3.5 运算符

  • 整数被0除将会产生一个异常,而浮点数被0除将会得到无穷大或NaN结果。
  • 对于使用strictfp关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。
  • 在Java中,没有幂运算,因此需要借助于Math类的pow方法
  • floorMod方法的目的是解决一个长期存在的有关整数余数的问题。
  • floorMod(position+adjustment,12)总会得到一个0~11之间的数。(遗憾的是,对于负除数,floorMod会得到负数结果,不过这种情况在实际中很少出现。)
  • 不必在数学方法名和常量名前添加前缀“Math”,只要在源文件的顶部加上下面这行代码就可以了。
  • 如果得到一个完全可预测的结果比运行速度更重要的话,那么就应该使用StrictMath类
  • 如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型。·否则,如果其中一个操作数是float类型,另一个操作数将会转换为float类型。·否则,如果其中一个操作数是long类型,另一个操作数将会转换为long类型。·否则,两个操作数都将被转换为int类型。
  • 如果想对浮点数进行舍入运算,以便得到最接近的整数(在很多情况下,这种操作更有用),那就需要使用Math.round方法:
  • 前缀形式会先完成加1;而后缀形式会使用变量原来的值。

3.6 字符串

  • Java字符串就是Unicode字符序列
  • String类的substring方法可以从一个较大的字符串提取出一个子串。
  • 当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串
  • 如果需要把多个字符串放在一起,用一个定界符分隔,可以使用静态join方法:
  • 由于不能修改Java字符串中的字符,所以在Java文档中将String类对象称为不可变字符串
  • 不可变字符串却有一个优点:编译器可以让字符串共享。
  • Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率
  • 可以使用equals方法检测两个字符串是否相等。
  • 要想检测两个字符串是否相等,而不区分大小写,可以使用equalsIgnoreCase方法。
  • 一定不要使用==运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否放置在同一个位置上。
  • 实际上只有字符串常量是共享的,而+或substring等操作产生的结果并不是共享的。
  • 空串是一个Java对象,有自己的串长度(0)和内容(空)
  • char数据类型是一个采用UTF-16编码表示Unicode码点的代码单元
  • 每次连接字符串,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生
  • 在JDK5.0中引入StringBuilder类。这个类的前身是StringBuffer,其效率稍有些低,但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder替代它。这两个类的API是相同的。

3.7 输入输出

  • 因为输入是可见的,所以Scanner类不适用于从控制台读取密码。Java SE 6特别引入了Console类实现这个目的。要想读取一个密码,可以采用下列代码:
  • 要想对文件进行读取,就需要一个用File对象构造一个Scanner对象

3.8 控制流程

  • 不能在嵌套的两个块中声明同名的变量

3.9 大数值

  • BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。

3.10 数组

  • 数组是一种数据结构,用来存储同一类型值的集合
  • 创建一个数字数组时,所有元素都初始化为0。boolean数组的元素会初始化为false。对象数组的元素则初始化为一个特殊值null,这表示这些元素(还)未存放任何对象。
  • 如果希望将一个数组的所有值拷贝到一个新的数组中去,就要使用Arrays类的copyOf方法:
  • Java中的[]运算符被预定义为检查数组边界,而且没有指针运算,即不能通过a加1得到数组的下一个元素。
  • 每一个Java应用程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行参数。
  • Java实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组。”

4.1 面向对象程序设计概述

  • 面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
  • 类(class)是构造对象的模板或蓝图
  • 实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。
  • 通过扩展一个类来建立另外一个类的过程称为继承(inheritance)
  • ·对象的行为(behavior)——可以对对象施加哪些操作,或可以对对象施加哪些方法?·对象的状态(state)——当施加那些方法时,对象如何响应?·对象标识(identity)——如何辨别具有相同行为与状态的不同对象?
  • 识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
  • 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。
  • 继承(inheritance),即“is-a”关系,是一种用于表示特殊与一般关系的

4.2 使用预定义类

  • 构造器的名字应该与类名相同。
  • 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
  • 可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。
  • 局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进行初始化。
  • 所有的Java对象都存储在堆中
  • 在Java中,必须使用clone方法获得对象的完整拷贝。
  • 不要使用构造器来构造LocalDate类的对象。实际上,应当使用静态工厂方法(factory method)代表你调用构造器
  • 只访问对象而不修改对象的方法有时称为访问器方法

4.3 用户自定义类

  • 在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
  • 关键字public意味着任何类的任何方法都可以调用这些方法
  • 构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的
  • ·构造器与类同名·每个类可以有一个以上的构造器·构造器可以有0个、1个或多个参数·构造器没有返回值·构造器总是伴随着new操作一起调用
  • 每一个方法中,关键字this表示隐式参数。如果需要的话,可以用下列方式编写raiseSalary方法
  • ·一个私有的数据域; ·一个公有的域访问器方法; ·一个公有的域更改器方法。
  • 如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。
  • 一个方法可以访问所属类的所有对象的私有数据
  • 可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
  • final关键字只是表示存储在evaluations变量中的对象引用不会再指示其他StringBuilder对象。不过这个对象可以更改:

4.4 静态域与静态方法

  • 如果将域定义为static,每个类中只有一个这样的域。
  • 它属于类,而不属于任何独立的对象。
  • 静态方法是一种不能向对象实施操作的方法。
  • Employee类的静态方法不能访问Id实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。
  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。·一个方法只需要访问类的静态域(例如:Employee.getNextId)。

4.5 方法参数

  • 按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
  • Java程序设计语言总是采用按值调用。
  • 方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
  • ·一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。·一个方法可以改变一个对象参数的状态。·一个方法不能让对象参数引用一个新的对象。

4.6 对象构造

  • 这种特征叫做重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。
  • 要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)
  • 如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。
  • 这是域与局部变量的主要不同点。必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域,将会被自动初始化为默认值(0、false或null)。
  • 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
  • 请记住,仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器
  • 如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。
  • 首先运行初始化块,然后才运行构造器的主体部分。
  • 1)所有数据域被初始化为默认值(0、false或null)。2)按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。3)如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。4)执行这个构造器的主体。
  • 在类第一次加载的时候,将会进行静态域的初始化
  • 由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
  • finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。

4.7 包

  • 使用包的主要原因是确保类名的唯一性。
  • 一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)
  • 只能使用星号(*)导入一个包,而不能使用import java.*或import java.*.*导入以java为前缀的所有包。
  • import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
  • 如果没有指定public或private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。

4.9 文档注释

  • 类注释必须放在import语句之后,类定义之前。

4.10 类设计技巧

  • 1.一定要保证数据私有
  • 4.不是所有的域都需要独立的域访问器和域更改器
  • 6.类名和方法名要能够体现它们的职责
  • 7.优先使用不可变的类

第5章 继承

  • 反射是指在程序运行期间发现更多的类及其属性的能力。

5.1 类、超类和子类

  • super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
  • 使用super调用构造器的语句必须是子类构造器的第一条语句。
  • 一个对象变量(例如,变量e)可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
  • 由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)
  • 有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是超类的对象。
  • 在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。
  • 为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中
  • 方法的名字和参数列表称为方法的签名
  • 如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。
  • 每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。
  • 不允许扩展的类被称为final类
  • 如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。
  • 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)
  • 进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
  • ·只能在继承层次内进行类型转换。·在将超类转换成子类之前,应该使用instanceof进行检查。
  • 在一般情况下,应该尽量少用类型转换和instanceof运算符。
  • 包含一个或多个抽象方法的类本身必须被声明为抽象的。
  • 抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。
  • 1)仅对本类可见——private。2)对所有类可见——public。3)对本包和所有子类可见——protected。4)对本包可见——默认(很遗憾),不需要修饰符。

5.2 Object:所有类的超类

  • 在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。
  • 在Object类中,这个方法将判断两个对象是否具有相同的引用。
  • Java语言规范要求equals方法具有下面的特性:1)自反性:对于任何非空引用x,x.equals(x)应该返回true。2)对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。3)传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。4)一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。5)对于任意非空引用x,x.equals(null)应该返回false。
  • 由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
  • 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中
  • Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

5.3 泛型数组列表

  • ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)
  • 一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
  • 使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。

5.4 对象包装器与自动装箱

  • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。
  • ==运算符也可以应用于对象包装器对象,只不过检测的是对象是否指向同一个存储区域

5.6 枚举类

  • 在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“==”就可以了。

5.7 反射

  • 能够分析类能力的程序称为反射(reflective)。
  • 在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。

5.8 继承的设计技巧

  • 1.将公共操作和域放在超类
  • 2.不要使用受保护的域
  • 3.使用继承实现“is-a”关系
  • 4.除非所有继承的方法都有意义,否则不要使用继承
  • 5.在覆盖方法时,不要改变预期的行为
  • 6.使用多态,而非类型信息

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

  • 内部类技术主要用于设计具有相互协作关系的类集合。

6.1 接口

  • 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义
  • 接口中的所有方法自动地属于public。
  • 为了让类实现一个接口,通常需要下面两个步骤:1)将类声明为实现给定的接口。2)对接口中的所有方法进行定义。
  • 接口变量必须引用实现了接口的类对象
  • 接口中的域将被自动设为public static final。
  • 尽管每个类只能够拥有一个超类,但却可以实现多个接口
  • 可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。
  • 1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。2)接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。

6.3 lambda表达式

  • lambda表达式就是一个代码块,以及必须传入代码的变量规范。
  • 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型
  • 对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。
  • 在lambda表达式中,只能引用值不会改变的变量

6.4 内部类

  • 内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
  • 局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

7.1 处理错误

  • 由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
  • 如果出现RuntimeException异常,那么就一定是你的问题
  • Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。

7.4 使用断言

  • 断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。

8.1 为什么要使用泛型程序设计

  • 泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。

8.3 泛型方法

  • 类型变量放在修饰符(这里是public static)的后面,返回类型的前面。

8.8 通配符类型

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

9.1 Java集合框架

  • 队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表
  • Iterator接口的remove方法将会删除上次调用next方法时返回的元素。

第14章 并发

  • 本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。