基础内容

多态

void doSomething(Shape shape){
	shape.erase();
	//...
	shape.draw()
}

Triangle triangle = new Triangle;
Line line = new Line();
doSomething(triangle);
doSomething(line);

其中triangle和line都是shape的子类,doSomething发送给Shape对象的任何消息也可以发送给Triangle对象和Line对象。这种将子类视为基类的过程叫作“向上转型”(upcasting)。这里编译doSomething()时,调用的不是基类Shape的方法,而是具体子类的方法,即多态。

参数化类型(泛型)

一个被参数化的类型是一种特殊的类,可以让编译器自动适配特定的类型。比如.对于参数化的集合而言,编译器可以将集合定义为只接受放入Shape的对象,因此从集合也只能取出Shape对象。 例如创建一个放置Shape对象的ArrayList:

ArrayList<Shape> shapes = new ArrayList<>();

对象

new关键字在堆上创建对象,而基本类型是直接创建一个“自动变量”,该变量会在栈上保存值,效率更高。 java还为基本类型提供了对应的“包装类”(wrapper class),通过包装类可以将基本类型呈现为位于堆上的非原始对象。

static字段是基于类创建的,非static字段则是基于对象创建的。将static应用于方法时,即便没有创建对象,也可以调用该方法。

操作符

  • =和≠比较的是对象的引用,通过不同方式创建的Integer对象会产生不同的结果。因此使用Integer时应只使用equals()。

出于效率原因,Integer会通过享元模式来缓存范围在-128-127内的对象,因此多次调用Integer.valueOf(127)生成的其实是同一个対象。而在此范围之外的值则不会这样,比如每次调用Integer. valueOf(128)返回的都是不同的对象。因此需要特别注意,在逬行=和≠的比较时,范围不同的值生成对象的方式并不一样,这会影响到比较的行为。从而产生不同的结果。另外,通过new Interger()生成的对象都是新创的,无论其值处于什么范围所以通过不同方式创建的Integer对象,也会影响到比较的结果。

  • 当一个非常大的数减去一个相对较小的数时,非常大的数值并不会发生显著变化,这叫作舍入误差。这种误差之所以发生,是因为机器不能存储足够的信息来表示一个大数值的微小变化。

对于自定义类,使用equals()方法的默认行为是比较引用,所以需要重写该方法来比较内容。大多数标准库会重写equals()方法来比较对象的内容而不是它们的引用。

如果表达式以一个字符串开头,则其后的所有操作数都必须是字符串类型的(编译器会自动把双引号里的字符序列转换成字符串)。如"" + x 就等同于调用Integer.toString()。

  • 将float和double类型数据转换为int类型时总是截尾操作,若想对结果舍入,需要使用java.lang.Math中的round()方法。

标签

即具体的规则为: 普通的continue会跳到最内层循环的起始处,并继续执行。带标签的continue会跳到对应标签的位置,并重新进入这个标签后面的循环。普通的break会“跳出循环的底部”,也就是跳出当前循环。带标签的break会跳出标签所指的循环。

初始化

java初始化使用构造器,方法名与类名一致,无返回类型。同时注意new关键字确实返回了新建对象的引用。

方法重载:允许具有不同参数类型的方法有相同的名字,尤其是对于构造器是必须的。重载中存在类型提升,当传入数据的类型小于方法中参数的类型,传入数据的类型就会被提升;char类型如果没有找到精确匹配,它会被提升为int类型。如果传入数据的类型比方法参数的类型更宽,就必须使用窄化转型,否则编译器会报错。

区分重载的方法:可以通过参数类型列表区分,但不能通过返回值区分。

无参构造器:如果创建了一个没有构造器的类,编译器会自动为这个类添加无参构造器;但是如果类中已经定义了构造器,无论其是否有参数,编译器都不会再自动创建一个无参构造器。

this关键字只能在非静态方法中使用,表示方法内对当前对象的引用;如果从类的一个方法中调用该类的另一个方法,那就没必要使用this,直接调用即可。在构造器中使用在this后加参数列表,那么就有了不同的含义,它会显式调用与该参数列表匹配的构造器(在构造器中调用构造器)。

static含义:在没有创建对象的时候,可以直接通过类本身调用一个静态方法;设为static的方法没有this,不能从静态方法中调用非静态方法。

垃圾收集:在类中定义finalize()方法,当垃圾收集器准备释放对象占用的资源时,它首先调用finalize()方法,并旦只在下一次垃圾收集时才会回收这个对象的内存。因此如果使用finalize(),它能让你在垃圾收集时执行一些重要的清理工作。注意:垃圾收集仅与内存有关,即垃圾收集器存在的唯一原因就是回收程序里不再使用的内存。finalize()方法不适合普通清理工作。

JVM垃圾收集器:自适应的、分代的、停止-复制、标记-清除。JVM中还有附加技术以提升速度,如JIT(just in time)即时编译器,即时编译器会将程序部分或全部编译为本地机器码,这样就不需要JVM的解释,从而运行得更快。

成员初始化:对于方法内的未初始化的局部变量,会在编译时错误;而类中字段的基础类型未初始化,将会被自动初始化;当类中定义了一个对象引用而不将其初始化,这个引用就会被赋予一个特殊值null。

初始化顺序: 类中的变量定义顺序决定了初始化的顺序,即使分散到方法定义之间,变量定义仍然会在任何方法(包括构造器)调用之前就被初始化。

静态数据初始化:static关键字不能用于局部变量,仅适用于成员变量;无论创建多少对象,静态数据只有一份存储空间;初始化的顺序是从静态字段开始(如果它们还没有被先前的对象创建触发初始化的话),然后是非静态字段。

显式静态初始化: 只执行一次,第一次创建该类的对象时,或第一次访问该类的静态成员时(即使从未创建过该类的对象) 。

非静态实例初始化:对于支持匿名内部类的初始化是必需的,但也可以用来保证无论 用哪个显式的构造器,某些操作都会发生。实例初始化子句在构造器之前执行。

动态数组建立: 基础类型数组的创建:int[] a = new int[20],如果创建了一个非基本类型数组如:Integer[] a = new Integer[20],这实际上是创建了引用数组,直到通过自动装箱为数组里的每个引用本身初始化了一个Integer对象之后,这个数组的初始化才真正结束:a[i] = rand.nextInt(100),还有另外两种形式用花括号包围列表来初始化对象数组:

可变参数列表:创建Object数组,便可以来创建和调用有可变参数的方法这包括数量可变的参数以及未知类型的参数。new Object[]{10, "just"}

局部变量类型推断: 用var关键字在局部定义中(方法内部),编译器可以自动发现类型,即类型推断。

库组件和访问权限修饰符

一个java源文件就是一个编译单元,每个编译单元内只能有一个public类。若该编译单元内还有还有其他类,则在该包之外是不可见的,因为它们非public,而是主public的支持类(support class)。

代码组织:当编译一个java文件时,文件中的每个类都会有一个输出文件,扩展名为.class。对于编译型语言,编译器输出一个中间形式(通常为obj文件),然后使用链接器(linker)或库生成器(librarian)将它与其他同类文件一起打包以创建一个可执行文件。然而在java中一个可运行程序就是一堆.class文件,可以使用jar归档器将它们打包并压缩成一个Java档案文件(JAR )。Java解释器负责查找、加载和解释这些文件。 使用package语句可以将组件属于同一个命名空间,使用package和import关键字将单个全局命名空间分隔开,这样名称就不会发生冲突了。若导入的两个库中包含了相同名称的类时,需要在创建对象时指定,如java.util.Vector vector = new java.util.Vector()

Java访问权限修饰符包括public, protected和private,它们放在类中成员定义的前面,包括字段和方法。每个访问权限修饰符仅控制对该特定定义的访问。

包访问权限:也称为默认访问权限,无关键字,当前包中所有所有其他类都可以访问该成员,对于该包之外的类显示为private。

类控制着哪些代码可以访问其成员,授予成员访问权限的方法

  1. 将成员设为public,这样任何地方的任何人都可以访问它。

  2. 通过去掉访问权限修饰符予成员包访问权限,并将其他类放在同一个包中,这样该包中的其他类就可以访问该成员了。

  3. 通过继承而来的类(子类)可以访问父类的protected成员以及public成员(但不能访问private成员)。只有当两个类在同一个包中时,它才可以访问父类的包访问权限成员。

  4. 提供可以读取和更改值的访问器(accessor)和修改器(mutator)方法(也称为“get/set”方法)。

默认包:当两个java文件在同一目录下,并且没有明确包名时,其中一个文件可以访问另一个文件中无关键字成员。Java将这样的文件看作属于该目录的“默认包”的隐含部分,因此它们为该目录中所有其他文件提供了包访问权限。

private关键字:除了包含该成员的类之外都无法访问,同一个包中的其他类无法访问private成员。在涉及多线程的情况下使用private十分重要。

另一个效果:上述例子中无参构造器是唯一定义的构造器,并且它是private的,因此这就阻止了该类的继承。

只要能确定是类的“辅助”方法,这个方法就可以设为private,以确保在包的其他地方不会意外地使用它,从而让自己无法再更改或删除。然而仅仅因为一个对象的引用在类中是private的,并不意味着其他对象不能拥有对同一个对象的public引用

protected关键字:如果创建一个新包,并继承另一个包中的类,那么唯一可以访问的成员是这个类的public成员;有时基类创建者想要把特定成员的访问权限赋给子类,而不是所有的类,就可以使用protected。

公共构造器:在只有包访问权限(即无关键字)的类中声明一个public构造器,并不会使该构造器成为public。

访问控制又被称为实现隐藏。将数据和方法包装在类中,并与实现隐藏相结合,称为封装。其结果就是具有特征和行为的数据类型。

设置访问权限控制的原因

  1. 确定客户程序员可以使用和不可使用的内容。

  2. 将接口和实现分离。这样客户程序员就只能将消息发送到public接口,而自己可以更改非public代码而不破坏客户端。

类的访问权限:只有包访问权限和public。如果想要阻止对某类的访问,可以将其所有构造器都设为private,禁止他人创建该类的对象,而自己可以在类中的静态方法中创建对象:

第一种方式还可以在返回之前对Soup1执行额外的操作,或计算Soup1对象生成的数量;第二种使用了所谓的设计模式,这里的特定模式称为单例模式,因为它从始至终都只有一个对象被创建。

复用

编译器并不是简单地为每个引用创建一个默认对象。初始化引用有4种形式:

  1. 定义对象时,这意味着它们将始终在调用构造器之前被初始化。

  2. 类的构造器中。

  3. 在对象实际使用之前,称为延迟初始化。

  4. 使用实例初始化。

初始化基类:当创建子类对象时,它里面包含了一个基类的子对象,这个子对象与直接通过基类创建的对象是一样的。正确初始化基类很重要,通过在子类构造器中调用基类构造器执行初始化。

@Override注解:添加这个注解表示重写一个方法。而当对该方法进行了重载而不是重写就会出现编译错误。

final关键字:表示“无法更改的”。一个既是static又是final的字段只会分配一块不能改变的存储空间。对于基本类型,final使其值恒定不变,但对于对象引用,final使引用恒定不变。一旦引用被初始化为一个对象,它就永远不能被更改为指向另一个对象了,但是对象本身是可以修改的。空白final是没有初始值的final字段,编译器会确保在使用前初始化这个空白final字段,要么在字段定义处使用表达式逬行赋值,要么在每个构造器中。final参数:通过在参数列表中声明创建,这意味着在方法内部不能更改参数引用指向的内容。final方法:防止继承类通过重写来改变该方法的含义,确保在继承期间方法的行为保持不变。类中任何private方法都是隐式的final(因为无法访问该方法且不能重写它),将final修饰符添加到private方法中,不会赋予该方法额外意义。final类:阻止该类的所有继承。该类的所有方法都是隐式final的(因为无法重写),所以再在该类方法中添加final没有意义。

多态

多态提供了另一个维度的接口和实现分离,将做什么和怎么做解耦。封装通过组合特征和行为来创建新的数据类型,隐藏实现通过把实现细节设为private来分离接口与实现,但是多态是根据类型(type)来解耦的。多态方法调用允许一种类型表现出与另一种相似类型之间的区别,只要它们都继承自相同的基类型即可。通过基类来调用不同子类的相同方法,从而实现行为上的区别,这样就表现出了多态方法调用的差异性。多态也称为动态绑定、后期绑定或运行时绑定

只有非private方法可以被重写,重写private方法实际会产生新的方法,因为private方法在子类中是不可见的,如果使用了@Override注解,编译器会提示错误信息。

当子对象向上转型为基类引用时,任何字段都会被编译器解析,因此不是多态的,只有普通的方法调用可以是多态的,静态方法也不会被多态,因为静态方法与类相关联,而不是与单个对象相关联。

构造器和多态:基类的构造器总是在子类的构造过程中被调用,如果没有在子类构造器代码中显式调用基类构造器,它将隐式调用基类的无参构造器。一个复杂对象的构造器调用顺序:

  1. 基类的构造器被调用,递归地重复此步骤,一直到构造层次结构的根。根类先被构造,然后是下一个子类,以此类推,直到最底层的子类。

  2. 然后按声明的顺序来初始化成员。

  3. 最后执行子类构造器的方法体。

继承与清理:若为一个类创建了清理方法(例dispose()),那么在继承时如果有任何特殊清理必须作为垃圾收集的一部分,就应该在子类中重写dispose()方法执行该操作,而且还需要调用基类的dispose()方法,否则基类的清理不会发生。清理时顺序应与初始化顺序相反,以防止子对象依赖于其他对象。

构造器与多态的初始化完整步骤:假设基类构造器中调用了类中的draw()方法,子类重写了draw()方法,而重写的该方法中要打印类中的字段radius,假设子类中一开始想要初始化private int radius = 1,这时若创建一个子类对象,首先执行基类构造器,由于子类中重写了draw()方法,这时实际调用的是子类的draw()方法,但是执行该方法时打印的radius却为0,radius并没有被初始化成功。完整的初始化过程:

  1. 在发生任何其他事情之前,为对象分配的存储空间会先被初始化为二进制零

  2. 如前面所述的那样调用基类的构造器 此时被重写的draw()方法会被调用(是的,这发生在RoundGlyph构造器被调用之前),由于第1步的缘故,此时会发现radius值为零

  3. 按声明的顺序来初始化成员

  4. 子类构造器的主体代码被执行

因此,编写构造器时有一个很好的准则:“用尽可能少的操作使对象逬入正常状态,如果可以避免的话,请不要调用此类中的任何其他方法”。只有基类中的final方法可以在构造器中安全调用(这也适用于private方法,它们默认就是final的)。

协变返回类型:子类重写方法的返回值可以是基类方法返回值的子类型,假设Wheat是Grain的子类,如:

状态设计模式:通过组合动态地改变对象的行为,假设创建一个基类actor引用,先初始化为HappyActor对象,不同的子类有不同的行为,然后可以在运行时重新绑定到不同的对象,如将actor引用替换为SadActor对象,这样产生的行为也会发生变化。因此就在运行时获得动态灵活性(状态模式)。相反,不能在运行时决定以不同方式来继承,这必须在编译时就确定下来。通用的准则:用继承来表达行为上的差异,使用字段来表达状态上的变化。在该例子中,通过继承得到了两个不同的类来表达act()方法的差异,而使用组合来允许其状态发生改变,在这里,状态的改变恰好导致了行为的变化。

替换和扩展:若子类只是重写了基类的方法而无新加的方法,即接口没有改变,这就是"is-a"关系,子类和基类是纯替换关系。当子类新加了方法后,就变成"is-like-a"关系,此时若发生向上转型,就无法调用这些新方法。

向下转型:向上转型会丢失特定类型的信息,所以自然就可以通过向下转型来重新获取类型信息。但是向下转型是不安全的,在Java中,每个转型都会被检查,因此,即使看起来只是在执行一个普通的带括号的强制转型,但在运行时会检查此强制转型,以确保它实际上是你期望的类型,如果不是,则会抛出一个ClassCastException错误。这种在运行时检查类型的行为是Java反射的一部分。

接口

抽象类:基类创建了一个通用接口,子类可以用不同的方式来表示这个接口。它建立了一种基本形式,这样你就可以抽象出所有子类的共同之处。该基类称为抽象基类(抽象类)。创建抽象类的目的是想通过一个公共接口来操作一组类,因此该基类仅表示接口,而不是特定实现,创建该对象没有意义。因此无法创建抽象类的对象。

抽象方法:只有一个声明,无方法体,如abstract void f();。若类中包含一个或多个抽象方法,则该类必须定义为抽象类。一个抽象类可以没有抽象方法,这时主要目的是阻止对它的任何实例化。

  • 如果一个新类型继承了抽象类,并希望能生成自己的对象,那它必须为基类中的所有抽象方法提供方法定义。如果不这样做,那么子类也是抽象的,编译器将强制你使用abstract关键字来限定这个子类。

  • 接口中的方法默认(无修饰符)是public,不是包访问。不允许使用private abstract修饰符,因为这样子类将无法对该方法重写定义。

接口和抽象类的区别:主要在两者的惯用方式,接口常暗示“类的类型”,或作为形容词来使用,例如Runnable或Serializable,而抽象类通常是类层次结构的一部分,并且是“物的类型”,如String或Instrument。

特性
接口
抽象类

组合

可以在新类中组合多个接口

只能继承一个抽象类

状态

不能包含字段(静态字段除外,但它们不支持对象状态),接口主要用于定义行为而不是存储状态

可以包含字段,非抽象方法可以引用这些字段

默认方法与抽象方法

默认方法不需要在子类型里实现,它只能引用接口里的方法(字段不行)

抽象方法必须在子类型里实现

构造器

不能有构造器

可以有构造器

访问权限控制

隐式的 public

可以为 protected 或包访问权限

接口interface:创建接口使用interface关键字,还可以在这前面添加public关键字,默认为包访问。接口可以包含字段,但这些字段是隐式的static和final。接口中声明的方法默认是public。使用implement关键字创建接口的类,表示“接口只是定义了它看起来是怎样的,但现在我要声明它是如何工作的”。即使不显式声明来自接口的方法,它们也是public的。

默认方法:default关键字允许接口中的方法创建方法体,可以让实现接口的类不定义该方法时也可以使用。它允许向现有接口中添加方法,而不会破坏已经在使用该接口的所有代码。也称为防御方法或虚拟扩展方法。在JDK 9中,接口中的default方法和static方法都可以是private的。

多重继承:Java中只能继承一个类,但能实现任意数量的接口。通过将接口和默认方法结合可以实现结合多个基类型的行为。

接口中的静态方法:允许在接口里包含逻辑上属于它的实用程序。

组合接口:创建新接口时,可以用extends关联多个父接口。

  • 接口的一个常见用途就是策略设计模式,编写一个执行某些操作的方法,该方法接收指定的接口作为参数。即表示“可以用这个方法操作任何遵循此接口的对象”。

嵌套接口:接口中的嵌套接口的默认访问权限是public,因此不用加public修饰符。

  • 在类中嵌套的接口可以使用修饰符private,但是接口中的接口不能使用private。private修饰的接口只能在定义该接口的类中使用。

  • 当一个类实现一个接口时,它只需要提供对该接口中声明的方法的实现。嵌套在该接口中的其他接口不需要在该类中单独实现。私有接口只能在该类的内部使用,无法在外部类或其他类中实现。

工厂方法设计模式:不直接调用构造器,而是在工厂对象上调用创建方法,它可以产生接口实现。

添加这样的额外的间接层的常见原因是构建框架。

接口中使用private方法就不需要default关键字了,因为使用private后自动就是default了。(但是还是有区别的,默认方法是为了让接口可以有默认实现,而私有方法是为了在接口内部实现代码复用

jdk 17引入密封类和密封接口,基类或接口可以限制自己派生的类。

sealed类的子类只能通过下面的某个修饰符来定义:

  • final: 不允许有进一步的子类

  • sealed: 允许有一组密封子类

  • non-sealed: 一个新关键字,允许未知的子类来继承它

一个sealed类必须至少有一个子类,一个sealed的基类无法阻止non-sealed的子类的使用,因此可以随时放开限制,但是sealed类Super的直接子类还是严格限制的。

JDK 16的record也可以用作接口的密封实现,因为record是隐式的final,不需要再加final关键字。

内部类

在内部类生成外部类的引用,用外部类+.this,让对象创建它的某个内部类的对象,用对象+.new,例如:

当内部类为private时,只有它的外部类能访问;当内部类为protected时,只能由它的外部类、相同包中的类以及外部类的子类访问。匿名内部类:不能有具体名的构造器

嵌套类:设置为static的内部类。

嵌套类意味着:

  1. 不需要一个外部类对象来创建嵌套类对象

  2. 无法从嵌套类对象内部访问非static的外部类对象 普通内部类中不能有static数据和static字段,也不能包含嵌套类。

引入内部类的原因:通常情况下,内部类继承自某个类或实现某个接口,内部类中的代码会操作用以创建该内部类对象的外部类对象,内部类提供了逬入其外部类的某种窗口。

每个内部类都可以独立地继承自一个实现。因此,外部类是否已经继承了某个实现,对内部类并没有限制。从某种角度上讲,内部类完善了多重继承问题的解决方案,接口解决了一部分问题,但内部类实际上支持了“多重实现继承”。

使用内部类,可以获得如下功能:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,独立于外围类对象的信息。

  2. 一个外围类中可以有多个内部类,它们可以以不同方式实现同一个接口,或者继承同一个类。

  3. 内部类对象的创建时机不与外围类对象的创建捆绑到一起。

  4. 内部类不存在可能引起混淆的“is-a”关系,它是独立的实体。

闭包:是一个可调用的对象,它保留了来自它被创建时所在的作用域的信息。生成闭包有两种方式:内部类和lambda表达式。

继承内部类:类只继承了内部类,则不能使用默认构造器,构造方法需要传递一个指向其包围类对象的引用,且加上enclosingClassReference.supper()语法。

局部内部类:在方法体内的内部类。局部内部类不能使用访问权限修饰符,但它可以访问当前代码块中的常量以及外围类中的所有成员。局部内部类允许定义具名的构造器以及重载版本,而匿名类只能使用实例初始化。局部内部类允许创建该类的多个对象,而匿名内部类通常用于返回该类的一个实例。

集合

借助泛型定义一个用来持有Apple对象的ArrayList:ArrayList<Apple> apples = new ArrayList<>();尖括号包围着的是类型参数(可以有多个),指定了这个集合实例中可以保存的类型。

JDK 10/11引入局部类型推断,因此上述定义也可以写为:var apples = new ArrayList<Apple>()

Java集合类库从设计上可以分为两个不同概念,表示为库的两个基本接口。

  • Collection:一个由单独元素组成的序列,而旦这些元素要符合一条或多条规则。List必须按照元素插入顺序来保存它们;Set中不能存在重复元素;而Queue则要按照排队规则来输出元素(通常与元素被插入的顺序相同)

  • Map:一组键值对象对,使用键查找值。ArrayList使用一个数值来查找某个对象,而Map使用另一个对象查找某个对象,也被称为关联数组,或被称为字典。

添加一组元素:Arrays.asList()方法可以接收一个Object数组,或者一个用逗号分隔的元素列表,并将其转换为List对象。Collection.addAll()方法接受一个Collection对象,将其中所有元素添加到这个Collection中。

List

List以特定的顺序维护元素。List接口在Collection的基础上增加了一些方法,支持在List中间插入和移除元素。有两种类型List:

  • 基本的ArrayList,擅长随机访问元素,但是在List的中间插入或删除元素比较慢。

  • LinkedList,提供了理想的顺序访问性能,在List的中间插入和删除元素的成本都比较低。LinkedList随机访问性能相对较差,但是与ArrayList相比提供了更多功能。

与数组不同的是,List可以在创建之后添加或移除元素,而且可以自己调整大小。用contains()方法来确定某个对象是否在该列表中;要移除一个对象,可以将该对象的引用传给remove()方法;利用指向某个对象的引用,也可以使用index0f()方法获得该对象在List中的索引编号。

当需要确定某个元素是否在List中,查找某个元素的索引编号,以及按照引用从List中删除某个元素时,都会用到equals()方法(根类Object的组成部分)。对于其他的类,equals()的定义可能有所不同,比如String类,如果两个String的内容相同,则它们是等价的。

Iterator(迭代器)

迭代器是一个对象,它可以在序列中移动,并用来选择该序列中的每个对象。Iterator能够将序列的遍历操作与序列的底层结构分离,即迭代器统一了对集合的访问。Java的Iterator只能向一个方向移动。迭代器可以做的事情:

  • 使用iterator()方法让Collection返回一个Iterator。这个迭代器将准备好返回序列中的第一个元素。

  • 使用next()方法获得序列的下一个对象。

  • 使用hasNext()方法检查序列中是否还有更多对象。

  • 使用remove()方法删除该迭代器最近返回的元素。

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

ListIterator是Iterator的子类型,只有List类可以生成。Listiterator可以双向移动,它还可以生成相对于迭代器在列表中指向的当前位置的下一个和上一个元素的索引,并且可以使用set()方法替换它所访问过的最后一个元素。还可以通过调用listlteiator(n)来生成一个指向列表中索引为n的元素处的ListIterator。

LinkedList

与ArrayList相比,它在List中间执行插入和删除操作的效率更高,不过随机访问操作的表现要差一些。 LinkedList还添加了一些可以使其用作栈、队列或双端队列(deque)的方法:

  • getFirst()和element()用法完全相同,都返回列表的头部而不移除它,当列表为空抛出NoSuchElementException,peek()为当列表为空时返回null。

  • removeFirst()和remove()也是完全相同的,都会移除并返回列表的头,对于空列表抛出NoSuchElementException,poll()为当列表为空时返回null。

  • addFirst()在列表的开头插入一个元素。

  • offer()和add()以及addLast()相同,都是向列表尾部插入一个元素。

  • removeLast()移除并返回列表中的最后一个元素。

Stack

栈是一个“后进先出(LIFO)”的集合,也被称为下推栈。Java 6加入ArrayDeque,提供了直接实现栈功能的方法。使用ArrayDeque构造Stack类:

类名后的表示这是类型参数为T的参数化的类型,当这个类被使用时,它会被替换为实际类型。我们定义了一个持有T类型对象的Stack。Stack是使用ArrayDeque实现的,它也持有T类型对象。注意,push()接受一个T类型的对象,而peek()和pop()返回一个T类型的对象。peek()方法用于提供栈顶元素,并不移走,而pop()移除并返回顶端元素。

Set

Set中不允许出现重复的对象值。最常见的用法是测试成员身份,即询问某个对象是否在Set中。所以查找通常是Set最重要的操作,我们通常选择一个HashSet的实现,他对快速查找做了优化。

Set就是一个Collection,只是行为不同。Set根据对象的值来确定成员身份。通常是使用contains()来测试Set成员身份。

HashSet使用哈希来提升速度,因为每种实现存储元素的方式不同,HashSet所维护的顺序与TreeSet或LinkedHashSet不同。TreeSet将元素排序存储在红黑树数据结构中,LinkedHashSet也使用哈希来提升查找速度,但看起来是使用链表按照插入顺序来维护元素的。

Map

将对象映射到其他对象。下面是一个验证Random类随机性的例子,生成大量随机数并计算落入不同区间的数的数量,Map的键是Random生成的数,值为该数出现的次数。

代码中get()方法,当集合中不存在该键时返回null,否则返回与该建关联的Integer值。

类似于数组和Collection,Map可以扩展为多维,例如创建一个值为Map的Map(那些Map的值可以是其他集合,甚至是其他Map)。

JDK 16增加了record关键字,原因:要让一个类的对象可以用作Map(或Set)中的键,必须为这个类定义两个函数:equals()和hashCode()。写对很难,如果以后要修改这个类的话,还很容易破坏它们,这就使得将这样的对象用作Map或Set中的键更难了。

record定义的是希望成为数据传输对象(也叫数据载体)的类。当使用record关键字时,编译器会自动生成:

  • 不可变的字段(即final字段)

  • 一个规范的构造器(即为各字段赋值的构造器)

  • 每个元素都有的访问器方法

  • equals()

  • hashCode()

  • toString()

对于大多数record,只需给它一个名字和参数,不需要在定义体中添加任何东西。不能向record中添加字段,只能将其定义在头部,不过可以加入静态的方法、字段和初始化器。

record可以定义方法,但这些方法只能读取字段,不能更改,因为这些字段会自动成为final变量。record也可以由其他对象组成,包括其他record,如record Company(Employee[] e) {}。不能继承record,因为它隐含为final,此外record也不能继承其他类,但是可以实现interface。record也可以嵌套在类中,或在某个方法内定义,嵌套和局部的record隐含都是静态的

尽管规范的构造器会被根据record的参数自动创建出来,但是我们可以使用一个紧凑构造器(compact constructor)来添加构造器行为,这种构造器无参数列表。紧凑构造器通常用于验证参数,也可以修改字段的初始化值。

尽管这看起来好像是在修改final值,但是编译器实际上会为x创建一个中间的占位符,然后在构造器的最后执行一次赋值,将结果赋值给this.x。

若有必要,也可以使用普通构造器,但是普通构造器的参数列表必须与record参数列表一摸一样,而且还必须初始化字段。极少情况会用到普通构造器。

创建record,编译器会为其生成equals()方法,以确保复制的record副本与原来的对象是等同的。

Queue

队列是一个典型的先进先出(FIFO)的集合,常用来将对象从程序的一个区域可靠地转移到另一个区域。LinkedList实现了Queue接口,通过将LinkedList向上转型为Queue作为Queue的一种实现使用。

offer()是Queue特有的方法之一,在队列尾部插入一个元素,无法插入返回false。peek()和element()都会返回队列的头部元素,但不删除,区别为当队列为空时,peek()返回null,element抛出异常。poll()和remove()将头部元素从队列中删除并返回,当队列为空,poll()返回null,remove()抛出异常。

Queue使得无法访问LinkedList的方法,而只有在这个接口中定义的方法才能访问。Queue特有的方法提供了完整旦独立的功能,即虽然Queue继承了Collection,但是不需要Collection中的任何方法。

PriorityQueue(优先级队列):当调用offer()方法将一个对象放到priorityQueue上时,这个对象会在排序后放入队列中。默认的排序方法使用的是对象在队列中的自然顺序,但可以通过提供自己的Comparator来修改顺序。PriorityQueue确保当调用peek()、pol()或remove()时得到的是优先级最高的元素。

Integer、String和Character能配合PiiorityQueue使用,是因为这些类已经有自然顺序。如果想在PriorityQueue中使用自己的类,就必须包含额外的用来生成自然顺序的功能,或提供自己的Comparator。

Collection和Iterator对比

Collection是所有序列集合共同的根接口。java.util.AbstractCollection类提供了Collection的一个默认实现,可以创建AbstractCollection的新子类,避免不必要的代码重复,即只需实现具体的集合行为,不用为Collection接口中的方法重新编写相同代码。

支持使用这样一个接口的理由是,它可以创建更通用的代码,通过面向接口而不是面向实现来编写代码,这样我们的代码可以应用于更多对象类型。

Iterator和Collection都可以配合Map和Collection的子类型使用,且不需要理解底层集合的特定实现。但是当实现一个不是Collection的外部类时,实现Collection接口需要实现大量方法很麻烦,尽管可以通过继承AbstractCollection类来简化实现Collection,但仍需实现"iterator()"和"size()"方法。这时候若只需要遍历集合而不需要实现Collection接口的方法时,使用Iterator是一个更简单有效的选择。

生成一个Iterator,是将序列与处理序列的方法连接起来的耦合性最低的方式,与实现Collection相比,这样做对序列类的约束要少得多。

for-in和迭代器

for-in可以配合任何Collection对象使用,如:

其原理是,Java 5引入了一个叫作Iterable的接口,该接口所包含的iterator()方法会生成一个Iterator。for-in使用这个Iterable接口来遍历序列。因此,如果我们创建了任何一个实现Iterable接口的类,都可以将其用于foi-in语句中。

这里iterator()返回的是实现Iterable的匿名内部类的一个实例,通过它可以获得数组中的每一个单词。

对于数组,可以配合for-in使用但其并没有自动实现Iterable,传递时需要先将其显式转换为Iterable,如:

Java集合类: 除了TreeSet,所有Set都与Collection拥有完全相同的接口。尽管List需要来自Collection的方法,但是List和Collection明显不同。另一方面,Queue接口中的方法是自成一体的,在实现Queue功能时,不需要Collection中的方法。Map和Collection之间唯一的交集是Map可以使用entrySet()和values()方法生成Collection。

注意标记接口java.util.RandomAccess,ArrayList使用了它,但是LinkedList没有。它用于为算法提供信息,算法可以根据特定的List来动态改变其行为。

函数式编程

lambda表达式

lambda表达式是使用尽可能少的语法编写的函数定义。通常用来实现函数式接口(即只有一个抽象方法的接口)。

基本语法:参数 -> 方法体,通常情况是用括号将参数包裹起来,单个参数可以不写括号,无参数时必须用括号指示空的参数列表。当方法体只有一行时,方法体中表达式的结果会自动成为返回值;若lambda表达式有多行时,必须将代码放入花括号中,这时又需要用return来返回一个值了。

方法引用

格式:类名或对象名 :: 方法名。是一种简化Lambda表达式的方式,通常用于将一个方法作为参数传递给另一个方法或者赋值给一个函数式接口。

Runnable接口:遵从特殊的单方法接口格式,其run()方法没有参数,也无返回值。可以将lambda表达式或方法引用用作Runnable。

Thread对象接受一个Runnable作为其构造器参数,它有一个start()方法会调用run()。该代码中只有匿名内部类需要提供名为run()的方法。

未绑定方法引用指尚未关联到某个对象的普通方法(非静态)。对于未绑定引用,必须先提供对象才能使用。也叫任意类型实例方法引用,即当Lambda参数列表的第一个参数是实例方法的调用者,后续参数是该方法参数时,可直接用类名引用实例方法:

构造器方法引用:可以捕获对某个构造器的引用,然后通过该引用调用这个构造器。

函数式接口

问题:方法引用和lambda表达式都必须赋值,而这些赋值都需要类型信息,让编译器确保类型的正确性。如x -> x.toString(),返回类型必须是String,但是我们不知道x是什么类型,所以编译器必须要可以以某种方式推断出x的类型。还有假设想把System.out::println传递给正在编写的方法,那么该方法的参数类型该怎么确定呢。

为解决该问题,Java 8引入包含一组接口的java.util.function,这些接口是lambda表达式和方法引用的目标类型。每个接口都只包含一个抽象方法,叫做函数式方法。编写接口时,可以使用@functionalInterface注解来强制实施函数式方法,该注解是可选的。

将一个方法引用或lambda表达式赋值给某个函数式接口(类型可以匹配),Java会调整这个赋值使其匹配目标接口。而在底层,Java编译器会创建一个实现了目标接口的类的实例,并将方法引用或lambda表达式包裹在其中。

java.util.function旨在创建一套足够完备的目标接口,这样一般情况下我们就不需要定义自己的接口了。接口的数量虽有了明显的增长,这主要是由于基本类型的缘故。如果理解命名模式,一般来说通过名字就可以了解特定的接口是做什么的。下面是基本的命名规则:

  1. 如果接口只处理对象,而非基本类型,那就会用一个直截了当的名字,像Function、Consumer和Predicate等。参数类型会通过泛型添加。

  2. 如果接口接受一个基本类型的参数,则会用名字的第一部分来表示,例如LongConsumer、DoubleFunction和IntPredicate等接口类型。基本的Supplier类型是个例外。

  3. 如果接口返回的是基本类型的结果,则会用To来表示,例如ToLongFunction和IntToLongFunction。

  4. 如果接口返回的类型和参数类型相同,则会被命名为Operator。UnaryOperator用于表示一个参数,BinaryOperator用于表示两个参数。

  5. 如果接口接受一个参数并返回boolean,则会被命名为Predicate。

  6. 如果接口接受两个不同类型的参数,则名字中会有一个Bi(如BiPredicate)。 下表描述了java.util.function中的目标类型:

特征
函数式方法命名
实例

无参数;无返回值

Runnable(java lang) run()

Runnable

无参数;可返回任意类型

Supplier get() getAs...()

Supplier BooleanSupplier IntSupplier LongSupplier DoubleSupplier

无参数;可返回任意类型

Callable(java.util.concurrent) call()

Callable

1个参数;无返回值

Consumer accept()

Consumer IntConsumer LongConsumer

2个参数的Consumer

BiConsumer accept()

BiConsumer<T, U>

两个参数的Consumer:引用,基本类型

ObjtypeConsumer accept()

ObjIntConsumer ObjLongConsumer ObjDoubleConsumer

1个参数;返回值为不同类型

Function apply() Totype & typeTotype applyAs...()

Function<T, R> IntFunction LongFunction DoubleFunction ToIntFunction ToLongFunction ToDoubleFunction IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction

一个参数;返回值为相同类型

UnaryOperator apply()

UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator

两个相同类型参数;返回值也是相同类型

BinaryOperator apply()

BinaryOperator IntBinaryOperator LongBinaryOperator DoubleBinaryOperator

两个相同类型参数;返回int

Comparator(java.util) compare()

Comparator

两个参数;返回boolean

Predicate test()

Predicate<T> BiPredicate<T, U> IntPredicate LongPredicate DoublePredicate

基本类型参数,返回基本类型

typeTotypeFunction applyAs...()

IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntfunction DoubleToLongFunction

两个参数;不同类型

Bi + 操作名(方法名会变)

BiFunction<T, U, R> BiConsumer<T, U> BiPredicate<T, U> ToIntBiFunction<T, U> ToLongBiFunction<T, U> ToDoubleBiFunction<T, U>

带有更多参数的函数式接口:java.util.function库中最多只有两个参数的接口,若需要更多参数则要编写自己的接口。

高阶函数

高阶函数只是一个能接受函数作为参数或能把函数当返回值的函数。

这里使用了Function接口中的andThen()的默认方法,andThen()会在in函数调用之后调用。

闭包

闭包是一个包含函数及其在创建时可以访问的变量环境的封装体。简而言之,闭包是一个函数,它能够捕获和访问其创建时所在的词法作用域中的变量,即使在创建它的作用域已经销毁之后仍然可以使用这些变量。

考虑一个使用了其函数作用域之外的变量的lambda表达式,当调用该函数时,它所引用的“外部变量”会发生什么,如果语言能够自动解决这个问题,即该语言是支持闭包的,也称其支持词法作用域,同时还涉及到变量捕获概念。

当i为外部变量时,每次调用该方法都会增加,但是当i为内部变量时,不能对变量i进行i++操作,此时的变量i为隐式的final变量。如果一个局部变量的初始值从不改变,它就是“实际上”的final变量。

使用包装类类型,如list,此时可以修改List中的内容,且每次调用该方法都会生成不同的ArrayList。但是不能对List重新赋值,而重新赋值正是引发“实际上最终变量”错误消息的元凶。

函数组合

函数组合即将多个函数结合使用,以创建新的函数。

组合方法
支持的接口

andThen(argument) 先执行原始操作,再执行参数操作

Function BiFunction Consumer BiConsumer IntConsumer... UnaryOperator... BinaryOperator

compose(argument) 先执行参数操作,再执行原始操作

Function UnaryOperator...

and(argument) 对原始谓词和参数谓词执行短路逻辑与(AND)计算

Predicate BiPredicate...

or(argument) 对原始谓词和参数谓词执行短路逻辑或(OR)计算

Predicate Bipredicate...

negate() 所得谓词为该谓词的逻辑取反

Predicate BiPredicate...

p4接受所有的谓词,并将其组合成一个更复杂的谓词。即“如果这个String不包含‘bar’并且长度小于5,或者其中包含‘foo’,则结果为true”。

Currying(柯里化)

柯里化的意思是将一个接受多个参数的函数转变为一系列只接受一个参数的函数。

流是一个与任何特定的存储机制都没有关系的元素序列。不同于在集合中遍历元素,使用流的时候,我们是从一个管道中抽取元素,并对它们逬行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线。

按照有序方式显示随机选择的5~20范围内的、不重复的int数。

像第二种那样显式地编写迭代机制,称为外部迭代,而在第一种使用流实现的机制称为内部迭代,这是流编程的一个核心特性。

流的另一个重要方面是惰性求值,这意味着它们只在绝对必要时才会被求值。我们可 以把流想象成一个“延迟列表” 因为延迟求值,所以流使我们可以表示非常大的(甚至 是无限大的)序列,而不用考虑内存问题。

java 8对流的支持

将流整合到现有的库中的最大挑战是使用了接口的库,因为想要将集合转换为流,集合类是至关重要的一部分。如果向接口中加入新方法就会破坏每个实现了该接口但没有实现新方法的类。java 8的解决方法是接口中的default方法,使用default方法可以将流方法硬塞进现有的类中,可能需要的操作类型分为三种:创建流,修改流元素(中间操作)和消费流元素(终结操作)。最后一种类型的操作往往意味着收集一个流的元素(通常是将其放进某个集合)。

流的创建

使用Stream.of()可以将一组条目变成一个流。每个Collection都可以使用stream()方法来生成一个流。

中间的map()操作接受流中的毎个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去。普通的map()接受对象并生成对象,但是当希望输出流为数值类型时,可以有特殊版本使用,如mapToInt()将一个对象流转变成一个包含Integer的IntStream,还有float和double类型也有类似操作。

为了从Map集合生成一个流,需要首先调用entrySet()来生成一个对象流,其中每个对象都包含着一个键和与其关联的值。

Random类只会生成int、long和double等基本类型的值,可以通过boxed()操作自动将基本类型转换为对应的包装器类型,使得show()可以接受这个流。

IntStream类提供了一个range()方法,可以生成一个流———由int值组成的序列。

Stream.generate()方法:

Stream.iterate() 从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法。其结果被添加到这个流上,并且保存下来作为下一次iterate()调用的第一个参数,以此类推。

这里Fibonacci数列中的最后两个元素相加,生成下一个元素。iterate()只会记住result,所以必须使用x来记住另一个元素。

流生成器:Stream库提供了一个Builder,可以创建一个生成器对象,为它提供多段构造信息,最后执行生成(build)操作。

split()中的语法表示每一行(line)或是在空格处分割,或是在方括号内定义的任何标点符号处分割。右方括号后的+表示“前面的事物可以出现一次或多次”。如果在调用build()后还尝试向Stream.Builder中添加单词,就会产生异常。

Arrays类中包含了名为stream()的静态方法,可以将数组转化为流。

splitAsStream()方法在java.util.regex.Pattern类中,它接受一个字符序列,并根据传入的公式(正则表达式(regular expression))将其分割为一个流。但是splitAsStream()方法的输入不是一个流,而是一个CharSequence。

这里多次调用stream(),每次都从保存的String创建一个新的流。但是这里整个文件都要存储在内存中,会导致无法利用流的以下优势:

  1. 它们“不需要存储”,意思是仅需序列的一小部分。

  2. 它们是惰性求值的。

中间操作

跟踪和调试:使用peek()操作可以查看流对象而不修改它们,是用来辅助调试的。

对流元素排序:使用sorted()可以以默认的方式比较排序,也可以使用接受Comparator参数的sorted()形式,该参数可以是一个lambda函数,也可以是预先定义好的Comparator。

移除元素

  • 使用distinct()可以移除流中的重复元素,与创建一个Set来消除重复元素相比,使用distinct()要省力很多。

  • filter(Predicate):过滤操作只保留符合特定条件的元素,也就是传给参数,结果为true的那些元素。

rangeClosed()包含了上界值。如果没有任何一个取余操作的结果为0,则noneMatch()操作返回tiue,如果有任何一个计算结果等于0,则返回false。noneMatch()会在第一次失败之后退出,而不会把后面的所有计算都尝试一遍。

将函数应用于每个流元素

  • map(Function):将Function应用于输入流中的每个对象,结果作为输出流继续传递。

  • mapToInt(ToIntFunction)、mapToLong(ToLongFunction)、mapToDouble(ToDoubleFunction):同上,但是结果放在各自的Stream中。

在“Increment”测试中,使用Integer.parseInt()尝试将String转化为Integer。若这个String不能表示为Integer,就会抛出NumberFormatException。

如果Function生成的结果类型是某种数值类型,则必须用相应的mapTo操作代替:

应用map期间组合流:当使用map()函数应用于由传入元素组成的流时,它生成了一个流,但是不是由元素组成的流,而是一个由元素流组成的流。

这里使用map()得到的是一个指向其他流的“头”组成的流。

flatMap()会做两件事:接受生成流的,并将其应用于传入元素(就像map()所做的那样),然后再将每个流“扁平化”处理,将其展开为元素。所以传出来的就都是元素了。

  • flatMap(Function):当Function生成的是一个流时使用。

  • flatMapToInt(Function):当Function生成的是一个IntStream时使用。

  • flatMapToDouble(Function)、flatMapToLong(Function)类似。

上面提到的FileToWordsRegexp.java存在一个问题,就是需要将整个文件都读入到一个由文本行组成的List中,这也就需要对应的存储空间。而我们真正想要的是创建一个不需要中间存储的单词流,这正是flatMap()所要解决的问题:

这里正则表达式模式使用的是\W+。\W意思是一个“非单词字符”,+意思是“一个或多个”。小写\w表示“单词字符”。

之前遇到的问题是,pattern.compile().splitAsStream()生成的是一个流,这意味着在由文本行组成的输入流上调用map(),会生成一个由单词流组成的流,而这时我们需要的只是一个单词流而已。而flatMap()可以将元素流组成的流扁平化,将其变为由元素组成的一个简单流。

因为现在得到的是一个真正的流(而不是像FileToWordsRegexp.java中那样,先将文件中的内容存储到一个集合中,然后基于这个集合创建的流),所以毎当我们想要一个新的流时,都必须再创建,因为它无法复用。

Optional类型

当向流中请求对象,但是流中什么都没有,这时会发生什么?有没有某种可以使用的对象,既可以作为流元素占位,也可以在我们要找的元素不存在时友好地告知我们。这个想法被实现为Optional类型,某些标准的流操作会返回Optional对象,因为它们不能确保所要的结果一定存在。某些流操作列举如下:

  • findFirst()返回包含第一个元素的Optional。如果这个流为空,则返回Optional.empty。

  • findAny()返回包含任何元素的Optional,如果这个流为空,则返回Optional.empty。

  • max()和min()分别返回包含流中最大值和最小值的Optional。如果这个流为空,则返回Optional.empty。

  • reduce()的一个版本,它并不以一个"identity"对象(初始值,在流为空时提供默认值)作为其第一个参数(在reduce()的其他版本中,"identity"对象会成为默认结果,所以不会有结果为空的风险),它会将返回值包在一个Optional中。

  • 对于数值化的流IntStream、LongStream和DoubleStream,average()操作将其结果包在一个Optional中,以防流为空的情况。

这时不会因为流是空的而抛出异常,而是会得到一个Optional.empty对象。Optional有一个toString()方法,可以显示有用的信息。这里空流是通过Stream.empty()创建的。如果只用Stream.empty(),会因为缺少上下文信息而不知道它是什么类型。就如同这样:Stream<String> s = Stream.empty()就可以推断出empty()调用的类型。"String.CASE_INSENSITIVE_ORDER"是一个Comparator对象,用于执行不区分大小写的字符串比较。

接收到一个Optional时,首先要调用isPresent(),看该对象内有没有包含值。如果有再使用get()来获取。

便捷函数:有很多函数可用于获取Optional中的数据,它们简化了上面“先检查再处理所包含对象”的过程。

  • ifPresent(Consumer):如果值存在,则用这个值来调用Consumer,否则什么都不做。

  • orElse(otherObject):如果对象存在,则返回这个对象,否则返回otherObject。

  • orElseGet(Supplier):如果对象存在,则返回这个对象,否则返回使用Supplier函数创建的替代对象。

  • orElsethrow(Supplier):如果对象存在,则返回这个对象,否则抛出一个使用Supplier函数创建的异常。

创建Optional:生成Optional的三种可使用的静态方法

  • empty(),返回一个空的Optional。

  • of(value),若已知value不为null,该方法将其包在一个Optional中。

  • ofNullable(value):针对一个可能为空的值,为空时自动生成Optional.empty,否则将值包装在 Optional中。

Optional对象上的操作:当流管道生成了Optional对象,下面3个方法可使得 Optional的后续能做更多的操作:

  • filter(Predicate):将Predicate应用于Optional的内容,并返回结果。若Optional与Predicate不匹配,则将其转换为empty。若本身就是empty,直接返回。

  • map(Function):如果Optional不为empty,则将Function应用于Optional中包含的对象,并返回结果。否则返回empty。

  • flatMap(Function):与map()类似,但是所提供的映射函数会将结果包在Optional中,于是flapMap()最后就不会再做任何包装了。(即不会出现 map() 那样的嵌套 Optional 结构)

数值化的Optional没有上面这些操作。对于普通的流flutter(),如果Predicate返回False,则会将元素从流中删除。

由Optional组成的流:假设有一个可能会生成null值的生成器,若用这个生成器创造一个流,并且想要将这些元素包在Optional中:

使用这个流,然后获得Optional中的对象:

终结操作

接受一个流,并生成一个最终结果。终结操作是在一个管线中可以做的最后一件事。

将流转换为一个数组

  • toArray(): 将流元素转换到适当类型的数组中;

  • toArray(generator): generator用于在特定情况下分配自己的数组存储。(即生成器函数,通常是一个数组构造函数引用)

在每个流元素上应用某个终结操作

  • forEach(Consumer): 例如System.out::println;

  • forEachOrdered(Consumer): 这个版本确保forEach对元素的操作顺序是原始的流顺序。

第一种形式被明确地设计为可以以任何顺序操作元素,这只有在引入parallel()操作时才有意义。

收集操作

  • collect(Collector): 使用这个Collector将流元素累加到一个结果集合中。

  • collect(Supplier, BiConsumer, BiConsumer): 与上面类似,但是Supplier会创建一个新的结果集合,第一个BiConsumer是用来将下一个元素包含到结果中的函数,第二个BiConsumer用于将两个值结合起来。

Pairs是一个基本的数据对象,RandomPair的stream()方法会生成一个Pair流。capChars是一个会生成大写字母的Iterator,它的定义从一个流开始,这个流由随机生成的位于65和91之间的int组成,mapToObj()用于将这些int转为char,然后自动装箱为Charactor。main()使用的是Collector.toMap()的最简单形式,只需从流中取出键和值的函数(还有其他形式,如接受一个可以处理键碰撞的函数)。

组合所有的流元素

  • reduce(BinaryOperator):使用BinaryOperator来组合所有的流元素。因为这个流可能为空,所以返回的是一个Optional。

  • reduce(identity, BinaryOperator):与上面一样,但是将identity作为这个组合的初始值。因此即使流为空,仍然可以得到identity作为结果。

  • reduce(identity, BiFunction, BinaryOperator):更高效的方式,可以通过组合显式的map()和reduce()操作来更简单地表达这样的需求。

reduce()中lambda表达式中第一个参数fr0是上次调用这个reduce()时带来的结果,第二个参数fr1是来自流中的新值。且使用了一个三元选择操作符,如果fr0的size小于50,就接受fr0,否则接受fr1,也就是序列中的下一个元素。作为结果,我们得到的是流中第一个size小于50的Frobniz,一旦找到这样一个对象,它就会抓住不放,哪怕后面还有其他候选。

匹配

  • allMatch(Predicate):当使用Predicate检测流中的元素时,如果每一个元素都得到true,则返回true。在遇到第一个false时,会短路计算。即在找到一个false后,它不会继续计算。

  • anyMatch(Predicate):当使用Predicate检测流中元素,如果有任何一个元素得到true,则返回true。在遇到第一个true时会短路计算返回true。

  • noneMatch(Predicate):如果没有任何一个元素得到true,则返回true。在遇到第一个true时,短路计算返回false。

这是一种一般化地描述所有这三种匹配操作的方法,注意到还将其变成了一个Match接口。

BiPredicate是一个二元谓词,即接受两个参数,并返回boolen。第一个参数是要测试的数值的流,第二个参数是谓词Predicate本身。因为Match接口能匹配所有Stream::...Match函数的模式,所以可以将每一个函数传给show()。对match.test()的调用会被翻译为对Stream::...Match函数的调用。

show()接受一个Match和一个val,后者表示谓词测试“n < val”中的最大数字。该方法生成了一个从1到20的Integer流。peek()表明在短路发生之前测试已经走了多远。

选择一个元素

  • findFirst():返回一个包含流中第一个元素的Optional,若流中无元素则返回Optional.empty。

  • findAny():返回一个包含流中某个元素的Optional,流中无元素返回Optional.empty。

findFirst()不管是否是并行流(通过parallel()获得的流)总是选择第一个元素。对于非并行的流,findAny()会选择第一个元素(尽管从定义上它可以选择任何一个元素)

选择某个流的最后一个元素使用reduce()。

这里reduce()的参数是用两个元素中的后一个替换了这两个,这样最终得到的就是流中最后一个元素了。

获得流相关信息

  • count():获得流中元素数量。

  • max(Comparator):通过Comparator确定流中最大元素。

  • min(Comparator):确定流中最小元素。

min()和max()会返回Optional,这里使用orElse()获得其中的值。

数值化流的相关信息

  • average():获得平均值。

  • max()与min():不需要Comparator。

  • sun():累加流中的数值。

  • summaryStatistics():返回可能有用的摘要数据。

最后更新于