数据存储

Java程序运行期间,有5个地方可以存储数据

  • 寄存器:速度最快,Java没有直接控制器,根据需求进行分配
  • 栈内存:存在于常规内存RAM中,仅次于寄存器,Java必须知道存在于栈内存中数据的生命周期,通常用于存储一些数据的引用(对象引用)
  • 堆内存:通用的内存池,存在于RAM中,所有的Java对象都存在其中。Java不知道数据必须在堆内存中停留多长的时间,用new实例化对象的时候,会自动在堆中分配(分配与清理堆内存要比栈花费更多的时间)
  • 常量存储:存在于程序代码中,如果需要严格保护,可以存在于ROM中
  • 非RAM存储:数据完全存在于程序之外,在程序未运行或者脱离程序控制后依然存在。例子:① 序列化对象。② 持久化对象 。这种存储方式将对象存放于另一个介质中,并在需要时恢复成常规的,基于RAM的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。

基本类型存储

基本类型不通过new来创建,而是使用一个自动的变量值,这个变量自动存储值,并且置于栈内存中。

基本类型 大小 最小值 最大值 包装类型
boolean Boolean
char 16 bits Unicode 0 Unicode 216 -1 Character
byte 8 bits -128 +127 Byte
short 16 bits - 215 + 215 -1 Short
int 32 bits - 231 + 231 -1 Integer
long 64 bits - 263 + 263 -1 Long
float 32 bits IEEE754 IEEE754 Float
double 64 bits IEEE754 IEEE754 Double
void Void

如果希望在堆内存中表示基本类型的数据,需要使用对应的保证类

作用域

作用域是用{}的位置来决定的,Java变量只有在其作用域内才可用

对象作用域

使用new在{}中创建一个新的对象,对象在作用域外并不会消失,消失的只是指向对象的引用

基本类型初始化

存在于类中的基本类型,会在Java初始化类的时候被自动初始化

基本类型 初始值
boolean false
char \u0000 (null)
byte (byte) 0
short (short) 0
int 0
long 0L
float 0.0f
double 0.0d

但是,局部的基本类型不会被初始化

方法

方法名和参数列表统称为方法签名(signature of the method)。签名作为方法的唯一标识。

调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。

命名规范

包名都需要小写

类名和变量名都使用驼峰命名法

其中类名首字母大写,变量名首字母小写

移位运算符

如果移动 charbyteshort,则会在移动发生之前将其提升为 int,结果为 int

移位可以与等号 <<=>>=>>>= 组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与 byteshort 一起使用的话,则结果错误。取而代之的是,它们被提升为 int 型并右移,但在重新赋值时被截断

重载与基本类型

基本类型可以自动从较小的类型转型为较大的类型。

引用计数

一种简单但速度很慢的垃圾回收机制叫做引用计数

垃圾回收机制

停止-复制(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。

效率低下主要因为两个原因。

其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。

其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。

标记-清扫(mark-and-sweep)。“标记-清扫”所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。

Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到”标记-清扫”方式。同样,Java 虚拟机会跟踪”标记-清扫”的效果,如果堆空间出现很多碎片,就会切换回”停止-复制”方式。这就是”自适应”的由来,你可以给它个啰嗦的称呼:”自适应的、分代的、停止-复制、标记-清扫”式的垃圾回收器。

(Just-In-Time, JIT)编译器的技术

这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快

你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。

另一种做法称为惰性评估,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快

创建对象的过程

假设有个名为 Dog 的类:

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

可变参数列表

编译器会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。

应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它

枚举类型

在你创建 enum 时,编译器会自动添加一些有用的特性。例如,它会创建 toString() 方法,以便你方便地显示某个 enum 实例的名称,这从上面例子中的输出可以看出。编译器还会创建 ordinal() 方法表示某个特定 enum 常量的声明顺序,static values() 方法按照 enum 常量的声明顺序,生成这些常量值构成的数组

封装

包的概念

访问修饰符(access specifier)

访问控制权限的等级,从“最大权限”到“最小权限”依次是:publicprotected包访问权限(package access)(没有关键字)和 private

类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 package 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。

之所以使用导入,是为了提供一种管理命名空间的机制,类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合

一个 Java 源代码文件称为一个编译单元(compilation unit)(有时也称翻译单元(translation unit)

每个编译单元的文件名后缀必须是 .java。在编译单元中可以有一个 public 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 .java)。每个编译单元中只能有一个 public 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 public 类,此时它们为主 public 类提供“支持”类 。

在 Java 中,可运行程序是一组 .class 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 jar 文档生成器)。Java 解释器负责查找、加载和解释这些文件。

package hiding;

意味着这个编译单元是一个名为 hiding 类库的一部分。换句话说,你正在声明的编译单元中的 public 类名称位于名为 hiding 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 import 关键字导入 hiding 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同

创建独一无二的包名

一个包从未真正被打包成单一的文件,它可以由很多 .class 文件构成,因而事情就变得有点复杂了。为了避免这种情况,一种合乎逻辑的做法是将特定包下的所有 .class 文件都放在一个目录下。

此技巧的第二部分是把 package 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 .class 文件所在的位置。

首先,它找出环境变量 CLASSPATH(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。CLASSPATH 包含一个或多个目录,用作查找 .class 文件的根目录。

从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 foo.bar.baz 变成 foo\bar\bazfoo/bar/baz 或其它)。然后这个路径与 CLASSPATH 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 .class 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。

使用包的忠告

当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。

编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。

访问权限修饰符

包访问权限

默认访问权限没有关键字,通常被称为包访问权限(package access)(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 private 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。

接口和实现

访问控制通常被称为隐藏实现(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是封装(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。

复用

初始化基类

构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化

即使不为 派生类 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数

带参数的构造函数

如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用:

对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)

保证适当的清理

Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。

当你必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 finalize()

@Override注解:它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法,使用与基类中完全相同的方法签名和返回类型时,你可以选择添加这个注解,如果你不小心用了重载而不是重写,编译器会产生一个错误消息

向上转型

继承图中派生类转型为基类是向上的,所以通常称作向上转型。因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全的。

final关键字

常它指的是“这是不能被改变的”。防止改变有两个原因:设计或效率。

final 数据

  1. 一个永不改变的编译时常量。
  2. 一个在运行时初始化就不会改变的值。

对于编译时常量这种情况,编译器可以把常量带入计算中;也就是说,可以在编译时计算,减少了一些运行时的负担。在 Java 中,这类常量必须是基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值。

对于对象引用,final 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法

final 参数

在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:

final 方法

使用 final 方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。

过去建议使用 final 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。

应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 final

final 和 private

类中所有的 private 方法都隐式地指定为 final。因为不能访问 private 方法,所以不能覆写它。可以给 private 方法添加 final 修饰,但是并不能给方法带来额外的含义。

“覆写”只发生在方法是基类的接口时。也就是说,必须能将一个对象向上转型为基类并调用相同的方法(这一点在下一章阐明)。如果一个方法是 private 的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在派生类中以相同的命名创建了 public,protected 或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。由于 private 方法无法触及且能有效隐藏,除了把它看作类中的一部分,其他任何事物都不需要考虑到它。

final 类

当说一个类是 finalfinal 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。

final 类的属性可以根据个人选择是或不是 final。同样,非 final 类的属性也可以根据个人选择是或不是 final。然而,由于 final 类禁止继承,类中所有的方法都被隐式地指定为 final,所以没有办法覆写它们。你可以在 final 类中的方法加上 final 修饰符,但不会增加任何意义。

类初始化和加载

每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载。一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 static 属性或方法。

构造器也是一个 static 方法尽管它的 static 关键字是隐式的。因此,准确地说,一个类当它任意一个 static 成员被访问时,就会被加载。

首次使用时就是 static 初始化发生时。所有的 static 对象和 static 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。static 变量只被初始化一次。

继承和初始化

// reuse/Beetle.java
// The full process of initialization
class Insect {
private int i = 9;
protected int j;
Insect() {
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = printInit("static Insect.x1 initialized");
static int printInit(String s) {
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k.initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
//输出:
/*
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/

当执行 java Beetle,首先会试图访问 Beetle 类的 main() 方法(一个静态方法),加载器启动并找出 Beetle 类的编译代码(在名为 Beetle.class 的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)

如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是 Insect)的 static 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中 static 的初始化可能依赖基类成员是否被正确地初始化。

至此,必要的类都加载完毕,对象可以被创建了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 null —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 super 调用指定的基类构造器(在 Beetle 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。

顺序

  • 首先是类的加载,先加载基类,在加载派生类,加载类的过程,首先给所有的静态变量赋初始值,之后加载静态方法,然后初始化静态变量,这些都只会在加载类的时候执行一次
  • 然后是实例化,实例化首先给基类的非静态变量赋默认值,然后初始化非静态变量,初始化完成后调用构造函数,基类完成后,依次调用派生类的实例化

方法调用绑定

将一个方法调用和一个方法主体关联起来称作绑定

*后期绑定:在运行时根据对象的类型进行绑定。后期绑定也称为*动态绑定运行时绑定。**

方法调用的具体实现取决于实际运行时对象的类型,而不是在编译时确定

实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用

Java 中除了 staticfinal 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后期绑定是否会发生——它自动发生。

多态是一项“将改变的事物与不变的事物分离”的重要技术

任何属性访问都被编译器解析,因此不是多态的

默认的属性是对应的引用下的属性,而不是实例化对象的属性

// polymorphism/FieldAccess.java
// Direct field access is determined at compile time
class Super {
public int field = 0;
public int getField() {
return field;
}
}
class Sub extends Super {
public int field = 1;
@Override
public int getField() {
return field;
}
public int getSuperField() {
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
System.out.println("sup.field = " + sup.field +
", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field +
", sub.getField() = " + sub.getField()
+ ", sub.getSuperField() = " + sub.getSuperField())
}
}
//输出:
/*
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/

Sub 对象向上转型为 Super 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中,Super.fieldSub.field 被分配了不同的存储空间,因此,Sub 实际上包含了两个称为 field 的属性:它自己的和来自 Super 的。然而,在引用 Subfield 时,默认的 field 属性并不是 Super 版本的 field 属性。为了获取 Superfield 属性,需要显式地指明 super.field

静态的方法只与类关联,与单个的对象无关

构造器和多态

构造器调用顺序

在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错

对象的构造器调用顺序如下:

  1. 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
  2. 按声明顺序初始化成员。
  3. 调用派生类构造器的方法体。

继承和清理

销毁的顺序应该与初始化的顺序相反,以防一个对象依赖另一个对象。对于属性来说,就意味着与声明的顺序相反(因为属性是按照声明顺序初始化的)。对于基类(遵循 C++ 析构函数的形式),首先进行派生类的清理工作,然后才是基类的清理。这是因为派生类的清理可能调用基类的一些方法,所以基类组件这时得存活,不能过早地被销毁。

一旦某个成员对象被其它一个或多个对象共享时,问题就变得复杂了,不能只是简单地调用 dispose()。这里,也许就必须使用引用计数来跟踪仍然访问着共享对象的对象数量

构造器内部多态方法的行为

编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的 final 方法(这也适用于可被看作是 finalprivate 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力

协变返回类型

表示派生类的被重写方法可以返回基类方法返回类型的派生类型

使用继承设计

有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。

向下转型与运行时类型信息

由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用向下转型

在 Java 中,每次转型都会被检查!所以即使只是进行一次普通的加括号形式的类型转换,在运行时这个转换仍会被检查,以确保它的确是希望的那种类型。

接口

接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。

抽象类和方法

对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具

创建一个抽象类是为了通过通用接口操纵一系列类。

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。下面是抽象方法的声明语法:

abstract void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。

// interface/Basic.java
abstract class Basic {
abstract void unimplemented();
}

如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。

可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。

留意 @Override 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。因此,你可能认为这里的 @Override 是多余的。但是,@Override 还提示了这个方法被覆写——我认为这是有用的,所以我会使用 @Override,不仅仅是因为当没有这个注解时,编译器会告诉我出错。

创建抽象类和抽象方法是有帮助的,因为它们使得类的抽象性很明确,并能告知用户和编译器使用意图。抽象类同时也是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法。

接口创建

一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。

Java 8 允许接口包含默认方法和静态方法

接口同样可以包含属性,这些属性被隐式指明为 staticfinal

当实现一个接口时,来自接口中的方法必须被定义为 public。否则,它们只有包访问权限,这样在继承时,它们的可访问权限就被降低了,这是 Java 编译器所不允许的

默认方法

Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。

关键字 default 允许在接口中提供方法实现——在 Java 8 之前被禁止。

增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法虚拟扩展方法

多继承

多继承意味着一个类可能从多个父类型中继承特征和特性。

返回类型不是方法签名的一部分,因此不能用来区分方法。为了解决这个问题,需要覆写冲突的方法

接口中的静态方法

Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具:

抽象类和接口

尤其是在 Java 8 引入 default 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分:

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

有一条实际经验:在合理的范围内尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。

完全解耦

每当一个方法与一个类而不是接口一起工作时(当方法的参数是类而不是接口),你只能应用那个类或它的子类。如果你想把这方法应用到一个继承层次之外的类,是做不到的。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。

多接口结合

使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它

结合接口时的命名冲突

打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

接口嵌套

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

实现 private 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式

当实现某个接口时,并不需要实现嵌套在其内部的接口。同时,private 接口不能在定义它的类之外被实现

小结

任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。

恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。

内部类

使用 .this 和 .new

如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。

// 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();
}
}

要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。

在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。

内部类与向上转型

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

private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码(因为私有的,或者protected的内部类的访问是受到限制的),并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。

内部类方法和作用域

局部内部类

// innerclasses/Parcel5.java
// Nesting a class within a method
public class Parcel5 {
public Destination destination(String s) {
final class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
@Override
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
Destination d = p.destination("Tasmania");
}
}

PDestination 类是 destination() 方法的一部分,而不是 Parcel5 的一部分。所以,在 destination() 之外不能访问 PDestination,注意出现在 return 语句中的向上转型-返回的是 Destination 的引用,它是 PDestination 的基类。当然,在 destination() 中定义了内部类 PDestination,并不意味着一旦 destination() 方法执行完毕,PDestination 就不可用了。

匿名内部类

// innerclasses/Parcel7.java
// Returning an instance of an anonymous inner class
public class Parcel7 {
public Contents contents() {
return new Contents() { // Insert class definition
private int i = 11;
@Override
public int value() { return i; }
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}

这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:

// innerclasses/Parcel7b.java
// Expanded version of Parcel7.java
public class Parcel7b {
class MyContents implements Contents {
private int i = 11;
@Override
public int value() { return i; }
}
public Contents contents() {
return new MyContents();
}
public static void main(String[] args) {
Parcel7b p = new Parcel7b();
Contents c = p.contents();
}
}

嵌套类

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

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

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

接口内部的类

嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 publicstatic 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外部接口

// innerclasses/ClassInInterface.java
// {java ClassInInterface$Test}
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
@Override
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}

从多层嵌套类中访问外部类的成员

// innerclasses/MultiNestingAccess.java
// Nested classes can access all members of all
// levels of the classes they are nested within
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}

为什么需要内部类

一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外部类的对象。所以可以认为内部类提供了某种进入其外部类的窗口。

内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外部类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。

那么内部类实现一个接口与外部类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:

每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)。

如果拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承:

// innerclasses/MultiImplementation.java
// For concrete or abstract classes, inner classes
// produce "multiple implementation inheritance"
// {java innerclasses.MultiImplementation}
package innerclasses;
class D {}
abstract class E {}
class Z extends D {
E makeE() {
return new E() {};
}
}
public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args) {
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}

如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
  2. 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。 稍后就会展示一个这样的例子。
  3. 创建内部类对象的时刻并不依赖于外部类对象的创建
  4. 内部类并没有令人迷惑的”is-a”关系,它就是一个独立的实体。

闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。

内部类是面向对象的闭包,因为它不仅包含外部类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。

在 Java 8 之前,内部类是实现闭包的唯一方式。在 Java 8 中,我们可以使用 lambda 表达式来实现闭包行为,并且语法更加优雅和简洁

闭包是一个函数与其相关引用环境(包含非局部变量)的组合。它可以被作为参数传递,赋值给变量,或者作为函数的返回值。闭包能够捕获其定义所在作用域的状态,即使在定义的作用域之外被调用,也能够访问和操作其定义时所捕获的变量

内部类 实现了 特定的接口,以提供一个返回 外部类实例对象的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 特定的接口 的引用,都只能调用 接口规定的方法,除此之外没有其他功能

Callback 的构造器需要一个 特定的接口 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Callback 对象可以使用此引用回调 实现特定接口的 类。

回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。例如,在图形界面实现 GUI 功能的时候,到处都用到回调。

内部类与控制框架

应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类

运用某个应用程序框架,通常是继承一个或多个类,并重写某些方法。你在重写的方法中写的代码定制了该应用程序框架提供的通用解决方案,来解决你的具体问题

继承内部类

因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:

// 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);
}
}

可以看到,InheritInner 只继承自内部类,而不是外部类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外部类对象的引用。此外,必须在构造器内使用如下语法:

enclosingClassReference.super();

内部类可以被重写么?

重写”内部类就好像它是外部类的一个方法,其实并不起什么作用

当继承了某个外部类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。

局部内部类

局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化。

使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象

一个匿名内部类无法被多次创建的意思是,无论你调用多少次方法创建匿名内部类的实例,实际上都是同一个类的不同实例,而局部内部类,这可以根据构造器来创建不同的实例

内部类标识符

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与 “$” 的后面。

另一方面,**$** 对Unix shell来说是一个元字符,所以当你列出.class文件时,有时会遇到麻烦。这对基于Unix的Sun公司来说有点奇怪。我的猜测是,他们没有考虑这个问题,而是认为你会很自然地关注源代码文件。

小结

比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。然而,多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了

集合

Object 默认的 toString() 方法打印类名,后边跟着对象的散列码的无符号十六进制表示(这个散列码是通过 hashCode() 方法产生的)

基本概念

Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:

  1. 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
  2. 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 Map 是强大的编程工具。

Collection 接口概括了序列的概念——一种存放一组对象的方式。

可以使用 for-in 语法来遍历所有的 Collection ,就像这里所展示的那样。在本章的后续部分,还将学习到一个更灵活的概念,迭代器

添加元素组

java.util 包中的 ArraysCollections 类中都有很多实用的方法,可以在一个 Collection 中添加一组元素。

Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。 Collections.addAll() 方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection 中。

直接使用 Arrays.asList() 的输出作为一个 List ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 List 上调用 add()remove(),由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)

集合的打印

必须使用 Arrays.toString() 来生成数组的可打印形式。

Collection 类型在每个槽中只能保存一个元素。此类集合包括: List ,它以特定的顺序保存一组元素; Set ,其中元素不允许重复; Queue ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 Map 在每个槽中存放了两个元素,即 (key)和与之关联的 (value)。

默认的打印行为,使用集合提供的 toString() 方法即可生成可读性很好的结果。 Collection 打印出的内容用方括号括住,每个元素由逗号分隔。 Map 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。

键和值保存在 HashMap 中的顺序不是插入顺序,因为 HashMap 实现使用了非常快速的算法来控制顺序。 TreeMap 把所有的键按照比较规则来排序, LinkedHashMap 在保持 HashMap 查找速度的同时按照键的插入顺序来排序。

列表List

有两种类型的 List

  • 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
  • LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。

contains():确定对象是否在列表

remove():删除一个对象,传递该对象的引用

indexOf():在 List 中找到该对象所在位置的下标号

注意 List 行为会根据 equals() 行为而发生变化,当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals() 方法

subList()

containsAll()

Collections.sort() Collections.shuffle()

retainAll() 方法实际上是一个“集合交集”操作,所产生的结果行为依赖于 equals() 方法

removeAll() 方法也是基于 equals() 方法运行的

set() 方法的命名显得很不合时宜,因为它与 Set 类存在潜在的冲突。在这里使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)

对于 List ,有一个重载的 addAll() 方法可以将新列表插入到原始列表的中间位置,而不是仅能用 CollectionaddAll() 方法将其追加到列表的末尾。

isEmpty()

clear()

toArray()

迭代器Iterators

Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:

  1. 使用 iterator() 方法要求集合返回一个 IteratorIterator 将准备好返回序列中的第一个元素。
  2. 使用 next() 方法获得序列中的下一个元素。
  3. 使用 hasNext() 方法检查序列中是否还有元素。
  4. 使用 remove() 方法将迭代器最近返回的那个元素删除。

Iterator 还可以删除由 next() 生成的最后一个元素,这意味着在调用 remove() 之前必须先调用 next()

在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。

现在考虑创建一个 display() 方法

Iterator 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。

ListIterator

ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。

它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。可以通过调用 listIterator() 方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator

链表LinkedList

LinkedList 也像 ArrayList 一样实现了基本的 List 接口,但它在 List 中间执行插入和删除操作时比 ArrayList 更高效。然而,它在随机访问操作效率方面却要逊色一些。

LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)

  • getFirst()element() 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。 peek() 方法与这两个方法只是稍有差异,它在列表为空时返回 null
  • removeFirst()remove() 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。 poll() 稍有差异,它在列表为空时返回 null
  • addFirst() 在列表的开头插入一个元素。
  • offer()add()addLast() 相同。 它们都在列表的尾部(末尾)添加一个元素。
  • removeLast() 删除并返回列表的最后一个元素。

堆栈Stack

堆栈是“后进先出”(LIFO)集合。它有时被称为叠加栈(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。

Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法:

集合Set

实际上, Set 就是一个 Collection ,只是行为不同。

早期 Java 版本中的 HashSet 产生的输出没有明显的顺序。这是因为出于对速度的追求, HashSet 使用了散列

散列算法有改动,以至于现在Integer 是有序的。但是,您不应该依赖此行为

HashSet 维护的顺序与 TreeSetLinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。 TreeSet 将元素存储在红-黑树数据结构中,而 HashSet 使用散列函数。 LinkedHashSet 也使用散列来提高查询速度,但是似乎使用了链表来维护元素的插入顺序。

要对结果进行排序,一种方法是使用 TreeSet 而不是 HashSet

最常见的操作之一是使用 contains() 测试成员归属性

映射Map

Map 可以返回由其键组成的 Set ,由其值组成的 Collection ,或者其键值对的 Set

如果键不在集合中,则 get() 返回 null (这意味着该数字第一次出现)。否则, get() 会为键生成与之关联的 Integer

队列Queue

LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。 通过将 LinkedList 向上转换为 Queue

offer()Queue 的特有方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 falsepeek()element() 都返回队头元素而不删除它,但如果队列为空,则 peek() 返回 null , 而 element() 抛出 NoSuchElementExceptionpoll()remove() 都删除并返回队头元素,但如果队列为空,则 poll() 返回 null ,而 remove() 抛出 NoSuchElementException

优先级队列PriorityQueue

优先级队列 :下一个弹出的元素是最需要的元素(具有最高的优先级)

当在 PriorityQueue 上调用 offer() 方法来插入一个对象时,该对象会在队列中被排序默认的排序使用队列中对象的自然顺序(natural order),但是可以通过提供自己的 Comparator 来修改这个顺序。 PriorityQueue 确保在调用 peek()poll()remove() 方法时,获得的元素将是队列中优先级最高的元素。

PriorityQueue 是允许重复的,最小的值具有最高的优先级(如果是 String ,空格也可以算作值,并且比字母的优先级高)。

Collections.reverseOrder() (Java 5 中新添加的)产生反序的 Comparator

最后一部分添加了一个 HashSet 来消除重复的 Character

IntegerStringCharacter 可以与 PriorityQueue 一起使用,因为这些类已经内置了自然排序。如果想在 PriorityQueue 中使用自己的类,则必须包含额外的功能以产生自然排序,或者必须提供自己的 Comparator

集合与迭代器

Collection 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”(incidental interface),即因为要表示其他若干个接口的共性而出现的接口。

java.util.AbstractCollection 类提供了 Collection 的默认实现

在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 Collection 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 Collection 就意味着需要提供 iterator() 方法

实现一个不是 Collection 的外部类时,由于让它去实现 Collection 接口可能非常困难或麻烦,因此使用 Iterator 就会变得非常吸引人

生成 Iterator 是将序列与消费该序列的方法连接在一起的耦合度最小的方式,并且与实现 Collection 相比,它在序列类上所施加的约束也少得多

// collections/NonCollectionSequence.java
import typeinfo.pets.*;
import java.util.*;
class PetSequence {
protected Pet[] pets = Pets.array(8);
}
public class NonCollectionSequence extends PetSequence {
public Iterator<Pet> iterator() {
return new Iterator<Pet>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < pets.length;
}
@Override
public Pet next() { return pets[index++]; }
@Override
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
NonCollectionSequence nc =
new NonCollectionSequence();
InterfaceVsIterator.display(nc.iterator());
}
}
/* Output:
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug
7:Manx
*/

for-in和迭代器

到目前为止,for-in 语法主要用于数组,但它也适用于任何 Collection 对象。

原因是 Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iteratoriterator() 方法。for-in 使用此 Iterable 接口来遍历序列

iterator() 返回的是实现了Iterator 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词

在 Java 5 中,许多类都是 Iterable ,主要包括所有的 Collection 类(但不包括各种 Maps

for-in 语句适用于数组或者其它任何 Iterable ,但这并不代表数组一定是 Iterable ,也不会发生任何自动装箱

尝试将数组作为一个 Iterable 参数传递会导致失败。这说明不存在任何从数组到 Iterable 的自动转换; 必须手工执行这种转换。

适配器方法惯用法

若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用重写,相反,而是添加了一个能够生成 Iterable 对象的方法,该对象可以用于 for-in 语句。这使得我们可以提供多种使用 for-in 语句的方式:

// collections/AdapterMethodIdiom.java
// The "Adapter Method" idiom uses for-in
// with additional kinds of Iterables
import java.util.*;
class ReversibleArrayList<T> extends ArrayList<T> {
ReversibleArrayList(Collection<T> c) {
super(c);
}
public Iterable<T> reversed() {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
public boolean hasNext() {
return current > -1;
}
public T next() { return get(current--); }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
}
public class AdapterMethodIdiom {
public static void main(String[] args) {
ReversibleArrayList<String> ral =
new ReversibleArrayList<String>(
Arrays.asList("To be or not to be".split(" ")));
// Grabs the ordinary iterator via iterator():
for(String s : ral)
System.out.print(s + " ");
System.out.println();
// Hand it the Iterable of your choice
for(String s : ral.reversed())
System.out.print(s + " ");
}
}
/* Output:
To be or not to be
be to not or be To
*/

在主方法中,如果直接将 ral 对象放在 for-in 语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 reversed() 方法,它会产生不同的行为。

通过使用这种方式,可以在 IterableClass.java 示例中添加两种适配器方法:

// collections/MultiIterableClass.java
// Adding several Adapter Methods
import java.util.*;
public class MultiIterableClass extends IterableClass {
public Iterable<String> reversed() {
return new Iterable<String>() {
public Iterator<String> iterator() {
return new Iterator<String>() {
int current = words.length - 1;
public boolean hasNext() {
return current > -1;
}
public String next() {
return words[current--];
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
public Iterable<String> randomized() {
return new Iterable<String>() {
public Iterator<String> iterator() {
List<String> shuffled =
new ArrayList<String>(Arrays.asList(words));
Collections.shuffle(shuffled, new Random(47));
return shuffled.iterator();
}
};
}
public static void main(String[] args) {
MultiIterableClass mic = new MultiIterableClass();
for(String s : mic.reversed())
System.out.print(s + " ");
System.out.println();
for(String s : mic.randomized())
System.out.print(s + " ");
System.out.println();
for(String s : mic)
System.out.print(s + " ");
}
}
/* Output:
banana-shaped. be to Earth the know we how is that And
is banana-shaped. Earth that how the be And we know to
And that is how we know the Earth to be banana-shaped.
*/

从输出中可以看到, Collections.shuffle() 方法不会影响到原始数组,而只是打乱了 shuffled 中的引用。之所以这样,是因为 randomized() 方法用一个 ArrayListArrays.asList() 的结果包装了起来

Arrays.asList() 生成一个 List 对象,该对象使用底层数组作为其物理实现。如果对 List 对象做了任何修改,又不想让原始数组被修改,那么就应该在另一个集合中创建一个副本。

小结

Java 提供了许多保存对象的方法:

  1. 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。
  2. Collection 保存单一的元素,而 Map 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 Collection 和各种 Map 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
  3. 像数组一样, List 也将数字索引与对象相关联,因此,数组和 List 都是有序集合。
  4. 如果要执行大量的随机访问,则使用 ArrayList ,如果要经常从表中间插入或删除元素,则应该使用 LinkedList
  5. 队列和堆栈的行为是通过 LinkedList 提供的。
  6. Map 是一种将对象(而非数字)与对象相关联的设计。 HashMap 专为快速访问而设计,而 TreeMap 保持键始终处于排序状态,所以没有 HashMap 快。 LinkedHashMap 按插入顺序保存其元素,但使用散列提供快速访问的能力。
  7. Set 不接受重复元素。 HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。 LinkedHashSet 按插入顺序保存其元素,但使用散列提供快速访问的能力。
  8. 不要在新代码中使用遗留类 VectorHashtableStack

simple collection taxonomy

可以看到,实际上只有四个基本的集合组件: MapListSetQueue ,它们各有两到三个实现版本(Queuejava.util.concurrent 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。

虚线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的虚线表示特定的类实现了一个接口。实心箭头表示某个类可以生成箭头指向的类的对象。例如,任何 Collection 都可以生成 IteratorList 可以生成 ListIterator (也能生成普通的 Iterator ,因为 List 继承自 Collection )。

TreeSet 之外的所有 Set 都具有与 Collection 完全相同的接口。ListCollection 存在着明显的不同,尽管 List 所要求的方法都在 Collection 中。另一方面,在 Queue 接口中的方法是独立的,在创建具有 Queue 功能的实现时,不需要使用 Collection 方法。最后, MapCollection 之间唯一的交集是 Map 可以使用 entrySet()values() 方法来产生 Collection

标记接口 java.util.RandomAccess 附加到了 ArrayList 上,但不附加到 LinkedList 上这为根据特定 List 动态改变其行为的算法提供了信息。

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

collection

map

函数式编程

程序员通过修改内存中的代码,使程序可以执行不同的操作,这种技术被称为自修改代码 (self-modifying code)

纯粹的自修改代码造成的结果就是:我们很难确定程序在做什么。它也难以测试

使用代码以某种方式操纵其他代码,不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。

这就是函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。

纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。

通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?结论是:只要能将代码传递给方法,我们就可以控制它的行为。

我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。(内部类,回调和闭包)

Lambda表达式

Java 8 的 Lambda 表达式,其参数和函数体被箭头 -> 分隔开。箭头右侧是从 Lambda 返回的表达式。它与单独定义类和采用匿名内部类是等价的,但代码少得多。

Java 8 的方法引用,它以 :: 为特征。 :: 的左边是类或对象的名称, :: 的右边是方法的名称,但是没有参数列表。

Lambda 表达式是使用最小可能语法编写的函数定义:

  1. Lambda 表达式产生函数,而不是类。 虽然在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是类,但是幕后有各种操作执行让 Lambda 看起来像函数 —— 作为程序员,你可以高兴地假装它们“就是函数”。
  2. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。

任何 Lambda 表达式的基本语法是:

  1. 参数。
  2. 接着 ->,可视为“产出”。
  3. -> 之后的内容都是方法体。
    • [1] 当只用一个参数,可以不需要括号 ()。 然而,这是一个特例。
    • [2] 正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。
    • [3] 如果没有参数,则必须使用括号 () 表示空参数列表。
    • [4] 对于多个参数,将参数列表放在括号 () 中。

到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式简化相应语法的另一种方式。

[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return

Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。

递归

递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量

// functional/RecursiveFibonacci.java
public class RecursiveFibonacci {
IntCall fib; // 不能直接初始化递归函数,必须在构造器中初始化,这是实例变量
RecursiveFibonacci() {
fib = n -> n == 0 ? 0 :
n == 1 ? 1 :
fib.call(n - 1) + fib.call(n - 2);
}
int fibonacci(int n) { return fib.call(n); }
public static void main(String[] args) {
RecursiveFibonacci rf = new RecursiveFibonacci();
for(int i = 0; i <= 10; i++)
System.out.println(rf.fibonacci(i));
}
}
private  interface IntCall{
int call(int n);
}
static IntCall fib = new IntCall() { // 这是静态变量,不可以直接 static IntCall fib = n -> n == 0 ? 1 : n == 1 ? 1 : fib.call(n - 1) + fib.call(n - 2),直接调用自己会出现非法自引用,原因是编译器认为在调用自己的时候可能函数还未被初始化
@Override
public int call(int n) {
return 0;
}

public int fibonacci(int n){
return n == 0 ? 0 : n == 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}
};

方法引用

方法引用组成:类名或对象名,后面跟 ::,然后跟方法名称。

Runnable接口

Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable

未绑定的方法引用

未绑定的方法引用是指没有关联对象的普通(非静态)方法

使用为绑定的方法引用的时候,需要在引用的方法参数中声明待引用类型

实例化待引用类型后,通过该实例调用方法

public class unboundMethodRef {
public static void main(String[] args) {
// sameTypeRef.show()没有指定关联对象
// sameTypeRef sf = normalClass::display; //无法从 static 上下文引用非 static 方法
differentTypeRef df = normalClass::display;
normalClass nc = new normalClass();

MultiArgRef maf = normalClass::displayMultiArgs;

System.out.println(df.invoke(nc));
System.out.println(maf.show(nc, "Hello", "World"));
}
}

class normalClass{
String display(){
return "Hello, World!!!";
}

String displayMultiArgs(String arg1, String arg2){
return arg1 + " , " + arg2;
}
}

interface sameTypeRef{
String show();
}

interface MultiArgRef{
String show(normalClass nc, String arg1, String arg2);
}

interface differentTypeRef{
String invoke(normalClass nc);
}

构造函数引用

你还可以捕获构造函数的引用,然后通过引用调用该构造函数

package love.aozakiaoko;

public class constructFP {
public static void main(String[] args) {
ZeroArg zeroArg = Dog::new;
OneArg oneArg = Dog::new;
TwoArg twoArg = Dog::new;

Dog show = zeroArg.show();
Dog show1 = oneArg.show("beibei");
Dog show2 = twoArg.show("mimi", 2);
}
}

class Dog{
String name;
int age;

Dog(){name = "default";}
Dog(String name){this.name = name;}
Dog(String name, int age){this.name = name; this.age = age;}
}

interface OneArg{
Dog show(String name);
}

interface TwoArg{
Dog show(String name, int age);
}

interface ZeroArg{
Dog show();
}

函数式接口

在使用函数式接口的时候,方法的名称无关紧要,只要参数类型和返回值类型相同。Java会自动把方法映射到接口方法,调用方法则是使用接口中的方法名

多参数函数式接口

使用@FunctionalInterface自己创建

以下是基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 FunctionConsumerPredicate 等。参数类型通过泛型添加。
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumerDoubleFunctionIntPredicate 等,但返回基本类型的 Supplier 接口例外。
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction <T>IntToLongFunction
  4. 如果返回值类型与参数类型相同,则是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  5. 如果接收参数并返回一个布尔值,则是一个 谓词 (Predicate)。
  6. 如果接收的两个参数类型不同,则名称中有一个 Bi

下表描述了 java.util.function 中的目标类型(包括例外情况):

特征 函数式方法名 示例
无参数; 无返回值 Runnable (java.lang) run() Runnable
无参数; 返回类型任意 Supplier get() getAs类型() Supplier<T> BooleanSupplier IntSupplier LongSupplier DoubleSupplier
无参数; 返回类型任意 Callable (java.util.concurrent) call() Callable<V>
1 参数; 无返回值 Consumer accept() Consumer<T> IntConsumer LongConsumer DoubleConsumer
2 参数 Consumer BiConsumer accept() BiConsumer<T,U>
2 参数 Consumer; 第一个参数是 引用; 第二个参数是 基本类型 Obj类型Consumer accept() ObjIntConsumer<T> ObjLongConsumer<T> ObjDoubleConsumer<T>
1 参数; 返回类型不同 Function apply() To类型类型To类型 applyAs类型() Function<T,R> IntFunction<R> LongFunction<R> DoubleFunction<R> ToIntFunction<T> ToLongFunction<T> ToDoubleFunction<T> IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
1 参数; 返回类型相同 UnaryOperator apply() UnaryOperator<T> IntUnaryOperator LongUnaryOperator DoubleUnaryOperator
2 参数,类型相同; 返回类型相同 BinaryOperator apply() BinaryOperator<T> IntBinaryOperator LongBinaryOperator DoubleBinaryOperator
2 参数,类型相同; 返回整型 Comparator (java.util) compare() Comparator<T>
2 参数; 返回布尔型 Predicate test() Predicate<T> BiPredicate<T,U> IntPredicate LongPredicate DoublePredicate
参数基本类型; 返回基本类型 类型To类型Function applyAs类型() IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
2 参数; 类型不同 Bi操作 (不同方法名) BiFunction<T,U,R> BiConsumer<T,U> BiPredicate<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> ToDoubleBiFunction<T>

高阶函数

高阶函数是一个消费或者产生函数的函数,也就是返回值为一个新的函数,或者调用参数里面的函数

package love.aozakiaoko;

import java.util.function.Function;

public class FunctionTest {

// Function<T, R> 表示输入类型为T,输出类型为R的一个函数

// transform是一个函数引用,该函数引用的输入为I输出为O,该函数接收一个输入为I输出为O的函数应用作为参数
//
static Function<I, O> transform(Function<I, O> in){
// Function接口的addThen方法,参数输入一个函数,先调用Function本身的函数
// 在用调用的结果作为参数去调用,addThen输入的函数
// 这里的意思是打印in函数输出的结果,并返回它
return in.andThen(o -> { //
System.out.println(o);
return o;
});
}

// compose会先调用传入的函数,所以传入函数的输入必须是I
static Function<I, O> testCompose(Function<I, O> in){
return in.compose(o -> {
System.out.println(o);
return o;
});
}

public static void main(String[] args) {
// 传入的函数是打印传入的对象,并返回一个O类型(因为该函数引用的泛型规定了,返回对象必须是O类型)
Function<I, O> f2 = transform(i -> {
System.out.println(i);
return new O();
});

Function<I, O> f1 = testCompose(o -> {
System.out.println(o);
return new O();
});

// 调用f2这个函数,输入I类型,打印I类型,返回一个O类型,打印O类型,返回O类型
O o = f2.apply(new I());

// 在调用输入的函数之前,先调用原本函数引用的函数,打印输入的参数I类型,返回一个I类型,在调用参数函数引用,打印返回的I类型,返回一个O类型
O o1 = f1.apply(new I());
}
}

class I {
@Override
public String toString() { return "I"; }
}
class O {
@Override
public String toString() { return "O"; }
}

闭包

public class Closure {

int i = 1;
IntSupplier makeFun(int x){
return () -> x + i++;
}

// Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的
// Lambda表达式的中的局部变量并不会消失,而是被关在了函数中,
IntSupplier makeFun2(int x){
int i = 1;
return () -> x + i;
}

public static void main(String[] args) {
Closure closure = new Closure();
IntSupplier makeFun1 = closure.makeFun(1);
IntSupplier makeFun2 = closure.makeFun2(2);
System.out.println(makeFun1.getAsInt());
System.out.println(makeFun1.getAsInt());
System.out.println();
System.out.println(makeFun2.getAsInt());
System.out.println(makeFun2.getAsInt());
}
}

Lambda 可以没有限制地引用 实例变量和静态变量。但 局部变量必须显式声明为final,或事实上是final

函数组合

public class FunctionCombination {
static Function<String, String>
f1 = s -> {
System.out.println(s);
return s.replace('A', '_');
},
f2 = s -> s.substring(3),
f3 = s -> s.toLowerCase(),
f4 = f1.compose(f2).andThen(f3); // 实际上是 先调用f2,在调用f1,在调用f3,返回一个结果
public static void main(String[] args) {
System.out.println(
f4.apply("GO AFTER ALL AMBULANCES"));
}
}

组合方法 支持接口
andThen(argument) 执行原操作,再执行参数操作 Function BiFunction Consumer BiConsumer IntConsumer LongConsumer DoubleConsumer UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator
compose(argument) 执行参数操作,再执行原操作 Function UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator
and(argument) 原谓词(Predicate)和参数谓词的短路逻辑与 Predicate BiPredicate IntPredicate LongPredicate DoublePredicate
or(argument) 原谓词和参数谓词的短路逻辑或 Predicate BiPredicate IntPredicate LongPredicate DoublePredicate
negate() 该谓词的逻辑非 Predicate BiPredicate IntPredicate LongPredicate DoublePredicate

柯里化和部分求值

import java.util.function.*;
public class Curry3Args {
public static void main(String[] args) {
Function<String, // a 这是入口
Function<String, // b
Function<String, String>>> sum = // 这里第一个String 是c
a -> b -> c -> a + b + c; // Lambda表达式于定义一一对应 sum = a + b + c
Function<String,
Function<String, String>> hi =
sum.apply("Hi ");
Function<String, String> ho =
hi.apply("Ho ");
System.out.println(ho.apply("Hup"));
}
}

流式编程


public class Randoms {
public static void main(String[] args) {
// 这种流式编程隐藏了迭代的过程,因此被称为内部迭代(internal iteration)
// 流是懒加载的。它只在绝对必要时才计算,可以将流看作“延迟列表”。由于计算延迟,流能够表示非常大(甚至无限)的序列,而不需要考虑内存问题
new Random(111)
// 产生流
.ints(5, 20)
// 去重
.distinct()
// 取前7个
.limit(7)
// 排序
.sorted()
// 根据传递给他的函数对流中的每个对象执行操作
.forEach(System.out::println);
}
}

流支持 / 流创建

package love.aozakiaoko.stream;

// streams/StreamOf.java
import java.util.*;
import java.util.stream.*;
public class StreamOf {
public static void main(String[] args) {
// 流支持:在接口中添加被 default(默认)修饰的方法。
// 流创建
Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
.forEach(System.out::println);
Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
.forEach(System.out::print);
System.out.println();
Stream.of(3.14159, 2.718, 1.618)
.forEach(System.out::println);


// 集合可以通过调用stream()方法来产生一个流
List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
System.out.println(bubbles.stream()
// 将对象流转化为intStream
.mapToInt(b -> b.i)
.sum());
Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
w.stream()
// map 获取流中所有元素,并对元素加以操作
.map(x -> x + " ")
.forEach(System.out::print);
System.out.println();
Map<String, Double> m = new HashMap<>();
m.put("pi", 3.14159);
m.put("e", 2.718);
m.put("phi", 1.618);
// 先使用entrySet转化为一个对象流
m.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.forEach(System.out::println);
}
}

class Bubble{
int i;
Bubble(int n){
i = n;
}

@Override
public String toString() {
return "Bubble(" + i + ')';
}
}

随机数流

public class RandomGenerators {
public static <T> void show(Stream<T> stream) {
stream
.limit(4)
.forEach(System.out::println);
System.out.println("++++++++");
}
public static void main(String[] args) {
Random rand = new Random(47);
// boxed把基础类型流转化为保证类型流
show(rand.ints().boxed());
show(rand.longs().boxed());
show(rand.doubles().boxed());
// 控制上限和下限:
show(rand.ints(10, 20).boxed());
show(rand.longs(50, 100).boxed());
show(rand.doubles(20, 30).boxed());
// 控制流大小:
show(rand.ints(2).boxed());
show(rand.longs(2).boxed());
show(rand.doubles(2).boxed());
// 控制流的大小和界限
show(rand.ints(3, 3, 9).boxed());
show(rand.longs(3, 12, 22).boxed());
show(rand.doubles(3, 11.5, 12.3).boxed());
}
}

基本类型流的创建

package love.aozakiaoko.stream;

import javax.xml.transform.Source;
import java.util.Random;
import java.util.Set;
import java.util.function.IntSupplier;
import java.util.stream.IntStream;

public class intStreams {
static int x = 1;
static int y = 1;

public static void main(String[] args) {
// 转换为 流
IntStream is1 = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
IntStream is2 = IntStream.of(10);
// 把两个流连接成一个流
IntStream concat = IntStream.concat(IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9), IntStream.of(10));
concat.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
is1.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
is2.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
// 得到一个空的流
IntStream emptyStream = IntStream.empty();
emptyStream.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
// 根据intSupplier函数接口来创建流
Random random = new Random(47);
IntStream randStream = IntStream.generate(random::nextInt);
randStream.limit(10).mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();

// 用建造者模式创建流
IntStream.Builder builder = IntStream.builder();
IntStream buildStream = builder.add(1).add(2).add(3).add(4).build();
buildStream.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
// 迭代的创建流,流中的前一个元素作为函数接口的参数,得到的结果作为新的流元素,并迭代
IntStream fibStream = IntStream.iterate(0, i -> {
int result = x + i;
x = i;
return result;
});
fibStream.skip(20).limit(10).mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
// 左闭右开的流数组
IntStream rangedStream = IntStream.range(1, 20);
rangedStream.mapToObj(i -> i + " ").forEach(System.out::print);
System.out.println();
// 左边右闭的流数组
IntStream rangeClosed = IntStream.rangeClosed(1, 20);
rangeClosed.mapToObj(i -> i + " ").forEach(System.out::print);

}

}

常用的中间操作

public class intStreamOperations {
public static void main(String[] args) {

IntStream intStream = IntStream.rangeClosed(1, 30);

intStream
// 跳过前两个
.skip(2)
// 只要20个元素
.limit(20)
// 每个元素平方
.map(i -> i * i)
// 排序
.sorted()
// 去重
.distinct()
// 化为Double流
.asDoubleStream()
// 化为String流
.mapToObj(i -> i + " ")
// 对象流才有的排序,这里是反向根据字典反向排序
.sorted(Collections.reverseOrder())
.forEach(System.out::print);

System.out.println();

List<String> list = Arrays.asList("My", "name", "is", "Aozaki", "Aoko");
// 把集合转换为流,对流中的每个元素执行collect传入的函数操作
// 这里的函数是将流中的每个元素直接用” “连接起来
System.out.println(list.stream().collect(Collectors.joining(" ")));
}
}

更多的Collectors提供的函数接口,可以查询https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Collectors.html

使用流实现简单工厂


public class CombinationStream {

public static void main(String[] args) {
Stream.of(1, 2, 3)
// flatMap 相当于把参数流中的元素取出来,用处理后的结果替换调用者的流
.flatMap(i -> Stream.of("Misaki", "Aoko", "Arecuid"))
.forEach(System.out::println);

Random rand = new Random(47);
Stream.of(1, 2, 3, 4, 5)
// 把参数流中的元素取出来,扁平化为int类型,替换原本的流
.flatMapToInt(i -> IntStream.concat(
rand.ints(0, 100)
.limit(i), IntStream.of(-1)
))
.forEach(i -> System.out.format("%d ", i));


System.out.println();

// 对字符串流元素应用正则表达式分割
String text = "Journey to the west maybe the most influential novel in four most famous ancient Chinese literal novel and certainly the most popular novel in foreign. This novel describe a difficulty journey that a famous monk XuanZhang with three followers cross the western land of China for the Buddhist scripture. Though the story topic is based on Buddhism, the novel use a amount of Chinese folk and mythology material to create many vivid characters and animals. The most famous character SunWukong whose story about fight for all kinds of monster is known to every Chinese children";
Stream.of(text).flatMap(s -> Arrays.stream(s.split("\\W+")))
.limit(7)
.map(String::trim)
.forEach(s -> System.out.format("%s\n", s));

System.out.println();
// 另一种表达方式
Stream.of(text).flatMap(line -> Pattern.compile("\\W+").splitAsStream(line))
.skip(7)
.limit(7)
.map(String::trim)
.forEach(s -> System.out.format("%s\n", s));
}

Optional类

标准的流操作会返回Optional对象

public class OptionalTest {
public static void main(String[] args) {
Stream<Object> empty = Stream.empty();
Optional<Object> any = empty.findAny();
System.out.println(any); // Optional.empty
// 流操作返回Optional对象,避免了在操作的过程中出现异常

Stream<Integer> integerStream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> integerStream2 = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> oA = integerStream1.findAny();
Optional<Integer> of = integerStream2.findFirst();
// 在使用Optional对象之前,要先调用isPresent(),防止结果为空
if(oA.isPresent()){
System.out.println(oA.get()); // 1
}else {
System.out.println("empty Stream");
}

if(of.isPresent()){
System.out.println(of.get()); // 1
}else {
System.out.println("empty Stream");
}
}
}

便利函数

有许多便利函数可以解包 Optional ,这简化了上述“对所包含的对象的检查和执行操作”的过程:

  • ifPresent(Consumer):当值存在时调用 Consumer,否则什么也不做。
  • orElse(otherObject):如果值存在则直接返回,否则生成 otherObject
  • orElseGet(Supplier):如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。
  • orElseThrow(Supplier):如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
// 便利函数
oA.ifPresent(System.out::println);
System.out.println(oA.orElse(-1));
System.out.println(oA.orElseGet(() -> -1));
System.out.println(oA.orElseThrow(RuntimeException::new));

Optional操作

  • filter(Predicate):对 Optional 中的内容应用Predicate 并将结果返回。如果 Optional 不满足 Predicate ,将 Optional 转化为空 Optional 。如果 Optional 已经为空,则直接返回空Optional
  • map(Function):如果 Optional 不为空,应用 FunctionOptional 中的内容,并返回结果。否则直接返回 Optional.empty
  • flatMap(Function):同 map(),应用于已生成 Optional 的映射函数。
// 创建Optional
test("empty", Optional.empty());
// of 把一个值包装进Optional中
test("of", Optional.of("str"));
try {
test("of", Optional.of(null));
} catch(Exception e) {
System.out.println(e);
}
// 如果为空,则返回一个Optional.empty
test("ofNullable", Optional.ofNullable("Hi"));
test("ofNullable", Optional.ofNullable(null));


// Optional 对象操作
/*
* 很简单,略
* */

Optional流

如果我们操作的生成流的函数可能会生成null值,可以用Optional来保证元素,这样可以更好的处理null

终端操作

终端操作会获取流的最终结果,使用终端操作后,我们无法继续传递使用流

  • toArray():将流转换成适当类型的数组。
  • toArray(generator):在特殊情况下,生成自定义类型的数组。
  • forEach(Consumer)常见如 System.out::println 作为 Consumer 函数。
  • forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作。
  • collect(Collector):使用 Collector 收集流元素到结果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新的结果集合,第二个参数 BiConsumer 将下一个元素收集到结果集合中,第三个参数 BiConsumer 用于将两个结果集合合并起来。
  • reduce(BinaryOperator):使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。
  • reduce(identity, BiFunction, BinaryOperator):更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 map()reduce() 来更简单的表达它。
  • allMatch(Predicate) :如果流的每个元素提供给 Predicate 都返回 true ,结果返回为 true。在第一个 false 时,则停止执行计算。
  • anyMatch(Predicate):如果流的任意一个元素提供给 Predicate 返回 true ,结果返回为 true。在第一个 true 是停止执行计算。
  • noneMatch(Predicate):如果流的每个元素提供给 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
  • findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty
  • findAny(:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty
  • count():流中的元素个数。
  • max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。
  • min(Comparator):根据所传入的 Comparator 所决定的“最小”元素。
  • average() :求取流元素平均值。
  • max()min():数值流操作无需 Comparator
  • sum():对所有流元素进行求和。
  • summaryStatistics():生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。

异常

异常来说,最重要的部分就是类名

异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。

异常与记录日志

class LoggingException extends Exception {
// 创建了一个与错误相关的包名和类名相关联的 Logger 对象
// Logger 对象会将其输出发送到 System.err
private static Logger logger =
Logger.getLogger("LoggingException");
LoggingException() {
StringWriter trace = new StringWriter();
// printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数
printStackTrace(new PrintWriter(trace));
// 向 Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()
logger.severe(trace.toString());
}
}
public class LoggingExceptions {
public static void main(String[] args) {
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
}
}

Exception方法

// exceptions/ExceptionMethods.java
// Demonstrating the Exception Methods
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch(Exception e) {
System.out.println("Caught Exception");
System.out.println(
"getMessage():" + e.getMessage());
// 用来获取详细信息,或用本地语言表示的详细信息。
System.out.println("getLocalizedMessage():" +
e.getLocalizedMessage());
// 返回对 Throwable 的简单描述,要是有详细信息的话,也会把它包含在内
System.out.println("toString():" + e);
// 打印 Throwable 和 Throwable 的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}

代码校验

https://www.jishuchi.com/read/onjava8/12015

文件

public class PathInfo {
static void show(String id, Object p) {
System.out.println(id + ": " + p);
}
static void info(Path p) {
show("toString", p);
show("Exists", Files.exists(p));
show("RegularFile", Files.isRegularFile(p));
show("Directory", Files.isDirectory(p));
show("Absolute", p.isAbsolute());
show("FileName", p.getFileName());
show("Parent", p.getParent());
show("Root", p.getRoot());
System.out.println("******************");
}
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
info(Paths.get("C:", "path", "to", "nowhere", "NoFile.txt"));
Path p = Paths.get("PathInfo.java");
info(p);
Path ap = p.toAbsolutePath();
info(ap);
info(ap.getParent());
try {
info(p.toRealPath());
} catch(IOException e) {
System.out.println(e);
}
URI u = p.toUri();
System.out.println("URI: " + u);
Path puri = Paths.get(u);
System.out.println(Files.exists(puri));
File f = ap.toFile(); // Don't be fooled
}
}
Path path = Paths.get("Pathinfo.java");
Path absolutePath = path.toAbsolutePath();
System.out.println(absolutePath);
System.out.println(absolutePath.getRoot());
System.out.println(absolutePath.startsWith(absolutePath.getRoot()));
System.out.println(path.getFileName());
System.out.println(absolutePath.getFileName());
System.out.println(absolutePath.endsWith(path.getFileName()));
System.out.println();
// 遍历不已 Root开始
System.out.println(absolutePath.getRoot());
for (Path pp : absolutePath){
System.out.println(pp);
}

relativize()

// base.relativize(target) 输出base到target的相对路径
Path basePath = Paths.get("/usr/local/");
Path targetPath = Paths.get("/usr/local/bin/file.txt");

Path relativePath = basePath.relativize(targetPath);
System.out.println(relativePath);
// bin/file.txt

resolve()

// base.resolve(relative) base 是绝对路径, relative是相对路径,将相对路径解析到基础路径上,生成的新的路径
Path basePath = Paths.get("/usr/local/");
Path relativePath = Paths.get("bin/file.txt");

Path resolvedPath = basePath.resolve(relativePath);
System.out.println(resolvedPath);
// /usr/local/bin/file.txt

resolveSibling

// 创建一个新的路径,通过替换给定路径的最后一个元素(文件或目录)来生成新的路径
Path basePath = Paths.get("/usr/local/");
Path currentPath = Paths.get("/usr/local/file.txt");

Path newPath = currentPath.resolveSibling(basePath);
System.out.println(newPath);
// /usr/local/

文件系统


public class FileInfo {
static void show(String id, Object o) {
System.out.println(id + ": " + o);
}
public static void main(String[] args) {
FileSystem fsys = FileSystems.getDefault();
for (FileStore fs : fsys.getFileStores()){
System.out.println("File Store : " + fs);
}
for (Path p : fsys.getRootDirectories()) {
System.out.println("Root Directory : " + p);
}

show("Separator", fsys.getSeparator());
show("UserPrincipalLookupService",
fsys.getUserPrincipalLookupService());
show("isOpen", fsys.isOpen());
show("isReadOnly", fsys.isReadOnly());
show("FileSystemProvider", fsys.provider());
show("File Attribute Views",
fsys.supportedFileAttributeViews());
}
}

路径监听

通过 WatchService 可以设置一个进程对目录中的更改做出响应。在这个例子中,delTxtFiles() 作为一个单独的任务执行,该任务将遍历整个目录并删除以 .txt 结尾的所有文件,WatchService 会对文件删除操作做出反应:

// files/PathWatcher.java
// {ExcludeFromGradle}
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.util.concurrent.*;
public class PathWatcher {
static Path test = Paths.get("test");
static void delTxtFiles() {
try {
Files.walk(test)
.filter(f ->
f.toString()
.endsWith(".txt"))
.forEach(f -> {
try {
System.out.println("deleting " + f);
Files.delete(f);
} catch(IOException e) {
throw new RuntimeException(e);
}
});
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
Directories.refreshTestDir();
Directories.populateTestDir();
Files.createFile(test.resolve("Hello.txt"));
WatchService watcher = FileSystems.getDefault().newWatchService();
test.register(watcher, ENTRY_DELETE);
Executors.newSingleThreadScheduledExecutor()
.schedule(PathWatcher::delTxtFiles,
250, TimeUnit.MILLISECONDS);
WatchKey key = watcher.take();
for(WatchEvent evt : key.pollEvents()) {
System.out.println("evt.context(): " + evt.context() +
"\nevt.count(): " + evt.count() +
"\nevt.kind(): " + evt.kind());
System.exit(0);
}
}
}
/* Output:
deleting test\bag\foo\bar\baz\File.txt
deleting test\bar\baz\bag\foo\File.txt
deleting test\baz\bag\foo\bar\File.txt
deleting test\foo\bar\baz\bag\File.txt
deleting test\Hello.txt
evt.context(): Hello.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
*/

文件查找

通过在 FileSystem 对象上调用 getPathMatcher() 获得一个 PathMatcher,然后传入您感兴趣的模式。模式有两个选项:globregex

// files/Find.java
// {ExcludeFromGradle}
import java.nio.file.*;
public class Find {
public static void main(String[] args) throws Exception {
Path test = Paths.get("test");
Directories.refreshTestDir();
Directories.populateTestDir();
// Creating a *directory*, not a file:
Files.createDirectory(test.resolve("dir.tmp"));
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{tmp,txt}");
Files.walk(test)
.filter(matcher::matches)
.forEach(System.out::println);
System.out.println("***************");
PathMatcher matcher2 = FileSystems.getDefault()
.getPathMatcher("glob:*.tmp");
Files.walk(test)
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
System.out.println("***************");
Files.walk(test) // Only look for files
.filter(Files::isRegularFile)
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
}
}
/* Output:
test\bag\foo\bar\baz\5208762845883213974.tmp
test\bag\foo\bar\baz\File.txt
test\bar\baz\bag\foo\7918367201207778677.tmp
test\bar\baz\bag\foo\File.txt
test\baz\bag\foo\bar\8016595521026696632.tmp
test\baz\bag\foo\bar\File.txt
test\dir.tmp
test\foo\bar\baz\bag\5832319279813617280.tmp
test\foo\bar\baz\bag\File.txt
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
dir.tmp
5832319279813617280.tmp
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
5832319279813617280.tmp
*/

文件读写

// files/ListOfLines.java
import java.util.*;
import java.nio.file.*;
public class ListOfLines {
public static void main(String[] args) throws Exception {
Files.readAllLines(
Paths.get("../streams/Cheese.dat"))
.stream()
.filter(line -> !line.startsWith("//"))
.map(line ->
line.substring(0, line.length()/2))
.forEach(System.out::println);
}
}
/* Output:
Not much of a cheese
Finest in the
And what leads you
Well, it's
It's certainly uncon
*/

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/package-summary.html