本文主要整理了 OnJava8 的阅读笔记。

第三章 万物皆对象

对象操纵:在 Java 中程序员实际操作的是对象的引用,方法参数中传递的也只是对象的引用。

对象创建:new。

  • 数据存储:

    • 寄存器:Java 中不存在该方式。
    • 栈内存:存放一些 Java 数据,比如对象的引用。
    • 堆内存:Java 对象都存于其中。
    • 常量内存:程序代码中,不会改变。
    • 非 RAM 存储:序列化对象(用于传送)和持久化对象(用于恢复)。
  • 基本类型的存储:不是通过 new 创建,变量直接存储值。有 boolean,byte,short,char,int,float,long,double,void。boolean 类型的大小没有明确规定。

  • 高精度数值:BigInteger 和 BigDecimal。

  • 数组的存储:当创建对象数组的时候,实际上是对象引用的数组,初始化为 null。

代码注释:/* ... *///

对象清理:

  • 作用域:{} 决定,不允许父作用域和子作用域声明相同的变量。
  • 对象作用域:使用 new 关键字创建的 Java 对象生命周期超出作用域。

类的创建:

  • 类型:class
  • 字段:类里面声明的变量
  • 方法:类里面定义的函数
  • 基本类型的默认值:全 0,但是不适用于局部变量。
  • 方法签名:方法名和参数列表统称为方法签名。

程序编写:

  • 命名可见性:反向使用自己的网络域名,但是存在空文件夹
  • 使用其他组件:import
  • static 关键字:类变量和类方法声明,在没有对象时候也可以进行调用,另外类变量在所有的对象中共享

第四章 运算符

赋值:=,基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。在方法的参数中传递一个对象,在方法体里面对其进行修改,那么在该对象在外部也会被修改。

算术运算符:+,-,*,/,%,其中+, -可以作为一元运算符。

递增和递减:++,--。前缀递增和递减立即修改变量的值,后缀则是使用变量的值,然后再修改。

关系运算符:>, >=, <, <=, ==, !=。判断基本类型的时候使用==,判断对象的使用 equals 方法,判断对象时使用 == 比较的只是引用。

逻辑运算符:&&, ||, !。Java 支持短路。

字面量常量:0x, 0, 0b, L, F, F 可以默认不写。

下划线:用于分割数字字面量。

指数计数法:e

位运算符:&, |, ^, ~

移位运算符:<<, >>, >>>。注意 >> 是算术右移,>>> 是逻辑右移(首位添0)。

三目运算符:<boolean condition> ? <value1> : <value2>

字符串运算符:+

类型转换:向上转换是安全的,向下转换需要显式说明(type)。对于浮点数向整数转换,小数总是被截断。

Java 没有 sizeof 运算符,因为每种类型的值都是固定的。

第五章 控制流

true 和 false:所有关系运算符都能产生条件语句,注意在 Java 中使用数值作为布尔值是非法的。

条件控制:if-else

迭代语句:whiledo-whileforfor-in

return:退出当前的方法,放回一个方法值。

break 和 continue:break 用于中止内层循环,continue 用于跳过此次迭代。

goto:Java 不支持 goto 语句,但是支持标签语法,可以和 break 和 continue 一起使用。

switch-case:每个 case 后面跟上 break,同时在 Java7 的时候开始支持字符串匹配。

第六章 初始化和清理

使用构造器保证初始化:构造器名称和类名相同,每次创建一个对象的时候,自动调用构造器进行初始化。构造器并没有返回值。

方法重载:每个被重载的方法需要具有一个独一无二的参数列表。返回值并不能用来区分重载的方法。

无参构造器:一个无参构造器就是不接受参数的构造器,如果没有显式提供任何构造器,那么编译器会自动提供一个无参构造器。

this:this 关键字只能在非静态的方法内部使用,等同于当前方法所属的对象引用。

在构造器中调用构造器:通过 this(param list) 实现。注意只能通过 this 调用一次构造器,不可重复多次调用构造器。并且,只能在构造器首行进行调用。

static 的含义:static 修饰的方法中不存在 this。静态方法和静态变量是为了类而创建的。

垃圾回收器:在 Java 中,对象并非总是被垃圾回收:

  1. 对象可能不被垃圾回收。
  2. 垃圾回收不等同于析构。
  3. 垃圾回收只和内存有关。

在 Java 中,虽然提供了一个 finialize() 的方法用于清理对象,但是事实上我们并不需要过多使用该方法。记住,无论是”垃圾回收”还是”终结”,都不保证一定会发生。如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存。

垃圾回收器如何工作:

  • 引用计数:每个对象有一个引用计数器,每次有引用指向该对象的时候,引用计数加 1。当引用离开作用域或者被置为 null 的时候,引用计数减 1。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间(但是,引用计数模式经常会在计数为 0 时立即释放对象)。这个机制存在一个缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。
  • 自适应的垃圾回收技术:对于任意“活”的对象,总是可以追溯到其存活在栈或者静态区的引用,从栈或者静态存储区出发,将会发现所有的活的对象。至于如何处理找到的存活对象,取决于不同的 Java 虚拟机实现。其中有一种做法叫做停止-复制(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。上述方法存在缺点:需要两个堆,然后再两个堆之间折腾,得维护比实际空间多一倍的空间;另外在于复制本身,一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即”自适应”)。这种模式称为标记-清扫(mark-and-sweep)。对一般用途而言,”标记-清扫”方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了。”标记-清扫”所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。”标记-清扫”后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。

成员初始化:在方法中的变量没有默认值,需要手动指定一个值之后才能使用;在类中的变量则会赋予默认值。

初始化的顺序:假设有个名为 Dog 的类:

  1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class
  2. 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
  3. 当用 new Dog() 创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。
  4. 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null
  5. 执行所有出现在字段定义处的初始化动作。
  6. 执行构造器。你将会在”复用”这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。

显式的静态初始化:static { statements; },与其他静态初始化动作一样,这段代码仅执行一次:当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时。

实例初始化:{ statements; },实例初始化子句是在构造器之前执行的。

数组初始化:Type[] arg = new Type[length]Type[] arg = {value1, value2,,,}

可变参数列表:void method(int t, char... args)

枚举类型:enum Type {}

第七章 封装

包的概念:包内包含有一组类,它们被组织在一个单独的命名空间下。对于单文件的程序,该文件在默认包(default package)下。另外,每个 Java 源文件只能有一个 public 类。

代码组织:为了将功能相近的 Java 源文件组织到一起,可以使用关键字 package。该关键字必须是文件中除了注释的第一行代码。当需要使用到某个包中的类时,可以使用 import 关键字。

独一无二的包名:通常选择反转的域名。

冲突:当两个包下面含有相同的类时,就会出现名称冲突的问题,可以将特定的类写全名称,比如java.util.ArrayList

使用包的注意事项:当创建一个包的时候,包名实际上就隐含了目录结构。

访问权限修饰符:Java 访问权限控制符 public,protected,private 位于定义的类名,属性名和方法名前。

public:当使用 public 关键字的时候,意味着 public 后声明的成员对于每个人都是可用的。

default:指不加修饰符定义的成员,可以被相同包下的文件访问。

private:除了包含成员的类,其他任何类都无法访问这个成员。

protected:继承的类可以访问父类中对应的成员,同时也提供了包访问权限。但是需要注意:

  • 基类的 protected 成员是包内可见的,并且对子类可见
  • 若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的 protected 方法

类访问权限:类既不能是 private,也不能是 protected 的,只能使用 public 或者是 包访问权限。

第八章 复用

复用方式:

  • 组合:在新类里面创建现有类的对象
  • 继承:创建现有类型的子类
  • 委托:介于继承和组合之间,将一个成员对象放在正在构建的类中,但同时又在新的类中公开来自成员对象的所有方法(Java 中不直接支持)

组合语法:将对象的引用放在一个新的类里面,就算是使用了组合。

继承语法:使用 extends 关键字。继承后,可以在方法里面使用 super 关键字来使用父类的方法。

子类的初始化:当某个派生类被实例化的时候,会递归向上调用父类的构造器,最高层级的父类的构造器首先被执行,然后是最高层级下的子类,,,一直到该派生类构造器。

带参数的构造器:当没有无参的基类构造器,只含有有参的基类构造器,此时就需要通过 super 手动调用基类的构造器。

组合和继承的选择:当想要在新类中包含一个已有类的功能时,使用组合,而非继承;当使用继承时,使用一个现有类并开发出它的新版本,通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。组合用来表达“有一个”的关系,而继承则是“是一个”的关系。

向上转型:派生类到基类的转型称之为向上转型,向上转型是安全的,因为子类必定包含了所有父类的方法。

final 关键字:final 修饰的数据通常指该数据不能被改变:

  • final 数据:对于基本类型,final 使得数值恒定不变,对于对象引用,final 则是使得引用恒定不变。空白 final 是指没有初始化值的 final 属性,编译器保证在使用空白 final 之前必须被初始化,此时必须在构造器中对 final 变量进行赋值。
  • final 参数:在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量。
  • final 方法:给方法上锁,防止子类通过覆写改变方法的行为。类中所有的 private 方法都隐式地指定为 final。
  • final 类:当说一个类是 final ,就意味着它不能被继承。

类初始化和加载:在 Java 中,每个类的编译代码都存在于它自己独立的文件中,该文件只有在使用程序代码时才会被加载。一般可以说“类的代码在首次使用时加载”。这通常是指创建类的第一个对象,或者是访问了类的 static 属性或方法。构造器也是一个 static 方法尽管它的 static 关键字是隐式的。因此,准确地说,一个类当它任意一个 static 成员被访问时,就会被加载。

第九章 多态

向上转型:当使用向上转型的时候,我们可以将所有派生类当做是基类来看待,提高了程序的可拓展性。

方法调用绑定:当派生类重写了基类的方法时,我们使用向上转型后,调用这些被重写的方法时,编译器会动态绑定到派生类中被重写的方法,执行方法调用。Java 中除了 staticfinal 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。

陷阱:

  • 试图重写私有方法
  • 只有普通的方法调用是多态的,属性并不能多态(访问属性的时候看的是类型,也就是说向上转型后,只能直接访问基类中的属性)

构造器和多态:

  • 构造器调用顺序:首先是基类构造器被调用,然后按照顺序初始化成员,接着调用派生类构造器的方法体
  • 继承和清理:在清理工作的时候,应该先释放派生类的对象,然后释放基类的对象
  • 构造器内部多态方法的行为:如果在构造器中调用了正在构造的对象的动态绑定方法,就会用到那个方法的重写定义

协变返回类型:派生类的被重写方法可以返回基类方法返回类型的派生类型。

向下转型:重新将基类类型改为派生类类型,是不安全的。

第十章 接口

接口和抽象类提供了一种将接口与实现分离的更加结构化的方法,抽象类是一种介于普通类和接口之间的折中手段。

抽象类和方法:抽象方法只有声明没有方法体,并且使用 abstract 关键字,包含有抽象方法的类称为抽象类,并且类本身也必须限定为抽象。抽象类不能被实例化,如果某个类继承自抽象类,就必须实现该抽象类中所有的抽象方法,如果不这么做的话,新的类也是一个抽象类。

接口创建:使用 interface 关键字创接口。一个接口表示,所有实现了该接口的类看起来都这样。在 Java8 之前,接口里面只允许抽象方法(不用加 abstract 关键字),在 Java8 里面又新增了默认方法。另外,接口同样可以包含属性,这些属性被隐式指明为 static 和 final。使用 implements 关键字使一个类遵循某个特定接口(或一组接口),它表示:接口只是外形,现在我要说明它是如何工作的。最后,接口中的方法是 public 权限的。

  • 默认方法:当实现了某个接口的类没有实现某个方法的时候,此时可以使用接口的默认方法,使用 default 关键字,可以带有方法体。增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。
  • 多继承:Java 中只支持单继承,但是 Java 通过默认方法具有某种多继承的特性,结合带有默认方法的接口意味着结合了多个基类中的行为。因为接口中仍然不允许存在属性(只有静态属性,不适用),所以属性仍然只会来自单个基类或抽象类,也就是说,不会存在状态的多继承。
  • 接口中的静态方法:Java8 中允许在接口中添加静态方法,这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具。

抽象类和接口:

特性 接口 抽象类
组合 新类可以组合多个接口 只能继承单一抽象类
状态 不能包含属性(除了静态属性,不支持对象状态) 可以包含属性,非抽象方法可能引用这些属性
默认方法和抽象方法 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 必须在子类中实现抽象方法
构造器 没有构造器 可以有构造器
可见性 隐式 public 可以是 protected 或友元

完全解耦:使用接口更有利于实现完全解耦,使得代码更具有可复用性。

多接口实现:一个类只能继承自一个父类,同时可以实现多个接口,提高类的灵活度。

使用继承扩展接口:通过继承,可以很容易在接口中增加方法声明,还可以在新的接口中实现多个接口。注意,通常来说,extends 只能用于单一类,但是接口可以多继承。

实现接口时的命名冲突:覆写、实现和重载令人不快地搅和在一起带来了困难,当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

接口适配:接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象则取决于方法的使用者。

接口字段:因为接口中的字段都自动是 static 和 final 的,所以接口就成为了创建一组常量的方便的工具。但是在 Java8 中,应尽量使用 enum 关键字来定义枚举变量。

接口嵌套:接口可以嵌套在类或者是其他接口中。

第十一章 内部类

内部类创建:将类的定义放在外部类的里面。如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须具体地指明这个对象的类型:OuterClassName.InnerClassName

链接外部类:当生成一个内部类的对象的时候,该对象能够访问到外部对象的所有成员,而不需要其他任何特殊权限。当某个外部类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用。然后,在你访问此外部类的成员时,就是用那个引用来选择外部类的成员。但是这些都是编译器的细节了。

使用 .this.new:如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this。有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 new 表达式中提供对其他外部类对象的引用,这是需要使用 .new 语法。

1
2
3
4
5
6
7
8
9
10
// innerclasses/DotNew.java
// Creating an inner class directly using .new syntax
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}

内部类和向上转型:当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。

在方法和作用域中声明内部类:可以在一个方法里面或者在任意的作用域内定义内部类。

匿名内部类:通常使用 new ClassName(params) { ... };,params 用于构造器传参,后面的分号指代语句结束。另外,如果匿名类内部希望使用一个定义在其外部的对象,那么编译器要求其参数引用必须是 final 的。注意在实例化匿名类的时候,可以使用非 final 修饰的变量。匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。

嵌套类:如果不需要内部类对象与其外部类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。想要理解 static 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:

  1. 要创建嵌套类的对象,并不需要其外部类的对象。
  2. 不能从嵌套类的对象中访问非静态的外部类对象。

嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。

  • 接口内部的类:嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static 的。
  • 从多层嵌套类中访问外部类的成员:一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外部类的所有成员。

为什么需要内部类:

  • 闭包和回调:在 Java8 之前,内部类是实现闭包的唯一方式,在 Java8 中,我们可以使用 lambda 表达式来实现闭包行为,并且更加优雅。

继承内部类:因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// innerclasses/InheritInner.java
// Inheriting an inner class
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//- InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}

内部类标示符:由于编译后每个类都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息。内部类也必须生成一个 .class 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外部类的名字,加上 “$” ,再加上内部类的名字。如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。

第十二章 集合

泛型和类型安全的集合:通过使用泛型,规定了向某个集合中可以添加的变量类型,方便进行处理,同时不会引发类型转型错误等问题。

Java 集合类库的两个概念:集合(Collection)和映射(Map)。

添加元素组:通过 Arrays.asList 和 Collections.addAll 方法来添加元素组。注意 Arrays.asList 的返回值是一个 List,但是这个 List 不能调整大小。

集合的打印:必须使用 Arrays.toString 来生成数组的可打印形式,但是打印集合无需任何操作。

列表 List:有 ArrayList 和 LinkedList,前者擅长随机访问,后者擅长插入删除操作。当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals() 方法。toArray() 方法将任意的 Collection 转换为数组。

迭代器 Iterators:在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 Listadd() 是插入元素的一种方式, get() 是获取元素的一种方式。如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。为此引入迭代器,迭代器相关方法有iterator,next,hasNext,remove。迭代器统一了对集合的访问方式。

ListIterator:ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。

LinkedList:LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。如 element,peek,poll,offer 等。

栈 Stack:后进先出规则,Java 1.0 中附带了一个 Stack 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误)。Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法。

集合 Set:Set 不保存重复的元素,Set 具有与 Collection 相同的接口,因此没有任何额外的功能。HashSet 产生的输出没有可辨别的顺序,这是因为出于对速度的追求, HashSet 使用了散列。由 HashSet 维护的顺序与 TreeSet 或 LinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。TreeSet 将元素存储在红-黑树数据结构中,而 HashSet 使用散列函数。LinkedHashSet 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。

映射 Map:根据键快速查找值的结构。Map 可以返回由其键组成的 Set ,由其值组成的 Collection ,或者其键值对的 Set 。keySet() 方法生成由在 petPeople 中的所有键组成的 Set ,它在 for-in 语句中被用来遍历该 Map 。

队列 Queue:先进先出的集合,LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。offer() 是与 Queue 相关的方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 false 。 peek() 和 element() 都返回队头元素而不删除它,但是如果队列为空,则 element() 抛出 NoSuchElementException ,而 peek() 返回 null 。 poll() 和 remove() 都删除并返回队头元素,但如果队列为空,poll() 返回 null ,而 remove() 抛出 NoSuchElementException 。

优先级队列 PriorityQueue:先进先出(FIFO)描述了最典型的队列规则(queuing discipline)。优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。当在 PriorityQueue 上调用 offer() 方法来插入一个对象时,该对象会在队列中被排序。默认的排序使用队列中对象的自然顺序(natural order),但是可以通过提供自己的 Comparator 来修改这个顺序。 PriorityQueue 确保在调用 peek(), poll() 或 remove() 方法时,获得的元素将是队列中优先级最高的元素。

集合和迭代器:Collection 是所有序列集合共有的根接口,使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。为了对集合进行遍历操作,我们可以使用迭代器来进行操作。

for-in 迭代器:到目前为止,for-in 语法主要用于数组,但它也适用于任何 Collection 对象。这样做的原因是 Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator() 方法。for-in 使用此 Iterable 接口来遍历序列。

适配器惯用法:如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 Iterable 对象的方法,该对象可以用于 for-in 语句。

注意:不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack 。

Java 集合框架简图:黄色为接口,绿色为抽象类,蓝色为具体类。虚线箭头表示实现关系,实线箭头表示继承关系。

collection

map

第十三章 函数式编程

Lambda 表达式:(params) -> { statements; },只有一个参数的时候,可以省略括号,如果只有一行的话,花括号应该省略。

方法引用:ClassName::MethodName

  • 未绑定的方法引用:未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象。
  • 构造函数的引用:ClassName::new

函数式接口:Lambda 表达式包含类型推导,但是如果存在(x, y) -> x + y这样的 lambda 表达式,编译器就不能自动进行类型推导了。因为 x, y 既可以是 String 类型,也可以是 int 类型。此时引入java.util.function包,包含了一组接口,每个接口只有一个抽象方法,称为函数式方法。Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。

  • 多参数函数式接口:在 function 包中,只有很少的接口,我们可以自己定义一个函数接口,如下:

    1
    2
    3
    4
    5
    6
    7
    // functional/TriFunction.java

    @FunctionalInterface
    public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
    }

高阶函数:消费或产生函数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// functional/ProduceFunction.java

import java.util.function.*;

interface FuncSS extends Function<String, String> {} // [1]

public class ProduceFunction {
static FuncSS produce() {
return s -> s.toLowerCase(); // [2]
}
public static void main(String[] args) {
FuncSS f = produce();
System.out.println(f.apply("YELLING"));
}
}

闭包:对外部变量引用的函数。

1
2
3
4
5
6
7
8
9
10
11
// functional/Closure1.java

import java.util.function.*;

public class Closure1 {
int i;
IntSupplier makeFun(int x) {
return () -> x + i++;
}
}

柯里化和部分求值:柯里化意为:将一个多参数的函数,转换为一系列单参数函数。

第十四章 流式编程

流式编程的特点:代码可读性更高;懒加载,意味着它只在绝对必要时才计算,由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。

流支持:Java 8 通过在接口中添加default修饰的方法实现流的平滑嵌入。流操作有三种类型:创建流,修改流元素(中间操作),消费流元素(终端操作)。

流创建:通过 Stream.of 将一组元素转化为流,除此之外,每个集合都可以通过调用 stream 方法来产生一个流。除此以外,还有:

  • 随机数流:new Random().ints()
  • int 类型流:IntStream.range(start, end, step)
  • generate:Stream.generate(obj)
  • iterate:Stream.iterate(initValue, cb)
  • 流的构造者模式:首先创建一个 builder 对象,然后将创建流所需的多个信息传递给它,最后builder 对象执行“创建”流的操作。
  • Arrays: Arrays.stream()

中间操作:用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。

  • peek:无修改地查看流中的元素
  • sorted:排序,可以使用 lambda 参数
  • distinct:消除重复元素
  • filter:通过过滤条件的被保存下来,否则删除
  • map:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。还有 mapToInt, mapToLong 等
  • flatMap:将产生流的函数应用在每个元素上(与 map() 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。对应还有 flatMapToInt 等

Optimal 类:一些标准流操作返回 Optional 对象,因为它们并不能保证预期结果一定存在。当流为空的时候你会获得一个 Optional.empty 对象,而不是抛出异常。

  • 解包 Optimal 的函数:ifPresent(Consumer), orElse(otherObject)
  • 创建 Optimal:静态方法有empty(), of(value), ofNullable(value)
  • Optimal 流:假设你的生成器可能产生 null 值,那么当用它来创建流时,你会自然地想到用 Optional 来包装元素。可以使用 filter() 来保留那些非空 Optional

终端操作:以下操作将会获取流的最终结果,终端操作(Terminal Operations)总是我们在流管道中所做的最后一件事。

  • 数组:toArray
  • 循环:forEach,forEachOrdered
  • 集合:collect
  • 组合:reduce
  • 匹配:allMatch,anyMatch,noneMatch
  • 查找:findFirst,findAny
  • 信息:count,max,min
  • 数字流信息:average,max,min

第十五章 异常

异常概念:C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。

异常捕获:

  • try 语句块:对可能产生异常的语句进行捕获
  • catch 语句块:对每种可能出现的异常准备相应的处理语句
  • 终止和恢复:Java 支持终止模型,这种模型假设错误非常严重,以至于程序无法返回到异常发生的地方继续执行。另外一种是恢复模型,如果 Java 想要实现类似恢复的行为,可以把 try 块放在循环里面,直到得到满意的结果

自定义异常:想要自定义异常类,必须从已有的异常类继承。

异常声明:在方法后面加上throws ExceptionType1,ExceptionType2,,,

捕获所有异常:直接捕获基类 Exception

  • 多重捕获:Java7 中支持,使用 | 连接不同类型的异常,如catch(Except1 | Except2 | Except3 | Except4 e) {}

  • 栈轨迹:printStackTrace

  • 重新抛出异常:

    1
    2
    3
    4
    catch(Exception e) {
    System.out.println("An exception was thrown");
    throw e;
    }

使用 finally 进行清理:不管是否发生异常,都会执行 finally 块里面的语句。

  • finally 作用:用于将资源恢复到初始状态
  • 在 return 中使用 finally:从何处返回无关紧要,finally 子句永远会执行
  • 异常丢失:finally 里面使用 return 将会导致不会抛出任何异常

Try-With-Resources 用法:try(){ }catch(Exception e){ },在 try 小括号里面的声明的对象需要实现java.lang.AutoCloseable接口,接口只有一个方法close()。退出 try 块会调用每个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。

异常匹配:异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。

第十七章 文件

文件和目录路径:一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,通过 Paths.get(URL) 来获得一个对应的 Path 对象。

  • 选取路径部分片段:getName
  • 路径分析:Files 工具类包含一系列完整的方法用于获得 Path 相关的信息,如exists,size,isDirectory
  • Paths 的增删修改:relative,resolve

遍历目录:Files.walkFileTree,具体操作实现由参数二 FileVisitor 里面的四个抽象方法决定:

  1. preVisitDirectory():在访问目录中条目之前在目录上运行。
  2. visitFile():运行目录中的每一个文件。
  3. visitFileFailed():调用无法访问的文件。
  4. postVisitDirectory():在访问目录中条目之后在目录上运行,包括所有的子目录。

文件系统:可以使用静态的 FileSystems 工具类获取默认的文件系统

路径监听:通过文件系统的 WatchService 可以设置一个进程对目录中的更改做出响应

文件查找:通过在 FileSystem 对象上调用 getPathMatcher 可以获得一个 PathMatcher,传入对应的两种模式:glob 或者 regex。

文件读写:如果文件比较小,使用 Files.readAllLines 可以一次性读取整个文件,返回一个 List<String>;使用 Files.write 写入 byte 数组或者任何可迭代对象。对于文件比较大的时候,可以使用 Files.lines 将文件转换为输入流。

第十八章 字符串

字符串的不可变性:String 对象是不可变的,String 类中的每个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象。

+ 的重载与 StringBuilder:在 String 中,+ 代表了字符串之间的 append 操作,每次 + 都会创建一个 String 对象,代价高昂。StringBuilder 提供了丰富全面的方法,包括insert,replace,append,delete。另外还有 StringBuffer,和 StringBuilder 不同点在于后者是线程不安全的,前者是线程安全的,从性能上看,可以优先使用 StringBuilder。

意外递归:如"som string" + this ,我们想要打印出某个字符串的地址,但是编译器首先辨别出左边是 String 对象,+ 要求右边的变量也是 String 对象(先进行转换),这就涉及到了意外递归。可以使用Object.toString()打印地址。

字符串操作:当需要改变字符串的内容时,String 类的方法都会返回一个新的 String 对象。同时,如果内容不改变,String 方法只是返回原始对象的一个引用而已。这可以节约存储空间以及避免额外的开销。

格式化输出:

  • System.out.printf, System.out.format

  • 格式化修饰符:%[argument_index$][flags][width][.precision]conversion

  • Formatter 转换:

    类型 含义
    d 整型(十进制)
    c Unicode字符
    b Boolean值
    s String
    f 浮点数(十进制)
    e 浮点数(科学计数)
    x 整型(十六进制)
    h 散列码(十六进制)
    % 字面值“%”
  • String.format

正则化表达式:

  • 在正则表达式中,用 \d 表示一位数字。如果在其他语言中使用过正则表达式,那你可能就能发现 Java 对反斜线 \ 的不同处理方式。在其他语言中,\\ 表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它任何特殊的意义。”而在Java中,\\ 的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。”例如,如果你想表示一位数字,那么正则表达式应该是 \\d。如果你想插入一个普通的反斜线,应该这样写 \\\。不过换行符和制表符之类的东西只需要使用单反斜线:\n\t。如果要表示“可能有一个负号,后面跟着一位或多位数字”,可以这样:

    1
    -?\\d+
  • 表达式:

    表达式 含义
    . 任意字符
    [abc] 包含abc的任何字符(和`a
    [^abc] abc之外的任何字符(否定)
    [a-zA-Z] az或从AZ的任何字符(范围)
    [abc[hij]] abchij中的任意字符(与`a
    [a-z&&[hij]] 任意hij(交)
    \s 空白符(空格、tab、换行、换页、回车)
    \S 非空白符([^\s]
    \d 数字([0-9]
    \D 非数字([^0-9]
    \w 词字符([a-zA-Z_0-9]
    \W 非词字符([^\w]
  • CharSequence:接口从 CharBuffer,String,StringBuffer,StringBuilder 抽象出了一般化定义

    1
    2
    3
    4
    5
    6
    7
    interface CharSequence {   
    char charAt(int i);
    int length();
    CharSequence subSequence(int start, int end);
    String toString();
    }

  • Pattern 和 Matcher:根据一个 String 对象生成一个 Pattern 对象,通过 Pattern 对象的 match 方法产生一个 Matcher 对象。

  • 组(group):A(B(C))D 中有三个组:组 0 是 ABCD,组 1 是 BC,组 2 是 C。通过 Matcher 对象的 group 方法可以获取到每个组。

第十九章 类型信息

Java 在运行时识别对象和类的信息的方式:传统的 RTTI(RunTime Type Information,运行时类型信息),反射机制。

RTTI 必要性:下面这个代码展示了 Shape 基类下的派生类的相关操作,使用 RTTI,我们可以在运行时进行类型确认,同时,编码的时候只需要注意对基类的处理就行,不会影响代码的可扩展性。

1
2
3
4
5
6
7
public class Shapes {
public static void main(String[] args) {
Stream.of(
new Circle(), new Square(), new Triangle())
.forEach(Shape::draw);
}
}

实际上,上述代码编译时候,Stream 和 Java 泛型系统确保放入的都是 Shape 对象或者其派生类,运行时,自动类型转换确保从 Stream 中取出的对象都是 Shape 类型。

Class 对象:Class 对象包含了与类有关的信息,每个类都会产生一个 Class 对象,每当编译一个新类,就会产生一个 Class 对象(保存在同名的 .class 文件中),为了生成该类的对象,JVM 首先会调用类加载器子系统将这个类加载到内存中。Java 是动态加载的,即只有在类需要的时候才会进行类的加载。所有的 Class 对象都属于 Class 类,可以通过Class.forName()来得到类的 Class 对象,或者通过someInstance.getClass()得到。

  • 类字面常量:对于一个 FancyToy.class 的文件,我们可以直接使用FancyToy.class得到对应的类对象,相较于Class.forName的方式,该种方式更加简单和安全,并且效率更高。另外,使用类字面常量的时候,不会自动初始化该 Class 对象。为了使用类的三个步骤:
    • 加载:查找字节码,并且创建一个 Class 对象
    • 链接:验证字节码,为 static 字段分配存储空间,如果需要,将解析这个类对其他类的引用
    • 初始化:先初始化基类,然后执行 static 初始化器和 static 初始化块
  • 泛化的 Class 引用:Class 引用总是指向某个 Class 对象,而 Class 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。使用 Class<?> 表示通配所有类型,Class<? extends Sup>表示通配所有 Sup 的派生类型,Class<? super Sub>表示通配 Sub 的基类。
  • cast 方法:接受参数对象,并将其类型转换为 Class 引用的类型。

类型转换检测:Java 中支持自动向上转型,但是向下转型是强制的,需要用户指代向下转换的类型,如果没有通过向下类型转换,就会报错,否则转型成功。另外,可以使用 instanceof 判断某个实例是否是某个对象的实例。Class.isInstance 可以动态测试对象类型。

类的等价比较:当查询类型信息的时候,使用 instanceof 或者 isInstance,这两种方式产生的结果相同,但是 Class 对象直接比较与上述方式不同。instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。

反射:运行时类信息。java.lang.reflect库中包含了相关的类来实现反射这一机制。RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法;而通过反射,.class 文件在编译时不可用,它由运行时环境打开并检查。

  • 类方法提取器:getMethods 和 getConstructors 获取对应的类方法

动态代理:代理是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作—这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。通过调用静态方法Proxy.newProxyInstance来创建动态代理。

接口和类型:interface 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。

第二十章 泛型

简单泛型:直接使用类型暂代符表示某种类型即可,下图是一个二元组:

1
2
3
4
5
6
7
8
// onjava/Tuple2.java
package onjava;

public class Tuple2<A, B> {
public final A a1;
public final B a2;
public Tuple2(A a, B b) { a1 = a; a2 = b; }
}

泛型接口:泛型可以应用于接口,例如生成器,这是一种专门负责创建对象的类。注意,Java 中支持基本类型作为泛型类型。

泛型方法:泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

1
2
3
4
5
6
7
// generics/GenericMethods.java

public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
}

泛型擦除:Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>List<Integer> 在运行时实际上是相同的类型。它们都被擦除成原生类型 List,使用 getClass 得到的结果相同。

  • 特殊方式:当我们想要调用某个泛型类型的方法的时候,我们可以使用<T extends Sup>,这样我们才能调用 Sup 里面的相关方法。
  • 擦除的问题:由于擦除的存在,所有关于参数的信息就丢失了。当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

补偿擦除:由于擦除的存在,我们无法在运行时知道参数的确切类型,为了解决这个问题,我们显式传递一个 Class 对象,以在类型表达式中使用。

  • 创建类型的实例:直接通过new T()是行不通的,但是我们可以通过对应的 Class 对象的 newInstance 方法来创建新的实例
  • 泛型数组:我们无法创建泛型数组,解决方式是在试图创建泛型数组的时候使用 ArrayList

边界:由于擦除会删除类型信息,因此唯一可用的无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字。

通配符:使用表示。使用<? extends Sup>表示继承自 Sup 的类,使用<? super Sub>表示 Sub 的基类,使用<?>表示任意一种类型。

使用泛型的问题:

  • 任何基本类型都不能作为类型参数

  • 一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口

  • 使用带有泛型类型参数的转型或 instanceof 不会有任何效果

自限定的类型:下图代码展示了这种惯用法:

1
class SelfBounded<T extends SelfBounded<T>> { // ...
  • 古怪的循环泛型(CRG):

    1
    2
    3
    4
    5
    6
    // generics/CuriouslyRecurringGeneric.java

    class GenericType<T> {}

    public class CuriouslyRecurringGeneric
    extends GenericType<CuriouslyRecurringGeneric> {}
  • 自限定:

    1
    class A extends SelfBounded<A>{}
  • 参数协变:自限定类型的价值在于它们可以产生协变参数类型,即方法参数类型会随子类而变化。

动态类型安全:Java5 中的 Collections 有一组便利工具函数,可以解决类型检查问题,如checkedMap等。

泛型异常:由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。

混型(Mixin):最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。

  • 与接口混合
  • 使用装饰器模式
  • 与动态代理混合

潜在类型机制:也称作鸭子类型机制,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当作鸭子对待”。

第二十一章 数组

数组特性:效率,类型,保存基本数据类型的能力。

一等对象:不管使用什么类型的数组,数组中的数据集实际上都是对堆中真正对象的引用。注意区分聚合初始化和动态聚合初始化。

返回数组:在 Java 中,可以直接返回一个数组,而不用担心其内存消耗情况,垃圾回收期会自动处理。

多维数组:使用多层方括号界定每个维度的大小,同样的也存在有不规则的数组。可以使用 Arrays.deepToString 来查看多维数组里面的内容。Arrays.setAll 方法用于初始化数组。

泛型数组:数组和泛型并不能很好的结合,不能实例化参数化类型的数组,但是允许您创建对此类数组的引用。

Arrays 相关方法:

  • fill:将单个值复制到整个数组,或者在对象数组的情况下,将相同的引用复制到整个数组
  • setAll:使用一个生成器用于生成不同的值,生成器的参数是 int 数组索引
  • asList:将数组转换为列表
  • copyOf:以新的长度复制数组
  • copyOfRange:复制现有数组的一部分数据
  • equals:判断数组是否相同
  • deepEquals:多维数组相等性比较
  • stream:生成流
  • sort:排序
  • binarySearch:二分查找
  • toString,deepToString:数组的字符串表示

数组元素修改:通过使用 setAll 方法来索引现有数据元素

第二十二章 枚举

基本 enum 特性:调用 values 方法,可以返回对应的数组,同时调用 ordinal 方法可以知道某个 value 的次序,这个次序默认从 0 开始。可以使用 import static 导入 enum 类型。

方法添加:除了不能继承自一个 enum 之外,我们基本上可以将 enum 看作一个常规的类。如果你打算定义自己的方法,那么必须在 enum 实例序列的最后添加一个分号。

switch 语句中的 enum:enum 的 values 本来就具有顺序,可以搭配 switch 使用。

values 方法:enum 类型的对象会有一个 values 方法,这个方法是由编译器添加的 static 方法。

实现而非继承:enum 继承自 Enum 类,由于 Java 不支持多继承,enum 不能再次继承其他类,但是创建一个新的 enum 时,可以实现一个或者多个接口。

使用接口组织枚举:无法从 enum 继承子类很令人沮丧,但是我们可以尝试使用接口来组织枚举类。

使用 EnumSet 代替 Flags:EnumSet 中的元素必须来自一个 enum。EnumSet 基础是 long,只有 64 位,但是在需要的时候,会增加一个 long。

使用 EnumMap:要求键必须来自一个 enum,EnumMap 在内部使用数组实现。

使用 enum 的状态机:枚举类型很适合用于创建状态机。

第二十三章 注解

java.lang 中的注解包括:@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface。

基本语法:

  • 定义注解:注解的定义看起来很像接口的定义,事实上,他们和其他 Java 接口一样,也会被编译成 class 文件。

    1
    2
    3
    4
    5
    6
    7
    // onjava/atunit/Test.java
    // The @Test tag
    package onjava.atunit;
    import java.lang.annotation.*;
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Test {}

    @target 标示注解的对象,@Retention 标示注解在哪里可用。

  • 元注解:@target,@Retention,@Documented,@Inherited,@Repeatable

编写注解处理器:使用反射机制 API 实现注解的读取。

  • 注解元素:基本类型,String,Class,enum,Annotation,以上类型的数组
  • 默认值限制:首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。任何非基本类型的元素,无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。
  • 生成外部文件:Web Service,自定义标签库以及对象/关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中提供过的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。
  • 注解不支持继承:不能使用 extends 关键字来继承 @interfaces。
  • 实现处理器:通过 getAnnotation 来检查是否存在注解,如果存在的话做出相应的操作

基于注解的单元测试:如 JUnit。