Oracle-专业认证-JavaSE8-编程测验-二-

82 阅读1小时+

Oracle 专业认证 JavaSE8 编程测验(二)

协议:CC BY-NC-SA 4.0

三、高级类设计

认证目标
开发使用抽象类和方法的代码
开发使用 final 关键字的代码
创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类
使用枚举类型,包括枚举类型中的方法和构造函数
开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释
创建和使用 Lambda 表达式

在前一章中,您学习了 OOP 的基本概念,并使用它们来构建 Java 程序。在这一章中,你将学习高级的类设计概念。您还将了解 Java 8: lambda 表达式中引入的关键函数式编程特性。

OCPJP 考试中很大一部分问题与 Java 语言和 Java 8 函数库的变化有关。本章涵盖了 lambda 表达式,它构成了理解 Stream API 和java.util.function包中可用设施的基础。因此,请务必阅读本章中关于 lambda 表达式的接口部分和最后一部分。

抽象类

认证目标
开发使用抽象类和方法的代码

在许多编程情况下,您希望指定一个抽象,而不指定实现级别的细节。在这种情况下,您可以使用抽象类或接口。当您想要定义一个具有一些公共功能的抽象时,可以使用抽象类。

考虑一下Shape类,它提供了您可以在绘图应用中绘制的不同形状的抽象。

abstract class Shape {

public double area() { return 0; } // default implementation

// other members

}

在类定义前加上关键字abstract,将该类声明为抽象类。你可以创建Shapes的对象比如SquareCircle,但是直接创建一个Shape类本身的对象有意义吗?不,没有名为Shape的真实世界物体。

如果你试图创建一个Shape类的实例,编译器会给出一个错误,因为抽象类不能被实例化。

Shape类定义中,有一个名为area()的方法返回特定形状的面积。这个方法适用于所有形状,这就是为什么它在这个基类Shape中。然而,Shape类中的area()方法的实现应该是怎样的呢?您不能提供默认实现;将这个方法实现为return 0;是一个糟糕的解决方案,尽管编译器会很乐意接受它。更好的解决方案是将其声明为抽象方法,如下所示:

public abstract double area(); // note: no implementation (i.e., no method body definition)

与声明类抽象类似,通过在方法前面加上关键字abstract来声明方法area()是抽象的。普通方法和抽象方法的主要区别在于,你不需要为抽象方法提供主体。如果你提供一个主体,它将变成一个错误,就像这样:

public abstract double area() { return 0; } // compiler error!

对于这个定义,您会得到一个编译器错误:"abstract methods cannot have a body"。抽象方法声明迫使所有子类提供该抽象方法的定义,这就是为什么它不能在抽象类本身中定义的原因。如果派生类没有实现基类中定义的所有抽象方法,则应该将该派生类声明为抽象类,如下例所示:

abstract class Shape {

public abstract double area(); // no implementation

// other members

}

class Rectangle extends Shape { }

这个代码片段导致了编译器错误"Rectangle is not abstract and does not override abstract method area() in Shape"。要解决这个问题,您需要声明派生类abstract或者在派生类中提供area()方法的定义。将Rectangle声明为抽象是没有意义的;所以你可以这样定义area()方法:

class Rectangle extends Shape {

private int length, height;

public double area() { return length * height; }

// other members …

}

要记住的要点

复习以下关于 OCPJP 八级考试抽象类和抽象方法的要点:

  • abstract关键字可以应用于一个类或非静态方法。
  • 抽象类可能有声明为静态的方法或字段。然而,abstract关键字不能应用于字段或静态方法。
  • 一个抽象类可以扩展另一个抽象类,也可以实现一个接口。
  • 抽象类可以从具体类派生出来!虽然语言允许,但这样做并不是一个好主意。
  • 抽象类不需要声明抽象方法,这意味着抽象类不需要将任何方法声明为抽象。但是,如果一个类有一个抽象方法,它应该被声明为一个抽象类。
  • 抽象类的子类需要提供所有抽象方法的实现;否则,您需要将该子类声明为抽象类。

使用“最终”关键字

认证目标
开发使用 final 关键字的代码

final关键字可以应用于类、方法和变量。不能扩展 final 类,不能重写 final 方法,也不能在 final 变量初始化后更改其值。

最终课程

final 类是一个不可继承的类,也就是说,如果你声明一个类为 final,你就不能继承它。您可能不希望一个类被子类化的两个重要原因是:

To prevent a behavior change by subclassing. In some cases, you may think that the implementation of the class is complete and should not change. If overriding is allowed, then the behavior of methods might be changed. You know that a derived object can be used where a base class object is required, and you may not prefer it in some cases. By making a class final, the users of the class are assured the unchanged behavior.   Improved performance. All method calls of a final class can be resolved at compile time itself. As there is no possibility of overriding the methods, it is not necessary to resolve the actual call at runtime for final classes, which translates to improved performance. For the same reason, final classes encourage the inlining of methods. With inlining, a method body can be expanded as part of the calling code itself, thereby avoiding the overhead of making a function call. If the calls are to be resolved at runtime, they cannot be inlined.  

在 Java 库中,很多类都声明为final;例如,String (java.lang.String)System (java.lang.System)类。这些类在 Java 程序中被广泛使用。如果这两个类没有被声明final,有人可能通过子类化来改变这些类的行为,然后整个程序可以开始不同的行为。为了避免这样的问题,像这样广泛使用的类和包装类如NumberInteger都在 Java 库中做成了final

A978-1-4842-1836-5_3_Figbb_HTML.gif使一个类成为 final 类的性能增益是适度的;重点应该是在适当的地方使用final。OCPJP 8 考试主要会考查你是否知道如何正确使用final关键词。你不用担心效率细节。

最终方法和变量

在一个类中,你可以声明一个方法为 final。final 方法不能被重写。因此,如果您已经在非 final 类中将某个方法声明为 final,则可以扩展该类,但不能重写 final 方法。但是,基类中的其他非最终方法可以在派生类实现中重写。

考虑一下Shape类中的方法setParentShape()getParentShape()(列表 3-1 )。

Listing 3-1. Shape.java

public abstract class Shape {

// other class members elided

final public void setParentShape(Shape shape) {

// method body

}

public Shape getParentShape() {

// method body

}

}

在这种情况下,Circle类(Shape的子类)只能覆盖getParentShape();如果您试图覆盖 final 方法,您将得到以下错误:"Cannot override the final method from Shape"

Final 变量就像光盘:一旦你在上面写了什么,你就不能再写了。在编程中,像 PI 这样的常量可以被声明为 final,因为你不希望任何人修改它们的值。如果你试图在初始化之后改变一个最终变量,你将得到一个编译器错误。

要记住的要点

复习以下要点,因为它们可能会在 OCPJP 八级考试中出现:

  • final修饰符可以应用于一个类、方法或变量。final 类的所有方法都是隐式的final(因此是不可重写的)。
  • 一个final变量只能赋值一次。如果变量声明将变量定义为final但没有初始化它,那么它被称为 blank final。您需要在类或初始化块中定义的所有构造函数中初始化一个空白的 final。
  • 关键字final可以应用于参数。一旦分配,final参数的值就不能更改。

嵌套类的风格

认证目标
创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类

在另一个类(或接口)体内定义的类称为嵌套类。通常定义一个类,它是直接属于包的顶级类。相反,嵌套类是包含在另一个类或接口中的类。

在另一个类或接口中创建类有什么好处?有几个好处。首先,您可以将相关的类放在一起作为一个逻辑组。其次,嵌套类可以访问封闭类的所有类成员,这在某些情况下可能很有用。第三,嵌套类简化了代码。例如,匿名内部类对于用 AWT/Swing 编写更简单的事件处理代码很有用。

Java 中有四种类型或风格的嵌套类:

  • 静态嵌套类
  • 内部类
  • 局部内部类
  • 匿名内部类

乍一看,这四种味道之间的区别并不明显。图 3-1 有助于阐明它们之间的区别。局部类在代码块(无论是方法、构造函数还是初始化块)中定义,而非局部类在类中定义。静态类使用static关键字限定,而非静态类在类定义中不使用static关键字。在匿名类中,你不提供类名;你只是定义它的身体。

A978-1-4842-1836-5_3_Fig1_HTML.jpg

图 3-1。

Types of nested classes with examples

正如你在图 3-1 中看到的,静态嵌套类是静态和非本地的,而内部类是非静态和非本地的。非静态局部嵌套类是局部内部类,局部匿名嵌套类是匿名内部类。

现在,让我们更详细地讨论这四种味道。

静态嵌套类(或接口)

您可以将一个类(或接口)定义为另一个类(或接口)内部的静态成员。由于外部类型可以是类或接口,内部类型也可以是类或接口,因此有四种组合。以下是这四种类型的示例,以便您可以了解它们的语法:

class Outer {            // an outer class has a static nested class

static class Inner {}

}

interface Outer {        // an outer interface has a static nested class

static class Inner {}

}

class Outer {            // an outer class has a static nested interface

static interface Inner {}

}

interface Outer {        // an outer interface has a static nested interface

static interface Inner {}

}

您不必在嵌套接口中显式使用static关键字,因为它是隐式静态的。现在,让我们看一个创建和使用静态嵌套类的例子。

考虑具有字段m_redm_green,m_blueColor类(列表 3-2 )。因为所有形状都可以着色,所以可以在一个Shape类中定义Color类。

Listing 3-2. TestColor.java

abstract class Shape {

public static class Color {

int m_red, m_green, m_blue;

public Color() {

// call the other overloaded Color constructor by passing default values

this(0, 0, 0);

}

public Color(int red, int green, int blue) {

m_red = red; m_green = green; m_blue = blue;

}

public String toString() {

return " red = " + m_red + " green = " + m_green + " blue = " + m_blue;

}

// other color members elided

}

// other Shape members elided

}

public class TestColor {

public static void main(String []args) {

// since Color is a static nested class,

// we access it using the name of the outer class, as in Shape.Color

// note that we do not (and cannot) instantiate Shape class for using Color class

Shape.Color white = new Shape.Color(255, 255, 255);

System.out.println("White color has values:" + white);

}

}

它可以打印

White color has:  red = 255 green = 255 blue = 255

在这段代码中,Shape类被声明为abstract。您可以看到Color类被定义为Shape类中定义的public static类。TestColor类使用语法Shape.Color来引用这个类。除了这个微小的区别,Color类看起来与在Shape类之外定义Color类没有什么不同。因此,静态嵌套类与定义为外部类的类一样好,只是有一点不同——它是在另一个类中物理定义的!

要记住的要点

以下是静态嵌套类(和接口)的一些值得注意的方面,将对你的 OCPJP 8 考试有所帮助:

  • 可达性(public, protected,等。)静态嵌套类是由外部类定义的。
  • 静态嵌套类的名称用OuterClassName.NestedClassName语法表示。
  • 当你在一个接口内定义一个内部嵌套类(或接口)时,嵌套类被隐式声明为publicstatic。这一点很容易记住:接口中的任何字段都被隐式声明为publicstatic,静态嵌套类也有同样的行为。
  • 静态嵌套类可以声明为abstractfinal
  • 静态嵌套类可以扩展另一个类,也可以用作基类。
  • 静态嵌套类可以有静态成员。(您很快就会看到,该语句不适用于其他类型的嵌套类。)
  • 静态嵌套类可以访问外部类的成员(显然只有静态成员)。
  • 外部类也可以通过嵌套类的对象访问嵌套类的成员(甚至是private成员)。如果不声明嵌套类的实例,外部类就不能直接访问嵌套类元素。

内部类

您可以将一个类(或一个接口)定义为另一个类中的非静态成员。在接口内部声明一个类或者一个接口怎么样?正如你在上面关于静态内部类的第三个要点中看到的,当你在一个接口中定义一个类或者一个接口时,它是隐式的static。所以,不可能声明一个非静态的内部接口!这就剩下了两种可能性:

class Outer {            // an outer class has an inner class

class Inner {}

}

class Outer {            // an outer class has an inner interface

interface Inner {}

}

让我们创建一个Point类来实现一个Circle的中心。既然您想将每个Circle与一个中心Point相关联,那么将Point作为Circle(清单 3-3 )的内部类是一个好主意。

Listing 3-3. Circle.java

public class Circle {

// define Point as an inner class within Circle class

class Point {

private int xPos;

private int yPos;

// you can provide constructor for an inner class like this

public Point(int x, int y) {

xPos = x;

yPos = y;

}

// the inner class is like any other class - you can override methods here

public String toString() {

return "(" + xPos + "," + yPos + ")";

}

}

// make use of the inner class for declaring a field

private Point center;

private int radius;

public Circle(int x, int y, int r) {

// note how to make use of the inner class to instantiate it

center = this.new Point(x, y);

radius = r;

}

public String toString() {

return "mid point = " + center + " and radius = " + radius;

}

public static void main(String []s) {

System.out.println(new Circle(10, 10, 20));

}

// other methods such as area are elided

}

在这个实现中,您已经将Point定义为Circle的私有成员。注意您是如何实例化内部类的:

center = this.new Point(x, y);

您可能想知道为什么不能使用通常的new语句:

center = new Point(x, y);

您需要为外部类的对象引用添加前缀,以创建内部类的实例。在这种情况下,它是一个this引用,所以您在new操作符前加上了前缀this

每个内部类都与外部类的一个实例相关联。换句话说,内部类总是与封闭对象相关联。

外部和内部阶层共享一种特殊的关系,就像朋友或同一家庭的成员。不管访问说明符是什么,成员访问都是有效的,比如private。然而,还是有细微的区别。您可以在内部类中访问外部类的成员,而无需创建实例;但是外部类却不是这样。为了访问内部类的成员(任何成员,包括私有成员),您需要创建一个内部类的实例。

内部类的一个限制是不能在内部类中声明静态成员,如下所示:

class Outer {

class Inner {

static int i = 10;

}

}

如果您尝试这样做,将会得到以下编译器错误:

Outer.java:3: inner classes cannot have static declarations

static int i = 10;
要记住的要点

以下是一些关于内部类和接口的重要规则,可能会在 OCPJP 8 考试中有用:

  • 可达性(publicprotected等)。)是由外部类定义的。
  • 就像顶级类一样,内部类可以扩展类或实现接口。类似地,其他类可以扩展内部类,其他类或接口可以扩展或实现内部接口。
  • 内部类可以声明为finalabstract
  • 内部类可以有内部类,但是你将很难阅读或理解如此复杂的类嵌套。(意思:避开他们!)

局部内部类

局部内部类是在代码块中定义的(比如在方法、构造函数或初始化块中)。与静态嵌套类和内部类不同,局部内部类不是外部类的成员;它们只是定义它们的方法或代码的局部变量。

以下是局部类的一般语法示例:

class SomeClass {

void someFunction() {

class Local { }

}

}

正如您在这段代码中看到的,Local是在someFunction中定义的一个类。它在someFunction之外是不可用的,甚至对SomeClass的成员也不可用。因为你不能声明一个局部变量static,你也不能声明一个局部类static

因为不能在接口中定义方法,所以在接口中不能有局部类或接口。也不能创建本地接口。换句话说,不能在方法、构造函数和初始化块中定义接口。

现在您已经理解了语法,让我们来看一个实际的例子。之前,您将Color类实现为静态嵌套类(清单 3-2 )。以下是您在讨论中看到的代码:

abstract class Shape {

public static class Color {

int m_red, m_green, m_blue;

public Color() {

this(0, 0, 0);

}

public Color(int red, int green, int blue) {

m_red = red; m_green = green; m_blue = blue;

}

public String toString() {

return " red = " + m_red + " green = " + m_green + " blue = " + m_blue;

}

// other color members elided

}

// other Shape members elided

}

现在,这个toString()方法显示了一个Color的字符串表示。假设您想要以下面的格式显示Color字符串:"You selected a color with RGB values red = 0 green = 0 blue = 0"。为此,您必须在类StatusReporter中定义一个名为getDescriptiveColor()的方法。在getDescriptiveColor()中,您必须创建一个Shape.Color的派生类,其中toString方法返回这个描述性消息。清单 3-4 是一个使用本地类的实现。

Listing 3-4. StatusReporter.java

class StatusReporter {

// important to note that the argument "color" is declared final

static Shape.Color getDescriptiveColor(final Shape.Color color) {

// local class DescriptiveColor that extends Shape.Color class

class DescriptiveColor extends Shape.Color {

public String toString() {

return "You selected a color with RGB values" + color;

}

}

return new DescriptiveColor();

}

public static void main(String []args) {

Shape.Color descriptiveColor =

StatusReporter.getDescriptiveColor(new Shape.Color(0, 0, 0));

System.out.println(descriptiveColor);

}

}

main 方法检查StatusReporter是否工作正常。这个程序打印

You selected a color with RGB values red = 0 green = 0 blue = 0

让我们看看局部类是如何定义的。getDescriptiveColor()方法接受普通的Shape.Color类对象并返回一个Shape.Color对象。在getDescriptiveColor()方法中,您定义了这个方法的本地类DescriptiveColor。这个DescriptiveColorShape.Color的派生类。在DescriptiveColor类中,唯一定义的方法是toString()方法,它覆盖了基类Shape.Color toString()方法。在定义了DescriptiveColor类之后,getDescriptiveColor类创建一个DescriptiveColor类的对象并返回它。

Test类中,您可以看到一个main()方法,它只调用了StatusReporter.getDescriptiveColor()方法并将结果存储在一个Shape.Color引用中。您会注意到,getDescriptiveColor()方法返回一个从Shape.Color派生的DescriptiveColor对象,因此descriptiveColor变量初始化工作正常。在println中,descriptiveColor的动态类型是DescriptiveColor对象,因此打印颜色对象的详细描述。

您是否注意到了getDescriptiveColor()方法的另一个特性?它的参数声明为final。即使你没有提供 final 关键字,编译器也将把 is 视为有效的 final——这意味着你不能给你在局部类中访问的变量赋值。如果您这样做,将会得到一个编译器错误,如:

static Shape.Color getDescriptiveColor(Shape.Color color) {

// local class DescriptiveColor that extends Shape.Color class

class DescriptiveColor extends Shape.Color {

public String toString() {

return "You selected a color with RGB values" + color;

}

}

color = null; // note this assignment – will NOT compile

return new DescriptiveColor();

}

您将得到以下编译器错误:

StatusReporter.java:8: error: local variables referenced from an inner class must be final or effectively final

return "You selected a color with RGB values" + color;

^

1 error

由于对color变量的赋值,它不再是最终变量,因此当局部内部类试图访问该变量时,编译器会给出一个错误。

您只能将最终变量传递给局部内部类。如果你没有声明一个局部内部类访问的变量,编译器会把它视为 final 变量。

要记住的要点

以下几点关于地方班的内容可能会在 OCPJP 八级考试中出现:

  • 您可以在代码体内创建一个非静态的局部类。接口不能有本地类,您也不能创建本地接口。
  • 局部类只能从定义该类的代码体中访问。局部类在定义该类的代码体之外是完全不可访问的。
  • 定义局部类时,可以扩展类或实现接口。
  • 局部类可以访问定义它的代码体中所有可用的变量。局部内部类访问的变量实际上被认为是最终变量。

匿名内部类

顾名思义,匿名内部类没有名字。该类的声明自动从实例创建表达式中派生。它们也被简称为匿名类。

匿名类在几乎所有可以使用局部内部类的情况下都很有用。局部内部类有名字,而匿名内部类没有——这是主要的区别。另一个区别是匿名内部类不能有任何显式构造函数。构造函数是以类名命名的,因为匿名类没有名字,所以不能定义构造函数!

(在我们继续之前,这里需要注意:没有“匿名接口”这样的东西)

下面是一个理解局部类语法的示例:

class SomeClass {

void someFunction() {

new Object() { };

}

}

这个代码看起来很神秘,不是吗?这是怎么回事?在语句new Object() { };中,您使用new关键字直接声明了一个Object的派生类。它不定义任何代码,而是返回该派生对象的一个实例。创建的对象没有在任何地方使用,所以它被忽略。new表达式调用这里的默认构造函数;您可以选择通过在new表达式中传递参数来调用基类的多参数构造函数。

现在让我们看一个更实际的例子。在前面的例子中(清单 3-4 ,您看到了在StatusReporter类的getDescriptiveColor方法中定义的DescriptiveColor类。您可以通过将本地类转换成匿名类来简化代码,如清单 3-5 所示。

Listing 3-5. StatusReporter.java

class StatusReporter {

static Shape.Color getDescriptiveColor(final Shape.Color color) {

// note the use of anonymous inner classes here

// -- specifically, there is no name for the class and we construct

// and use the class "on the fly" in the return statement!

return new Shape.Color() {

public String toString() {

return "You selected a color with RGB values" + color;

}

};

}

public static void main(String []args) {

Shape.Color descriptiveColor =

StatusReporter.getDescriptiveColor(new Shape.Color(0, 0, 0));

System.out.println(descriptiveColor);

}

}

它可以打印

You selected a color with RGB values red = 0 green = 0 blue = 0

真好。程序的其余部分,包括main()方法,保持不变,而getDescriptiveColor()方法变得更简单了!你没有明确地创建一个有名字的类(名字是DescriptiveColor);相反,您只是在 return 语句中“动态地”创建了一个Shape.Color的派生类。注意,关键字class也是不需要的。

要记住的要点

请注意以下关于匿名类的要点,它们可能对 OPCJP 8 考试有用:

  • 匿名类在new表达式本身中定义。
  • 定义匿名类时,不能显式扩展类或显式实现接口。

枚举数据类型

认证目标
使用枚举类型,包括枚举类型中的方法和构造函数

假设您希望用户从定义几种打印机类型的一组常量中进行选择:

public static final int DOTMATRIX = 1;

public static final int INKJET = 2;

public static final int LASER= 3;

解决方案是可行的。然而,在这种情况下,您可以传递任何其他整数(比如 10),编译器会欣然接受。因此,该解决方案不是类型安全的解决方案。在这种情况下,Java 5 引入了数据类型 enum 来帮助您。

清单 3-6 为上面的例子定义了一个枚举类(是的,枚举是特殊的类)。

Listing 3-6. EnumTest.java

// define an enum for classifying printer types

enum PrinterType {

DOTMATRIX, INKJET, LASER

}

// test the enum now

public class EnumTest {

PrinterType printerType;

public EnumTest(PrinterType pType) {

printerType = pType;

}

public void feature() {

// switch based on the printer type passed in the constructor

switch(printerType){

case DOTMATRIX:

System.out.println("Dot-matrix printers are economical and almost obsolete");

break;

case INKJET:

System.out.println("Inkjet printers provide decent quality prints");

break;

case LASER:

System.out.println("Laser printers provide best quality prints");

break;

}

}

public static void main(String[] args) {

EnumTest enumTest = new EnumTest(PrinterType.LASER);

enumTest.feature();

}

}

它可以打印

Laser printers provide best quality prints

让我们更详细地回顾一下清单 3-6 。

  • 在 switch-case 语句中,不需要为枚举元素提供完全限定的名称。这是因为 switch 接受枚举类型的实例,因此 switch-case 理解您在其中指定枚举元素的上下文(类型)。
  • 在创建枚举对象 E numTest时,我们已经提供了值PrinterType.LASER。如果我们提供除枚举值之外的任何其他值,您将会得到一个编译器错误。换句话说,枚举是类型安全的。

注意,您可以在一个单独的文件中声明一个 enum(在本例中为PrinterType),就像您可以声明任何其他普通的 Java 类一样。

现在让我们看一个更详细的例子,在这个例子中,您在一个枚举数据类型中定义成员属性和方法(清单 3-7 )。

Listing 3-7. EnumTest.java

enum PrinterType {

DOTMATRIX(5), INKJET(10), LASER(50);

private int pagePrintCapacity;

private PrinterType(int pagePrintCapacity) {

this.pagePrintCapacity = pagePrintCapacity;

}

public int getPrintPageCapacity() {

return pagePrintCapacity;

}

}

public class EnumTest {

PrinterType printerType;

public EnumTest(PrinterType pType) {

printerType = pType;

}

public void feature() {

switch (printerType) {

case DOTMATRIX:

System.out.println("Dot-matrix printers are economical");

break;

case INKJET:

System.out.println("Inkjet printers provide decent quality prints");

break;

case LASER:

System.out.println("Laser printers provide the best quality prints");

break;

}

System.out.println("Print page capacity per minute: " +

printerType.getPrintPageCapacity());

}

public static void main(String[] args) {

EnumTest enumTest1 = new EnumTest(PrinterType.LASER);

enumTest1.feature();

EnumTest enumTest2 = new EnumTest(PrinterType.INKJET);

enumTest2.feature();

}

}

上述程序的输出如下所示:

Laser printers provide the best quality prints

Print page capacity per minute: 50

Inkjet printers provide decent quality prints

Print page capacity per minute: 10

在这个程序中,您为 enum 类定义了一个新属性、一个新构造函数和一个新方法。属性pagePrintCapacity由 enum 元素(比如LASER(50))指定的初始值设置,它调用 enum 类的构造函数。但是,枚举类不能有公共构造函数,否则编译器会报错如下消息:"Illegal modifier for the enum constructor; only private is permitted"

枚举类中的构造函数只能指定为私有。

要记住的要点

  • 枚举被隐式声明为publicstaticfinal,这意味着你不能扩展它们。
  • 当您定义一个枚举时,它隐式地继承自java.lang.Enum。在内部,枚举被转换为类。此外,枚举常数是枚举类的实例,该常数被声明为该枚举类的成员。
  • 您可以对 enum 元素应用valueOf()name()方法来返回 enum 元素的名称。
  • 如果你在一个类中声明一个枚举,那么默认情况下它是静态的。
  • 不能对枚举数据类型使用 new 运算符,即使在枚举类内部也是如此。
  • 可以使用==运算符比较两个枚举是否相等。
  • 如果枚举常量来自两个不同的枚举,equals()方法不返回 true。
  • 当枚举常量的toString()方法被调用时,它打印枚举常量的名称。
  • 当在枚举类型上被调用时,Enum类中的静态values()方法返回枚举常数的数组。
  • 不能克隆枚举常数。试图这样做将导致CloneNotSupportedException

Enum 避免了幻数,提高了源代码的可读性和可理解性。此外,枚举是类型安全的构造。因此,只要需要一组相关的常数,就使用枚举。

接口

认证目标
开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释

接口是一组定义协议(即行为契约)的抽象方法。实现接口的类必须实现接口中指定的方法。接口定义了一个协议,实现接口的类遵循该协议。换句话说,一个接口通过定义一个抽象向它的客户承诺某些功能。所有实现接口的类都为承诺的功能提供了自己的实现。

声明和实现接口

现在是时候为形状对象实现自己的接口了。一些圆形物体(如CircleEllipse)可以滚动到给定的程度。您可以创建一个Rollable接口并声明一个名为roll()的方法:

interface Rollable {

void roll(float degree);

}

如您所见,您使用interface关键字定义了一个接口,该关键字声明了一个名为roll()的方法。该方法采用一个参数:滚动的degree。现在让我们在一个Circle类中实现这个接口(参见清单 3-8 )。

Listing 3-8. Circle.java

// Shape is the base class for all shape objects; shape objects that are associated with

// a parent shape object is remembered in the parentShape field

abstract class Shape {

abstract double area();

private Shape parentShape;

public void setParentShape(Shape shape) {

parentShape = shape;

}

public Shape getParentShape() {

return parentShape;

}

}

// Rollable interface can be implemented by circular shapes such as Circle

interface Rollable {

void roll(float degree);

}

abstract class CircularShape extends Shape implements Rollable { }

// Circle is a concrete class that is-a subtype of CircularShape;

// you can roll it and hence implements Rollable through CircularShape base class

public class Circle extends CircularShape {

private int xPos, yPos, radius;

public Circle(int x, int y, int r) {

xPos = x;

yPos = y;

radius = r;

}

public double area() { return Math.PI * radius * radius; }

@Override

public void roll(float degree) {

// implement rolling functionality here...

// for now, just print the rolling degree to console

System.out.printf("rolling circle by %f degrees", degree);

}

public static void main(String[] s) {

Circle circle = new Circle(10,10,20);

circle.roll(45);

}

}

在这种情况下,CircularShape实现了Rollable接口并扩展了Shape抽象类。现在像Circle这样的具体类可以扩展这个抽象类并定义roll()方法。本例中需要注意的几个要点是:

  • 抽象类CircularShape实现了Rollable接口,但不需要定义roll()方法。扩展了CircularShape的具体类Circle稍后定义了这个方法。
  • 您使用关键字implements来实现一个接口。注意,类定义中的方法名、参数和返回类型应该与接口中给出的完全匹配;如果它们不匹配,则认为该类没有实现该接口。
  • 或者,您可以使用@Override注释来指示一个方法正在从它的基类型中重写一个方法。在这种情况下,roll 方法在Circle类中被覆盖,并使用了@Override注释。

一个类也可以同时实现多个接口——直接或间接地通过它的基类。例如,Circle类也可以实现标准的Cloneable接口(用于创建Circle对象的副本)和Serializable接口(用于将对象存储在文件中以便以后重新创建对象,等等。),像这样:

class Circle extends CircularShape implements Cloneable, Serializable {

/* definition of methods such as clone here */

}
要记住的要点

以下是一些关于界面的要点,对你参加 OCPJP 八级考试有所帮助:

  • 无法实例化接口。对接口的引用可以引用实现它的任何派生类型的对象。
  • 一个接口可以扩展另一个接口。使用extends(而不是implements)关键字来扩展另一个接口。
  • 接口不能包含实例变量。如果你在一个接口中声明了一个数据成员,它应该被初始化,所有这样的数据成员都被隐式地当作“public static final”成员。
  • 一个接口可以有三种方法:抽象方法、默认方法和静态方法。
  • 接口可以用空体声明(即没有任何成员的接口)。例如,java.util定义了没有主体的接口EventListener
  • 一个接口可以在另一个接口或类中声明;这种接口被称为嵌套接口。
  • 与只能拥有publicdefault访问权的顶级接口不同,嵌套接口可以声明为publicprotectedprivate
  • 如果在抽象类中实现接口,抽象类不需要定义方法。但是,最终一个具体的类必须定义接口中声明的抽象方法。
  • 您可以对一个方法使用@Override注释来表明它正在从它的基类型中重写一个方法。

抽象类与接口

抽象类和接口有很多共同点。例如,两者都可以声明所有派生类都应该定义的方法。它们的相似之处还在于,你既不能创建抽象类的实例,也不能创建接口的实例。那么,抽象类和接口有什么区别呢?表 3-1 列出了一些重要的区别。

表 3-1。

Abstract Classes v.s Interfaces

 抽象类接口
使用的关键字使用abstractclass关键字定义一个类别。使用interface关键字定义一个接口。
实现类使用的关键字使用extends关键字从抽象类继承。使用implements关键字实现一个接口。
菲尔茨抽象类可以有静态和非静态字段。接口中不能有非静态字段(实例变量);默认情况下,所有字段都是 public static final(即下一项中讨论的常量)
常数抽象类可以有静态和非静态常量。接口只能有静态常量。如果声明一个字段,它必须被初始化。所有字段都被隐式地认为是public staticfinal
构造器您可以在抽象类中定义构造函数(例如,这对于初始化字段很有用)。不能在接口中声明/定义构造函数。
访问说明符抽象类中可以有私有和受保护的成员。接口中不能有任何私有或受保护的成员;默认情况下,所有成员都是公共的。
单一继承与多重继承一个类只能继承一个类(可以是抽象类,也可以是具体类)。一个类可以实现任意数量的接口。
目的抽象基类提供协议;此外,它还充当 is-a 关系中的基类。一个接口只提供一个协议。它指定了实现它的类必须提供的功能。
抽象、默认和静态方法

您看到的Rollable示例只有一个方法— roll()。然而,接口拥有多个方法是很常见的。例如,java.utilIterator接口定义如下:

public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {

throw new UnsupportedOperationException("remove");

}

default void forEachRemaining(Consumer<? super E> action) {

Objects.requireNonNull(action);

while (hasNext())

action.accept(next());

}

}

该接口用于遍历集合。(不用担心Iterator<E>里的<E>。它指的是元素类型,属于泛型,我们将在下一章详细介绍)。它声明了两个方法hasNext()next()——实现这个接口的类必须定义这两个方法。没有必要使用abstract关键字(但是如果你愿意,你可以提供abstract关键字),因为没有主体的方法被隐式地认为是抽象的。

该接口还有对remove()forEachRemaining()的方法定义。这些方法被称为默认方法,它们是用default关键字限定的。实现Iterator接口的类继承了这两个方法,并且可以选择覆盖它们。

接口也可以包含静态方法。例如,java.util.stream.Stream有静态方法builderemptyofiterategenerateconcat

在 Java 8 之前,接口只能声明方法(也就是说,它们只能提供抽象方法)。为了支持 lambda 函数,Java 8 对接口做了很大的改变:现在可以在接口中定义默认方法和静态方法。

默认方法

在接口中,默认方法是使用关键字default用方法体定义的方法。默认方法是实例方法。在默认方法中,this关键字是指声明接口。默认方法可以从包含它们的接口中调用方法。

Java 8 为什么要给接口添加默认方法?简答:为了支持 lambda 表达式(我们将在下一节讨论 lambdas)。默认方法使得接口的发展变得容易。怎么会?在 Java 8 之前,不能定义方法——只能声明它们。因此,如果您在现有的接口中添加一个新方法,这样的添加会破坏实现该接口的类,因为它们不会定义该方法。但是在 Java 8 中,使用默认方法,可以更容易地进化接口。

java.lang.Iterable接口为例。在 Java 8 之前,它只有一种方法:

Iterator<T> iterator();

在 Java 8 中,Iterable接口又扩展了两个方法:forEachspliterator方法。为了避免破坏实现该接口的类,这些方法被定义为默认方法。所以所有实现了Iterable接口的类(比如ArrayList类)现在也有了这两个方法。这里是Iterable接口的定义,没有文档注释。

public interface Iterable<T> {

Iterator<T> iterator();

default void forEach(Consumer<? super T> action) {

Objects.requireNonNull(action);

for (T t : this) {

action.accept(t);

}

}

default Spliterator<T> spliterator() {

return Spliterators.spliteratorUnknownSize(iterator(), 0);

}

}

在这个接口中添加forEachspliterator方法不会破坏实现Iterator接口的现有类,因为它们是默认方法。这样,默认方法有助于接口的发展。默认方法也简化了您的生活,因为现在可以在接口中提供具体的定义——所以您不需要覆盖它们。

现有库中的许多类(尤其是Collections)都是用 Java 8 中的默认方法添加的。例如,Java 中的List接口有这三个在 Java 8 中添加的方法:

default void    sort(Comparator<? super E> c)

default Spliterator<E>  spliterator()

default void    replaceAll(UnaryOperator<E> operator)
要记住的要点

以下是一些关于抽象、默认和静态方法的要点,对你参加 OCPJP 八级考试有帮助:

  • 您不能将成员声明为protectedprivate。只允许接口成员进行public访问。因为默认情况下所有方法都是公共的,所以可以省略public关键字。
  • 接口中声明的所有方法(即没有方法体)都被隐式地认为是抽象的。如果您愿意,可以为该方法显式使用abstract限定符。
  • 默认方法必须有方法体。默认方法必须使用关键字default进行限定。实现接口的类继承默认的方法定义,并且可以被重写。
  • 默认方法可以作为抽象方法在派生类中重写;对于这种覆盖,也可以使用@Override注释。
  • 您不能将默认方法限定为synchronizedfinal
  • 静态方法必须有一个方法体,并且使用static关键字对它们进行限定。
  • 您不能为静态方法提供abstract关键字:请记住,您不能在派生类中重写静态方法,所以从概念上讲,通过不提供方法体来使静态方法保持抽象是不可能的。
  • 不能对静态方法使用default关键字,因为所有默认方法都是实例方法。
钻石问题

在 Java 中,一个接口或类可以扩展多个接口。例如,这里有一个来自java.nio.channels包的类层次结构(图 3-2 )。基础接口是Channel。两个接口,ReadableByteChannelWriteableByteChannel,扩展了这个基本接口。最后,ByteChannel接口扩展了ReadableByteChannelWriteableByteChannel.注意继承层次的结果形状看起来像一个“钻石”

A978-1-4842-1836-5_3_Fig2_HTML.jpg

图 3-2。

Diamond hierarchy in java.nio.channels package

在这种情况下,基本接口Channel没有任何方法。ReadableByteChannel接口声明了read方法,WriteableByteChannel接口声明了write方法;ByteChannel接口从这些基本类型中继承了readwrite方法。由于这两种方法是不同的,我们没有冲突,因此这个层次结构是好的。

但是如果我们在基类型中有两个具有相同签名的方法定义会怎么样呢?ByteChannel接口会继承哪个方法?当这个问题发生时,它被称为“钻石问题”

在我们讨论处理菱形问题的工作示例之前,让我们首先清楚地了解一下菱形问题在 Java 中是何时以及如何发生的。

  • 在 Java 中,你不能扩展多个类;因此,因为扩展了两个基类,所以不会出现菱形问题。然而,当抽象类和接口定义具有相同签名的方法时,在派生类中可能会出现菱形问题。
  • 当两个基本接口具有相同签名的抽象方法时,这并不会真正导致“钻石问题”,因为它们是方法声明而不是定义(如 Java 8 之前的情况)。
  • 接口只能定义方法而不能定义字段(它们只能包含常量)。因此,对于界面中的字段,不会出现菱形问题;它只出现在方法定义中。

幸运的是,当派生类型从不同的基类型继承同名的方法定义时,可以使用规则来解析方法。让我们在这里讨论两个重要的场景。

场景 1:如果两个超接口用相同的签名定义方法,编译器会发出错误。我们必须手动解决冲突(清单 3-9 )。

Listing 3-9. Diamond.java

interface Interface1 {

default public void foo() { System.out.println("Interface1's foo"); }

}

interface Interface2 {

default public void foo() { System.out.println("Interface2's foo"); }

}

public class Diamond implements Interface1, Interface2 {

public static void main(String []args) {

new Diamond().foo();

}

}

Error:(9, 8) java: class Diamond inherits unrelated defaults for foo() from types Interface1 and Interface2

在这种情况下,通过在Diamond类中使用 super 关键字来显式地指出要使用哪个方法定义,从而手动解决冲突:

public void foo() { Interface1.super.foo(); }

将该方法定义添加到 Diamond 类中并执行后,该程序将打印:

Interface1's foo

场景 2:如果基类和基接口用相同的签名定义方法,则使用类中的方法定义,忽略接口定义(清单 3-10 )。

Listing 3-10. Diamond.java

class BaseClass {

public void foo() { System.out.println("BaseClass's foo"); }

}

interface BaseInterface {

default public void foo() { System.out.println("BaseInterface's foo"); }

}

public class Diamond extends BaseClass implements BaseInterface {

public static void main(String []args) {

new Diamond().foo();

}

}

这种情况下没有编译器错误:编译器解析类中的定义,接口定义被忽略。这个程序打印“Base foo”。这可以被认为是“阶级胜利”法则。该规则有助于保持与 Java 8 之前版本的兼容性。怎么做?当一个新的默认方法被添加到一个接口中时,它可能碰巧与基类中定义的方法具有相同的签名。通过“类获胜”规则解决冲突,基类中的方法将总是被选择。

功能界面

Java 库中有许多接口声明了一个抽象方法;一些这样的接口是:

// in java.lang package

interface Runnable { void run(); }

// in java.util package

interface Comparator<T> { boolean compare(T x, T y); }

// java.awt.event package:

interface ActionListener { void actionPerformed(ActionEvent e); }

// java.io package

interface FileFilter { boolean accept(File pathName); }

Java 8 引入了“函数式接口”的概念,将这一思想形式化。一个函数式接口只指定一个抽象方法。因为函数式接口只指定了一个抽象方法,所以它们有时被称为单一抽象方法(SAM)类型或接口。

注意:函数式接口可以接受通用参数,如上面例子中的Comparator<T>Callable<T>接口。我们将在下一章介绍泛型(第四章)。

A978-1-4842-1836-5_3_Figbb_HTML.gif一个函数式接口的声明产生了一个可以和 lambda 表达式一起使用的“函数式接口类型”。此外,在 Java 8 中引入的java.util.functionjava.util.stream包中广泛使用了函数式接口。鉴于这个主题的重要性,你可以预期在你的 OCPJP 8 考试中会有许多与功能接口相关的问题。

对于被视为函数式接口的接口,它应该只有一个抽象方法。但是,它可能定义了任意数量的默认或静态方法。让我们看几个 Java 库中的例子来理解这一点。

下面是java.util.function.IntConsumer接口的定义(不带注释和 javadoc 注释):

public interface IntConsumer {

void accept(int value);

default IntConsumer andThen(IntConsumer after) {

Objects.requireNonNull(after);

return (int t) -> { accept(t); after.accept(t); };

}

}

虽然这个接口有两个成员,但是andThen方法是默认方法,只有accept方法是抽象方法。因此,IntConsumer界面是一个功能性界面。

再举一个例子,java.util.function.Predicate是一个函数式接口,因为它只有一个抽象方法:

boolean test(T t)

但是需要注意的是Predicate也有以下默认的方法定义:

default Predicate<T> and(Predicate<? super T> other)

default Predicate<T> negate()

default Predicate<T> or(Predicate<? super T> other)

此外,它还定义了一个静态方法isEqual:

static <T> Predicate<T> isEqual(Object targetRef)

给定所有这些方法定义,Predicate仍然是一个函数式接口,因为它只有一个抽象方法test

@FunctionalInterface 批注

Java 编译器推断任何具有单一抽象方法的接口都是函数式接口。但是,您可以用@FunctionalInterface注释标记函数式接口来确认这一点。推荐的做法是为函数式接口提供@FunctionalInterface,因为有了这个注释,编译器可以给出更好的错误/警告。

这里有一个使用@FunctionalInterface的例子,它有一个抽象方法,所以它可以干净地编译:

@FunctionalInterface

public abstract class AnnotationTest {

abstract int foo();

}

这个怎么样?

@FunctionalInterface

public interface AnnotationTest {

default int foo() {};

}

它会导致编译器错误“在接口中没有找到抽象方法”,因为它只提供了一个默认方法,而没有任何抽象方法。这个怎么样?

@FunctionalInterface

public interface AnnotationTest { /* no methods provided */ }

此接口没有任何方法。因为它缺少一个抽象方法,但是用@FunctionalInterface进行了注释,所以会导致编译器错误。

这是另一种变化:

@FunctionalInterface

public interface AnnotationTest {

int foo();

int bar();

}

这段代码还会导致编译器错误“发现多个非重写抽象方法”,因为当一个函数式接口要求恰好提供一个抽象方法时,它有多个抽象方法。

Methods from Object Class in Functional Interfaces

根据 Java 语言规范(8.0 版),“接口不从 Object 继承,而是隐式声明许多与 Object 相同的方法。”如果你在接口中提供一个来自Object类的抽象方法,它仍然是一个功能接口。

例如,考虑声明两个抽象方法的Comparator接口:

@FunctionalInterface

public interface Comparator<T> {

int compare(T o1, T o2);

boolean equals(Object obj);

// other methods are default methods or static methods and are elided

}

这个接口是一个函数式接口,尽管它声明了两个抽象方法:compare()equals()方法。当它有两个抽象方法时,它是一个怎样的函数式接口?因为equals()方法签名与Object匹配,而compare()方法是唯一剩下的抽象方法,因此Comparator接口是一个函数式接口。

这个接口定义怎么样?

@FunctionalInterface

interface EqualsInterface {

boolean equals(Object obj);

}

编译器给出错误:“EqualsInterface is not a functional interface: no abstract method found in interface EqualsInterface”。为什么呢?因为方法equals来自Object,所以它不被认为是一个函数式接口。

要记住的要点

以下是一些关于功能接口的要点,对你参加 OCPJP 八级考试有帮助:

  • @FunctionalInterface标注功能接口。否则,如果函数式接口不正确(例如,它有两个抽象方法),编译器将不会发出任何错误。
  • 您只能对接口使用@FunctionalInterface注释,而不能对类、枚举等使用。
  • 如果派生接口只有一个抽象方法或者只继承一个抽象方法,那么它可以是函数式接口。
  • 对于函数式接口,在接口中声明来自Object类的方法不算抽象方法。

λ函数

认证目标
创建和使用 Lambda 表达式

Java 8 中主要的新语言特性之一是 lambda 函数。事实上,这是自 Java 1 发布以来最大的变化之一。Lambdas 广泛用于编程语言世界,包括编译到 Java 平台的语言。例如,Groovy 语言可以编译到 Java 平台,并且对 lambda 函数(也称为闭包)有很好的支持。Oracle 决定通过 Java 8 将 lambdas 引入 JVM 上的主流语言——Java 语言本身。

Lambda Function Related Changes in Java 8

lambdas 的引入需要语言、库和 VM 实现的协调变化:

  • 用于定义 lambda 函数的箭头操作符(“-->”),用于方法引用的双冒号操作符(“::”),以及关键字default
  • streams 库以及收藏库与 streams 的集成
  • Lambda 函数是使用 Java 7 中引入的invokedynamic指令实现的

为了支持在语言中引入 lambdas,Java 8 中的类型推理也得到了加强。Lambdas 使库作者能够在库中创建并行算法,以利用现代硬件(即多核)中固有的并行性。

在 Java 8 中,java.util已经通过使用 lambda 函数得到了很大的增强,我们将在下一章讨论这一点(第四章)。Java 8 增加了两个新的包java.util.functionjava.util.streams。我们将在第五章的中的java.util.function和第六章的中的java.util.streams(称为流 API)中讨论类型。

Lambdas 可以极大地改变你设计和编写代码的方式。为什么呢?lambda 支持函数式编程范式——这意味着学习和使用 lambda 将意味着范式的转变。但是你不需要担心做出重大的改变——Java 无缝地将功能性与现有的面向对象特性集成在一起,你可以逐渐地在你的程序中使用越来越多的功能性特性。

在函数式编程范式中,lambda 函数可以存储在变量中,作为参数传递给其他函数,或者从其他函数返回,就像原始类型和引用变量一样。因为“lambda 函数”是可以传递的代码片段,所以可以认为函数范式支持“代码作为数据”传递“可执行代码段”的能力增强了 Java 的表达能力。

Lambda 函数:语法

lambda 函数由可选参数、箭头标记和主体组成:

LambdaParameters -> LambdaBody
  • LambdaParameterslambda 函数的参数在左括号“(”和右括号“)”内传递。当传递多个参数时,它们用逗号分隔。
  • 箭头运算符。为了支持 lambdas,Java 引入了新的运算符“->”,也称为 lambda 运算符或 arrow 运算符。这个箭头操作符是必需的,因为我们需要从语法上将参数从主体中分离出来。
  • LambdaBody可以是表达式,也可以是块。主体可以由单个语句组成(在这种情况下,不需要定义块的显式花括号);这样的λ体被称为“表达式λ”如果一个 lambda 体中有很多语句,它们需要在一个代码块中;这种λ体被称为“块λ”

编译器对 lambda 表达式执行类型推断:

  • 如果没有在 lambda 函数定义中指定类型参数,编译器会推断参数的类型。指定参数类型时,需要指定全部或不指定;否则你会得到一个编译错误。
  • 如果只有一个参数,可以省略括号。但是在这种情况下,您不能显式提供类型。您应该让编译器来推断单个参数的类型。
  • lambda 函数的返回类型是从主体中推断出来的。如果 lambda 中的任何代码返回值,那么所有路径都应该返回值;否则你会得到一个编译错误。

有效 lambda 表达式的一些示例(假设相关的函数式接口可用):

  • (int x) -> x + x
  • x -> x % x
  • () -> 7
  • (int arg1, int arg2) -> (arg1 + arg2) / (arg1 – arg2)

无效 lambda 表达式的示例:

  • -> 7 // if no parameters, then empty parenthesis () must be provided
  • (arg1, int arg2) -> arg1 / arg2``// if argument types are provided, then it should be should be  provided
λ函数—一个例子

让我们从一个简单的 lambda 函数的“hello world”示例开始(清单 3-11 )。

Listing 3-11. FirstLambda.java

interface LambdaFunction {

void call();

}

class FirstLambda {

public static void main(String []args) {

LambdaFunction lambdaFunction = () -> System.out.println("Hello world");

lambdaFunction.call();

}

}

执行时,该程序打印

Hello world

在这个程序中,接口LambdaFunction声明了一个名为call()的抽象方法;因此,它是一个功能接口。在FirstLambda类的main方法中,一个 lambda 函数被分配给函数式接口类型LambdaFunction的一个变量。

LambdaFunction lambdaFunction = () -> System.out.println("Hello world");

这里,表达式() -> System.out.println("Hello world")是一个λ表达式:

  • 语法()表示没有参数。
  • 箭头操作符"->"将方法参数与 lambda 主体分开。
  • 语句System.out.println("Hello world")是 lambda 表达式的主体。

lambda 表达式和函数式接口LambdaFunction有什么关系?它是通过LambdaFunction接口内的单一抽象方法:void call()。此抽象方法的签名和 lambda 表达式必须匹配:

  • lambda 表达式有()表示它没有参数——它与不带参数的call方法相匹配。
  • 语句System.out.println("Hello world")是 lambda 表达式的主体。这个主体充当 lambda 函数的实现。
  • 在这个 lambda 表达式体中没有 return 语句,因此编译器将这个表达式的返回类型推断为void类型,这与call方法的返回类型相匹配。

下一条语句lambdaFunction.call();调用 lambda 函数。此函数调用的结果是,控制台上会显示“Hello world”。

为什么 lambda 函数的函数类型要与给定函数式接口中抽象方法的函数类型相匹配?它用于类型检查。如果类型不匹配,您将得到一个编译器错误,如下所示:

LambdaFunction lambdaFunction = (``int i

因为这个 lambda 表达式有一个整数参数,但是LambdaFunction中的call()方法不接受任何参数,所以编译器给出一个错误:“incompatible types: incompatible parameter types in lambda expression"”。

兰姆达斯街区

块 lambda 包含在“{”和“}”内的代码块中,如:

LambdaFunction lambdaFunction = ( int i ) -> { System.out.println("Hello world");

当您想要在 lambda 主体中提供多个语句时,块 lambda 非常有用(在 lambda 表达式中,您只能有一个语句)。此外,在 block lambda 中,您可以提供一个显式的return语句(参见清单 3-12 )。

Listing 3-12. BlockLambda.java

class BlockLambda {

interface LambdaFunction {

String intKind(int a);

}

public static void main(String []args) {

LambdaFunction lambdaFunction =

(int i) -> {

if((i % 2)  == 0) return "even";

else return "odd";

};

System.out.println(lambdaFunction.intKind(10));

}

}

它打印:

even

在这段代码中,我们定义了一个块 lambda。我们从这个 lambda 块返回一个String,并且使用了显式的返回语句。在定义 block lambdas 时,我们应该确保为所有路径都提供了 return 语句,就像在这种情况下一样(否则,您会得到一个编译器错误)。返回语句应该与对应函数式接口中定义的抽象方法的返回类型相匹配(这里是LambdaFunction接口中intKind函数的String返回类型)。

匿名内部类与 Lambda 表达式

在 Java 8 之前,作为 Java 程序员,我们习惯于编写匿名内部类。清单 3-13 等同于之前的程序(清单 3-11 ,除了它使用匿名内部类代替 lambda 函数。

Listing 3-13. AnonymousInnerClass.java

interface Function {

void call();

}

class AnonymousInnerClass {

public static void main(String []args) {

Function function = new Function() {

public void call() {

System.out.println("Hello world");

}

};

function.call();

}

}

清单 3-11 和清单 3-12 中的功能是相同的,但是使用匿名内部类会导致冗长的代码,而 lambda 表达式是简洁的。

A978-1-4842-1836-5_3_Figbb_HTML.gif思考 lambdas 的一种方式是“匿名函数”或“未命名函数”:它们是没有名称的函数,不与任何类相关联。具体来说,它们不是定义它们的类的静态或实例成员。如果在 lambda 函数中使用this关键字,它指的是定义 lambda 的作用域中的对象。

有效最终变量

Lambda 函数可以引用封闭范围内的局部变量。该变量需要显式声明为 final,否则该变量将被视为有效的 final。Effectively final 意味着编译器将变量视为最终变量,如果我们试图在 lambda 函数或函数的其余部分中修改它,将会发出一个错误。lambdas 的这种行为类似于从本地和匿名类访问外部作用域中的变量。他们访问的变量实际上也被认为是最终变量(正如我们前面讨论的)。

这里有一个例子。你听说过“猪拉丁”吗?这是孩子们玩的一种游戏,通过改变单词或添加后缀来创造单词,以创造奇怪的发音。在这个例子中,让我们简单地给一个单词加上后缀“ay”(清单 3-14 )。

Listing 3-14. PigLatin.java

interface SuffixFunction {

void call();

}

class PigLatin {

public static void main(String []args) {

String word = "hello";

SuffixFunction suffixFunc = () -> System.out.println(word + "ay");

suffixFunc.call();

}

}

该程序打印:

helloay

在 lambda 表达式中,我们使用了局部变量word。因为在 lambda 表达式中使用,这个变量被认为是final(尽管它没有被明确声明为final)。尝试这段代码,它有一个附加语句分配给suffix(甚至在调用 lambda 函数之前):

String word = "hello";

SuffixFunction suffixFunc = () -> System.out.println(word + "ay");

word = "e";

suffixFunc.call();

编译器对这段代码发出一个错误:

PigLatin.java:11: error: local variables referenced from a lambda expression must be final or effectively final

SuffixFunction suffixFunc = () -> System.out.println(word + "ay");

^

1 error

这是因为后缀在“e”被初始化后就被赋给了它,因此编译器不能把它当作最终变量。

为什么局部变量在 lambda 表达式中被访问时被认为是 final?原因是这种变异不是线程安全的。

请注意,这一限制不适用于数据成员和类成员。因此,当多个线程同时修改 lambda 表达式中的变量时,您可能会面临风险。此外,effectively final 仅适用于引用,而不适用于引用所指向的值。因此,您可以从 lambda 函数改变局部数组中的值——这不安全,但却是可能的。

要记住的要点

以下是关于 lambda 函数的一些要点,对你参加 OCPJP 8 考试有帮助:

  • Lambda 表达式只能出现在可以进行赋值、函数调用或强制转换的上下文中。
  • lambda 函数被视为嵌套块。因此,就像嵌套块一样,我们不能在封闭块中声明与局部变量同名的变量。
  • Lambda 函数必须从所有分支返回值——否则会导致编译器错误。
  • 当声明参数类型时,lambda 被称为“显式类型化”;如果它们是推断出来的,那么它就是“隐式类型化的”
  • 如果 lambda 表达式抛出异常会发生什么?如果它是一个检查过的异常,那么函数式接口中的方法应该声明;否则会导致编译错误。

摘要

让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。

开发使用抽象类和方法的代码

  • 指定支持的功能的抽象,但不公开更精细的细节。
  • 您不能创建抽象类的实例。
  • 抽象类支持运行时多态性,而运行时多态性又支持松散耦合。

开发使用 final 关键字的代码

  • final 类是不可继承的类(即,您不能从 final 类继承)。
  • final 方法是不可重写的方法(即子类不能重写 final 方法)。
  • final 类的所有方法都是隐式 final 的(即不可重写)。
  • final 变量只能赋值一次。

创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类

  • Java 支持四种类型的嵌套类:静态嵌套类、内部类、局部内部类和匿名内部类。
  • 静态嵌套类可能有静态成员,而其他类型的嵌套类则没有。
  • 静态嵌套类和内部类可以访问外部类的成员(甚至私有成员)。但是,静态嵌套类只能访问外部类的静态成员。
  • 局部类(局部内部类和匿名内部类)可以访问外部作用域中声明的所有变量(无论是方法、构造函数还是语句块)。

使用枚举类型,包括枚举类型中的方法和构造函数

  • 枚举是实现用户受限输入的一种类型安全的方法。
  • 即使在枚举定义内,也不能对枚举使用 new。
  • 默认情况下,枚举类是最终类。
  • 所有枚举类都是从java.lang.Enum隐式派生的。

开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释

  • 一个接口可以有三种方法:抽象方法、默认方法和静态方法。
  • 当派生类型继承了基类型中具有相同签名的两个方法定义时,就会出现“菱形问题”。
    • 如果两个超接口有相同的方法名,其中一个有定义,编译器会发出错误;这种冲突必须手动解决。
    • 如果基类和基接口用相同的签名定义方法,则使用类中的方法定义,忽略接口定义。
  • 一个函数式接口只包含一个抽象方法,但是可以包含任意数量的默认或静态方法。
  • 函数式接口的声明会产生一个“函数式接口类型”,它可以与 lambda 表达式一起使用。
  • 对于函数式接口,在接口中声明来自Object类的方法不算抽象方法。

创建和使用 Lambda 表达式

  • 在 lambda 表达式中,左边的->提供参数;右边,身体。箭头运算符(“-->”)有助于简化 lambda 函数的表达式。
  • 您可以创建对函数式接口的引用,并为其分配 lambda 表达式。如果从该接口调用抽象方法,它将调用指定的 lambda 表达式。
  • 如果省略,编译器可以执行 lambda 参数的类型推断。声明时,参数可以有修饰符,如final
  • lambda 函数访问的变量实际上被认为是最终变量。

Question TimeWhich ONE of the following statements is TRUE? You cannot extend a concrete class and declare that derived class abstract   You cannot extend an abstract class from another abstract class   An abstract class must declare at least one abstract method in it   You can create an instance of a concrete subclass of an abstract class but cannot create an instance of an abstract class itself     Choose the correct answer based on the following class definition: public abstract final class Shape { } Compiler error: a class must not be empty   Compiler error: illegal combination of modifiers abstract and final   Compiler error: an abstract class must declare at least one abstract method   No compiler error: this class definition is fine and will compile successfully     Choose the best option based on this program: class Shape {     public Shape() {          System.out.println("Shape constructor");      }      public class Color {          public Color() {              System.out.println("Color constructor");          }      } } class TestColor {      public static void main(String []args) {         Shape.Color black = new Shape().Color(); // #1      } } Compiler error: the method Color() is undefined for the type Shape   Compiler error: invalid inner class   Works fine: Shape constructor, Color constructor   Works fine: Color constructor, Shape constructor     Choose the best option based on this program: class Shape {     private boolean isDisplayed;     protected int canvasID;     public Shape() {          isDisplayed = false;          canvasID = 0;      }      public class Color {          public void display() {              System.out.println("isDisplayed: "+isDisplayed);              System.out.println("canvasID: "+canvasID);          }      } } class TestColor {      public static void main(String []args) {          Shape.Color black = new Shape().new Color();          black.display();      } } Compiler error: an inner class can only access public members of the outer class   Compiler error: an inner class cannot access private members of the outer class   Runs and prints this output:   isDisplayed: false   canvasID: 0   Compiles fine but crashes with a runtime exception     Determine the behavior of this program: public class EnumTest {     PrinterType printerType;     enum PrinterType {INKJET, DOTMATRIX, LASER};     public EnumTest(PrinterType pType) {         printerType = pType;     }     public static void main(String[] args) {         PrinterType pType = new PrinterType();         EnumTest enumTest = new EnumTest(PrinterType.LASER);     } } Prints the output printerType:LASER   Compiler error: enums must be declared static   Compiler error: cannot instantiate the type EnumTest.PrinterType   This program will compile fine, and when run, will crash and throw a runtime exception     Is the enum definition given below correct? public enum PrinterType {     private int pagePrintCapacity;          // #1     DOTMATRIX(5), INKJET(10), LASER(50);    // #2     private PrinterType(int pagePrintCapacity) {         this.pagePrintCapacity = pagePrintCapacity;     }     public int getPrintPageCapacity() {         return pagePrintCapacity;     } } Yes, this enum definition is correct and will compile cleanly without any warnings or errors   No, this enum definition is incorrect and will result in compile error(s)   No, this enum definition will result in runtime exception(s) Yes, this enum definition is correct but will compile with warnings.     Determine the behavior of this program: interface DoNothing {     default void doNothing() { System.out.println("doNothing"); } } @FunctionalInterface interface DontDoAnything extends DoNothing {     @Override     abstract void doNothing(); } class LambdaTest {     public static void main(String []args) {         DontDoAnything beIdle = () -> System.out.println("be idle");         beIdle.doNothing();     } } This program results in a compiler error for DontDoAnything interface: cannot override default method to be an abstract method   This program results in a compiler error: DontDoAnything is not a functional interface   This program prints: doNothing   This program prints: be idle     Determine the behavior of this program: public class EnumTest {     public EnumTest() {         System.out.println("In EnumTest constructor ");     }     public void printType() {         enum PrinterType { DOTMATRIX, INKJET, LASER }     } } This code will compile cleanly without any compiler warnings or errors, and when used, will run without any problems   This code will compile cleanly without any compiler warnings or errors, and when used, will generate a runtime exception   This code will produce a compiler error: enum types must not be local   This code will give compile-time warnings but not any compiler errors     Determine the behavior of this program: interface BaseInterface {     default void foo() { System.out.println("BaseInterface's foo"); } } interface DerivedInterface extends BaseInterface {     default void foo() { System.out.println("DerivedInterface's foo"); } } interface AnotherInterface {     public static void foo() { System.out.println("AnotherInterface's foo"); } } public class MultipleInheritance implements DerivedInterface, AnotherInterface {     public static void main(String []args) {         new MultipleInheritance().foo();     } } This program will result in a compiler error: Redundant method definition for function foo   This program will result in a compiler error in MultipleInheritance class: Ambiguous call to function foo   The program prints: DerivedInterface’s foo   The program prints: AnotherInterface's foo     Determine the behavior of this program: class LambdaFunctionTest {     @FunctionalInterface     interface LambdaFunction {         int apply(int j);         boolean equals(java.lang.Object arg0);     }     public static void main(String []args) {         LambdaFunction lambdaFunction = i -> i * i;    // #1         System.out.println(lambdaFunction.apply(10));     } } This program results in a compiler error: interfaces cannot be defined inside classes   This program results in a compiler error: @FunctionalInterface used for LambdaFunction that defines two abstract methods   This program results in a compiler error in code marked with #1: syntax error   This program compiles without errors, and when run, it prints 100 in console    

答案:

D. You can create an instance of a concrete subclass of an abstract class but cannot create an instance of an abstract class itself   B. Compiler error: illegal combination of modifiers abstract and final You cannot declare an abstract class final since an abstract class must to be extended. Class can be empty in Java, including abstract classes. An abstract class can declare zero or more abstract methods.   A. Compiler error: The method Color() is undefined for the type Shape You need to create an instance of outer class Shape in order to create an inner class instance, as in new Shape().new Color();.   C. Runs and prints this output: isDisplayed: false canvasID: 0 An inner class can access all members of an outer class, including the private members of the outer class.   C. Compiler error: cannot instantiate the type EnumTest.PrinterType You cannot instantiate an enum type using new.   B. No, this enum definition is incorrect and will result in compile error(s) You need to define enum elements first before any other attribute in an enum class. In other words, this enum definition will compile cleanly if you interchange the statements marked with “#1” and “#2” within comments in this code.   D. This program prints: be idle A default method can be overridden in a derived interface and can be made abstract. DoNothing is a functional interface because it has an abstract method. The call beIdle.doNothing() calls the System.out.println given inside the lambda expression and hence it prints “be idle” on the console.   C. It will produce a compiler error: enum types must not be local An enum can only be defined inside of a top-level class or interface and not within a method.   C. The program prints: DerivedInterface’s foo A default method can be overridden. Since DerivedInterface extends BaseInterface, the default method definition for foo is overridden in the DerivedInterface. Static methods do not cause conflicting definition of foo since they are not overridden, and they are accessed using the interface name, as in AnotherInterface.foo. Hence, the program compiles without errors. In the main method within MultipleInheritance class, the overridden foo method is invoked and hence the call resolves to foo method defined in DerivedInterface.   D. This program compiles without errors, and when run, it prints 100 in console An interface can be defined inside a class. The signature of the equals method matches that of the equal method in Object class; hence it is not counted as an abstract method in the functional interface. It is acceptable to omit the parameter type when there is only one parameter and the parameter and return type are inferred from the LambdaFunction abstract method declaration int apply(int j). Since the lambda function is passed with the value 10, the returned value is 10 * 10, and hence 100 is printed in console.

四、泛型和集合

认证目标
创建和使用泛型类
创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象
使用 java.util.Comparator 和 java.lang.Comparable 接口
集合流和过滤器
使用流和列表的 forEach 方法进行迭代
描述流接口和流管道
使用 lambda 表达式筛选集合
对流使用方法引用

每个重要的 Java 应用都使用数据结构和算法。Java 集合的框架提供了大量易于使用的通用数据结构和算法。这些数据结构和算法可以以类型安全的方式用于任何合适的数据类型;这是通过使用一种称为泛型的语言特性来实现的。

Java 中的集合实现数据结构,算法使用泛型和 lambda 函数实现。因此,这些主题在 1Z0-809 考试大纲中被合并为一个主题。在这一章中,我们从讨论泛型开始。因为我们的经验表明,正确回答关于泛型的问题通常很棘手,所以我们将详细讨论泛型。接下来我们讨论重要的集合,也讨论java.lang.Comparatorjava.lang.Comparable接口。最后,我们将详细介绍如何在 Java 集合框架中使用 lambda 函数和流。你可能会在 OCPJP 8 考试中遇到许多关于泛型、集合和流的问题,所以本章提供了考试主题的详细内容。

创建和使用泛型类

认证目标
创建和使用泛型类

泛型是 Java 1.5 版中引入的一种语言特性。在 Java 中引入泛型之前,Object基类被用作泛型的替代。使用泛型,你为一种类型(比如说T)编写适用于所有类型的代码,而不是为每种类型编写单独的类。让我们从一个简单的例子开始。

假设您想要打印方括号内的对象值。例如,要打印一个值为 10 的Integer对象,而不是将“10”打印到控制台,您希望在一个“框”内打印该值,如下所示:“[10]”。清单 4-1 包含了一个通用版本的BoxPrinter类。

Listing 4-1. BoxPrinterTest.java

// This program shows container implementation using generics

class BoxPrinter<T> {

private T val;

public BoxPrinter(T arg) {

val = arg;

}

public String toString() {

return "[" + val + "]";

}

}

class BoxPrinterTest {

public static void main(String []args) {

BoxPrinter<Integer> value1 = new BoxPrinter<Integer>(new Integer(10));

System.out.println(value1);

BoxPrinter<String> value2 = new BoxPrinter<String>("Hello world");

System.out.println(value2);

}

}

它打印以下内容:

[10]

[Hello world]

这里有很多需要注意的地方。

See the declaration of BoxPrinter: class BoxPrinter<T> You gave the BoxPrinter class a type placeholder <T>—the type name T within angle brackets “” following the class name. You can use this type name inside the class to indicate that it is a placeholder for the actual type to be provided later.   Inside the class you first use T in field declaration: private T val; You are declaring val of the generic type—the actual type will be specified later when you use BoxPrinter. In main(), you declare a variable of type BoxPrinter for an Integer like this: BoxPrinter<Integer> value1 Here, you are specifying that T is of type Integer—identifier T (a placeholder) is replaced with the type Integer. So, the val inside BoxPrinter becomes Integer because T gets replaced with Integer.   Now, here is another place where you use T: public BoxPrinter(T arg) {         val = arg; }  

类似于类型为Tval的声明,您是说BoxPrinter构造函数的参数属于类型T。稍后在main()方法中,当在new中调用构造函数时,指定T的类型为Integer:

new BoxPrinter<Integer>(new Integer(10));

现在,在BoxPrinter构造函数中,argval应该属于同一类型,因为它们都属于类型T。例如,如果您按如下方式更改构造函数:

new BoxPrinter<String>(new Integer(10));

BoxPrinter的类型是String,传递的参数的类型是Integer,所以在使用泛型时,您会得到一个类型不匹配的编译器错误(这很好,因为您会更早发现问题)。

让我们考虑另一个例子。这里有一个Pair泛型类,可以保存两种不同类型的对象,T1T2(清单 4-2 )。

Listing 4-2. PairTest.java

// It demonstrates the usage of generics in defining classes

class Pair<T1, T2> {

T1 object1;

T2 object2;

Pair(T1 one, T2 two) {

object1 = one;

object2 = two;

}

public T1 getFirst() {

return object1;

}

public T2 getSecond() {

return object2;

}

}

class PairTest {

public static void main(String []args) {

Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");

System.out.println("World cup " +  worldCup.getFirst() +

" in " + worldCup.getSecond());

}

}

该程序打印以下内容:

World cup 2018 in Russia

这里的T1T2是类型持有者。您可以在尖括号内给出这些类型占位符:<T1, T2>。当使用Pair类时,您必须指定您将使用哪些特定类型来代替T1T2。例如,您使用IntegerString来表示Pair,就像在main()方法中的Pair<Integer, String>一样。现在,假设Pair类有这样的主体:

// how Pair<Integer, String> can be treated internally

class Pair {

Integer object1;

String object2;

Pair(Integer one, String two) {

object1 = one;

object2 = two;

}

public Integer getFirst() {

return object1;

}

public String getSecond() {

return object2;

}

}

换句话说,尝试手动查找并替换类型占位符,并用代码中的实际类型替换它们。这将帮助您理解泛型实际上是如何工作的。这样,您就可以理解getFirst()getSecond()方法如何在main()方法中返回IntegerString值。

在声明中

Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");

请注意,类型完全匹配。如果你尝试

Pair<Integer, String> worldCup = new Pair<String, String>(2018, "Russia");

您将得到以下编译器错误:

TestPair.java:20: cannot find symbol

symbol  : constructor Pair(int,java.lang.String)

location: class Pair<java.lang.String,java.lang.String>

现在,试试这个说法怎么样?

Pair<Integer, String> worldCup = new Pair<Number, String>(2018, "Russia");

您将得到另一个编译器错误,因为声明的类型worldCup与初始化表达式中给定的类型不匹配:

TestPair.java:20: incompatible types

found   : Pair<java.lang.Number,java.lang.String>

required: Pair<java.lang.Integer,java.lang.String>

现在修改通用的Pair类。Pair<T1, T2>存储类型为T1T2的对象。采用类型T并存储该类型的两个对象T的通用 pair 类怎么样?显然,一种方法是用相同的类型实例化Pair<T1, T2>,比如说Pair<String, String>,但这不是一个好的解决方案。为什么呢?没有办法确保用相同的类型实例化Pair!清单 4-3 是Pair的修改版——姑且称之为PairOfT——它采用了一个类型占位符T

Listing 4-3. PairOfT.java

// This program shows how to use generics in your programs

class PairOfT<T> {

T object1;

T object2;

PairOfT(T one, T two) {

object1 = one;

object2 = two;

}

public T getFirst() {

return object1;

}

public T getSecond() {

return object2;

}

}

现在,这种说法行得通吗?

PairOfT<Integer, String> worldCup = new PairOfT<Integer, String>(2018, "Russia");

不,因为PairOfT有一个类型参数,而你在这里给了两个类型参数。所以,你会得到一个编译错误。那么,这个说法怎么样?

PairOfT<String> worldCup = new PairOfT<String>(2018, "Russia");

不,您仍然会得到一个编译器错误:

TestPair.java:20: cannot find symbol

symbol  : constructor PairOfT(int,java.lang.String)

location: class PairOfT<java.lang.String>

PairOfT<String> worldCup = new PairOfT<String>(2018, "Russia");

原因是 2018 年——当被装箱时——是一个Integer,你应该给出一个String作为自变量。这个说法怎么样?

PairOfT<String> worldCup = new PairOfT<String>("2018", "Russia");

是的,它可以编译并且运行良好。

菱形语法

在上一节中,我们讨论了如何创建泛型类型实例,如以下语句所示:

Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");

我们还讨论了如果这些类型不匹配,编译器会如何出错,如下面的语句所示,它不会编译:

Pair<Integer, String> worldCup = new Pair<String, String>(2018, "Russia");

看看确保在声明类型(本例中为Pair<Integer, String>)和新对象创建表达式(本例中为new Pair<String, String>())中提供相同类型的参数有多繁琐?

为了简化您的生活,Java 1.7 引入了 diamond 语法,其中可以省略类型参数:您可以让编译器从类型声明中推断类型。因此,声明可以简化为

Pair<Integer, String> worldCup = new Pair<>(2018, "Russia");

为了清楚起见,清单 4-4 包含了使用这个菱形语法的完整程序。

Listing 4-4. TestPair.java

// This program shows the usage of the diamond syntax when using generics

class Pair<T1, T2> {

T1 object1;

T2 object2;

Pair(T1 one, T2 two) {

object1 = one;

object2 = two;

}

public T1 getFirst() {

return object1;

}

public T2 getSecond() {

return object2;

}

}

class TestPair {

public static void main(String []args) {

Pair<Integer, String> worldCup = new Pair<>(2018, "Russia");

System.out.println("World cup " +  worldCup.getFirst() +

" in " + worldCup.getSecond());

}

}

该程序将干净地编译并打印以下语句:

World cup 2018 in Russia

注意,忘记初始化表达式中的菱形操作符< >是一个常见的错误,如

Pair<Integer, String> worldCup = new Pair(2018, "Russia");

下面是您将从编译器得到的警告(当您将命令行选项-Xlint:unchecked传递给 javac 时):

Pair.java:19: warning: [unchecked] unchecked call to Pair(T1,T2) as a member of the raw type Pair

Pair<Integer, String> worldCup = new Pair(2018, "Russia");

^

where T1,T2 are type-variables:

T1 extends Object declared in class Pair

T2 extends Object declared in class Pair

Pair.java:19: warning: [unchecked] unchecked conversion

Pair<Integer, String> worldCup = new Pair(2018, "Russia");

^

required: Pair<Integer,String>

found:    Pair

2 warnings

由于Pair是一个泛型类型,并且您忘记了使用<>或显式提供类型参数,编译器将其视为一个原始类型,其中Pair带有两个Object类型参数。尽管这种行为在这个特定的代码段中没有引起任何问题,但它是危险的,可能会导致错误,如下一节所示。

原始类型和泛型类型的互操作性

可以使用泛型类型而不指定其关联类型。在这种情况下,该类型被称为原始类型。例如,List<T>应该与关联的类型一起使用,即List<String>;但是,它可以在不指定伴随类型的情况下使用,即仅使用List。在后一种情况下,List被称为原始类型。

当您使用原始类型时,您将失去泛型提供的类型安全优势。例如,类型Vector是一个原始类型。原始类型绕过编译时的类型检查;然而,它们可能抛出运行时异常(例如,ClassCastException)。因此,不建议在新代码中使用原始类型。

好了,现在你明白你不应该使用原始类型。但是,你可能会问,为什么编译器本身不为这样的类型声明抛出错误?答案是向后兼容。Java 1.5 中引入了 Java 泛型。Java 支持原始类型,以使基于泛型的代码与遗留代码兼容。但是,强烈建议您不要在代码中使用原始类型。

为什么呢?如果将原始类型与泛型一起使用,会发生什么?让我们在清单 4-5 中使用这两种类型,并检查效果。

Listing 4-5. RawTest1.java

import java.util.List;

import java.util.LinkedList;

import java.util.Iterator;

class RawTest1 {

public static void main(String []args) {

List list = new LinkedList();

list.add("First");

list.add("Second");

List<String> strList = list;  //#1

for(Iterator<String> itemItr = strList.iterator(); itemItr.hasNext();)

System.out.println("Item: " + itemItr.next());

List<String> strList2 = new LinkedList<>();

strList2.add("First");

strList2.add("Second");

List list2 = strList2; //#2

for(Iterator<String> itemItr = list2.iterator(); itemItr.hasNext();)

System.out.println("Item: " + itemItr.next());

}

}

你对上述计划有什么期望?你认为它能正确编译/执行吗?嗯,是的——它将编译(有警告)并执行,没有任何问题。它打印以下内容:

Item: First

Item: Second

Item: First

Item: Second

清单 4-6 引入了一些变化;观察输出。

Listing 4-6. RawTest2.java

import java.util.List;

import java.util.LinkedList;

import java.util.Iterator;

class RawTest2 {

public static void main(String []args) {

List list = new LinkedList();

list.add("First");

list.add("Second");

List<String> strList = list;

strList.add(10);        // #1: generates compiler error

for(Iterator<String> itemItr = strList.iterator(); itemItr.hasNext();)

System.out.println("Item : " + itemItr.next());

List<String> strList2 = new LinkedList<>();

strList2.add("First");

strList2.add("Second");

List list2 = strList2;

list2.add(10); // #2: compiles fine, results in runtime exception

for(Iterator<String> itemItr = list2.iterator(); itemItr.hasNext();)

System.out.println("Item : " + itemItr.next());

}

}

在上面的示例中,您添加了两条语句。第一个声明如下:

strList.add(10);     // #1: generates compiler error

您试图在一个List<String>类型列表中添加一个整数项,因此您得到一个编译时错误"no suitable method found for add(int)"。如前所述,这种编译器级别的检查是好的,因为如果没有它,稍后可能会导致运行时异常。这是您添加的第二条语句:

list2.add(10);     // #2: compiles fine, results in runtime exception

这里,list2链表(原始类型)用一个泛型类型List<String>初始化。初始化之后,您在 list raw 类型中添加了一个整数。这是允许的,因为list2是一个原始类型。但是,会产生一个ClassCastException

我们从这个例子中学到的教训是避免在我们的程序中混合原始类型和泛型类型,因为这可能导致运行时的错误行为。如果您需要在程序中同时使用这两种类型,请确保在容器中添加单一类型的项,并使用相同的类型进行检索。

避免将原始类型与泛型类型混合。

通用方法

与泛型类类似,您可以创建泛型方法,即采用泛型参数类型的方法。泛型方法对于编写适用于多种类型而功能保持不变的方法非常有用。例如,java.util.Collections类中有许多泛型方法。

让我们实现一个名为fill()的简单方法。给定一个容器,fill()方法用值val填充所有容器元素。清单 4-7 包含了Utilities类中fill()方法的实现。

Listing 4-7. UtilitiesTest.java

// This program demonstrates generic methods

import java.util.List;

import java.util.ArrayList;

class Utilities {

public static <T> void fill(List<T> list, T val) {

for(int i = 0; i < list.size(); i++)

list.set(i, val);

}

}

class UtilitiesTest {

public static void main(String []args) {

List<Integer> intList = new ArrayList<Integer>();

intList.add(10);

intList.add(20);

System.out.println("The original list is: " + intList);

Utilities.fill(intList, 100);

System.out.println("The list after calling Utilities.fill() is: " + intList);

}

}

它打印以下内容

The original list is: [10, 20]

The list after calling Utilities.fill() is: [100, 100]

让我们一步一步地看看这段代码:

You create a method named fill() in the Utilities class with this declaration: public static <T> void fill(List<T> list, T val) You declare the generic type parameter T in this method. After the qualifiers public and static, you put <T> and then followed it by return type, method name, and its parameters. This declaration is different from generic classes—you give the generic type parameters after the class name in generic classes.   In the body, you write the code as if it’s a normal method. for(int i = 0; i < list.size(); i++)     list.set(i, val); You loop over the list from 0 until its size and set each of the elements to value val in each iteration. You use the set() method in List, which takes the index position in the container as the first argument and the actual value to be set as the second argument.   In the main() method in the UtilitiesTest class, this is how you call the fill() method: Utilities.fill(intList, 100);  

请注意,您没有显式给出泛型类型参数值。由于intList是类型Integer并且 100 被装箱为类型Integer,编译器推断出fill()方法中的类型T是类型Integer

泛型和子类型

您可以将派生类型对象分配给其基类型引用;这就是你所谓的分型。但是,对于泛型,类型参数应该完全匹配;否则你会得到一个编译错误。换句话说,子类型对于泛型参数不起作用。是的,这是一个很难记住的规则,所以让我们更详细地讨论为什么子类型对于泛型类型参数不起作用。

子类型化适用于类类型:你可以将一个派生类型对象赋给它的基类引用。但是,子类型对泛型类型参数不起作用:不能将派生的泛型类型参数赋给基类型参数。

让我们看看,如果假设可以对泛型类型参数使用子类型,会出现什么问题。

// illegal code –``assume

List<Number> intList = new ArrayList<Integer>();

intList.add(new Integer(10)); // okay

intList.add(new Float(10.0f)); // oops!

List<Number>类型的intList应该保存一个数组列表对象。但是,你储存了一个 ArrayList<Integer>。这看起来很合理,因为List延伸了ArrayList,而Integer延伸了Number。但是,您最终可以在intList中插入一个Float值!回想一下,intList的动态类型是ArrayList<Integer>类型——所以您在这里违反了类型安全(因此将得到不兼容类型的编译器错误)。由于泛型旨在避免类似这样的类型安全错误,因此不能将派生的泛型类型参数赋给基类型参数。

正如您所看到的,泛型参数类型的子类型化是不允许的,因为它是不安全的——但它仍然是一个不方便的限制。幸运的是,Java 支持通配符参数类型,您可以在其中使用子类型。我们现在将探索这种能力。

泛型的类型参数有一个限制:对于赋值,泛型类型参数应该完全匹配。为了克服这个子类型问题,可以使用通配符类型。

通配符参数

在上一节中,您看到了子类型对于泛型类型参数不起作用。所以,

List<Number> intList = new ArrayList<Integer>();

给出编译器错误

WildCardUse.java:6: incompatible types

found   : java.util.ArrayList<java.lang.Integer>

required: java.util.List<java.lang.Number>

List<Number> numList = new ArrayList<Integer>();

如果您稍微更改语句以使用通配符参数,它将会编译

List<?> wildCardList = new ArrayList<Integer>();

通配符是什么意思?就像你在卡牌游戏中用来替代任何一张牌的通配符(啊,玩卡牌游戏太好玩了!),您可以使用通配符来表示它可以匹配任何类型。对于List<?>,你的意思是它是任何类型的List——换句话说,你可以说它是一个“未知列表!”

但是等一下……当你想要一个表示“任何类型”的类型时,你使用Object类,不是吗?同样的语句,但是使用了Object类型参数,怎么样?

List<Object> numList = new ArrayList<Integer>();

运气不好——使用List<Number>您会得到与上面相同的错误!

WildCardUse.java:6: incompatible types

found   : java.util.ArrayList<java.lang.Integer>

required: java.util.List<java.lang.Object>

List<Object> numList = new ArrayList<Integer>();

换句话说,您仍然在尝试对泛型参数使用子类型化——但它仍然不起作用。如你所见,List<Object>List<?>不一样。事实上,List<?>是任何List类型的超类型,这意味着你可以在List<?>预期的地方通过List<Integer>,或者List<String>,甚至List<Object>

让我们在一个例子中使用通配符,看看它是否有效(参见清单 4-8 )。

Listing 4-8. WildCardUse.java

// This program demonstrates the usage of wild card parameters

import java.util.List;

import java.util.ArrayList;

class WildCardUse {

static void printList(List<?> list){

for(Object element: list)

System.out.println("[" + element + "]");

}

public static void main(String []args) {

List<Integer> list = new ArrayList<>();

list.add(10);

list.add(100);

printList(list);

List<String> strList = new ArrayList<>();

strList.add("10");

strList.add("100");

printList(strList);

}

}

该程序打印以下内容:

[10]

[100]

[10]

[100]

好了,它工作了,使用通配符的列表可以传递整数列表和字符串列表。发生这种情况是因为printList()方法的参数类型— List<?>。太好了!

通配符的限制

让我们考虑下面的代码片段,它试图添加一个元素并打印列表:

List<?> wildCardList = new ArrayList<Integer>();

wildCardList.add(new Integer(10));

System.out.println(wildCardList);

您会得到以下编译器错误:

WildCardUse.java:7: cannot find symbol

symbol  : method add(java.lang.Integer)

location: interface java.util.List<capture#145 of ? extends java.lang.Number>

wildCardList.add(new Integer(10));

为什么呢?你绝对确定add()方法存在于List接口中。那编译器为什么不找方法呢?

这个问题需要一些详细的解释。当你使用通配符类型<?>时,你对编译器说你忽略了类型信息,那么<?>代表未知类型。每当您试图将参数传递给泛型类型时,java 编译器都会尝试推断所传递参数的类型以及泛型的类型,并证明类型安全。现在,您试图使用add()方法在列表中插入一个元素。因为wildCardList不知道它持有哪种类型的对象,所以向它添加元素是有风险的。您最终可能会添加一个字符串,例如“hello ”,而不是一个整数值。为了避免这个问题(记住,语言中引入泛型是为了确保类型安全!),编译器不允许你调用修改对象的方法。因为add方法修改了对象,所以您会得到一个错误!错误信息看起来也很混乱,就像在<capture#145 of ? extends java.lang.Number>中一样。

一般来说,当你使用通配符参数时,你不能调用修改对象的方法。如果你试图修改,编译器会给出令人困惑的错误信息。但是,您可以调用访问该对象的方法。

要记住的要点

以下是一些可能对你的 OCPJP 八级考试有价值的建议:

  • 即使类或接口本身不是泛型的,也可以在接口或类中定义或声明泛型方法。
  • 不带类型参数的泛型类称为原始类型。当然,原始类型不是类型安全的。Java 支持原始类型,因此可以在比 Java 5 更早的代码中使用泛型类型(注意,泛型是在 Java 5 中引入的)。当您在代码中使用原始类型时,编译器会生成警告。您可以使用@SuppressWarnings({ "unchecked" })来抑制与原始类型相关的警告。
  • List<?>是任何List类型的超类型,这意味着你可以通过List<Integer>,或者List<String>,甚至是List<?>预期的List<Object>
  • 泛型的实现本质上是静态的,这意味着 Java 编译器解释源代码中指定的泛型,并用具体类型替换泛型代码。这被称为类型擦除。编译后,代码看起来类似于开发人员用具体类型编写的代码。本质上,使用泛型有两个好处:首先,它引入了一个抽象,使您能够编写泛型实现;其次,它允许您编写具有类型安全的泛型实现。
  • 由于类型擦除,泛型有许多限制。几个重要的例子如下:
    • 不能使用 new 运算符实例化泛型类型。例如,假设 mem 是一个字段,下面的语句将导致编译器错误:T mem = new T();  // wrong usage - compiler error
    • 不能实例化泛型类型的数组。例如,假设 mem 是一个字段,下面的语句将导致编译器错误:T[] amem = new T[100]; // wrong usage - compiler error
    • 可以声明 T 类型的实例字段,但不能声明 T 类型的静态字段。例如,class X<T> { T instanceMem;  // okay static T statMem;      // wrong usage - compiler error }
  • 不可能有泛型异常类;因此,下面的代码不会被编译:class GenericException<T> extends Throwable { } // wrong usage - compiler error
  • 不能用基元类型实例化泛型类型——换句话说,List<int>不能被实例化。但是,您可以使用装箱的基本类型。

创建和使用集合类

认证目标
创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象

Java 库有一个集合框架,它大量使用泛型,并提供了一组容器和算法。在本节中,我们将重点介绍如何使用集合框架。具体来说,我们将讨论重要的集合类,包括ArrayListTreeSetTreeMapArrayDeque对象。

A978-1-4842-1836-5_4_Figbb_HTML.gif术语集合是一个通用术语,而CollectionCollectionsjava.util包的特定 API。Collections——如在java.util.Collections中一样——是一个只包含静态方法的实用程序类。通用术语集合指的是诸如映射、集合、堆栈和队列之类的容器。为了避免混淆,我们将在本章中使用容器这个术语来指代这些集合。

抽象类和接口

java.util库中的类型层次由许多提供通用功能的抽象类和接口组成。表 4-1 列出了这个层次中的一些重要类型。我们将在本节稍后的部分更详细地讨论其中的一些类型。

表 4-1。

Important Abstract Classes and Interfaces in the Collections Framework

抽象类/接口简短描述
Iterable实现这个接口的类可以用来迭代一个foreach语句。
Collection集合层次结构中类的公共基接口。当你想写非常通用的方法时,可以通过Collection接口。例如,java.util.Collections中的max()方法接受一个Collection并返回一个对象。
List存储元素序列的容器的基本接口。您可以使用索引来访问元素,并在以后检索相同的元素(以便它保持插入顺序)。您可以在一个List中存储重复的元素。
SetSortedSetNavigableSet不允许重复元素的容器接口。SortedSet按排序顺序维护集合元素。NavigableSet al lows 在集合中搜索最接近的匹配。
QueueDequeQueue是容器的基本接口,包含一系列要处理的元素。例如,实现Queue的类可以是 LIFO(后进先出—如堆栈数据结构)或 FIFO(先进先出—如队列数据结构)。在Deque中,你可以在两端插入或移除元素。
MapSortedMapNavigableMap将键映射到值的容器的接口。在SortedMap中,按键是有序排列的。一个NavigableMap允许你搜索并返回给定搜索标准的最接近的匹配。注意,Map层次没有扩展Collection接口。
IteratorListIterator如果一个类实现了Iterator接口,你可以正向遍历容器。如果一个类实现了ListIterator接口,你可以正向和反向遍历。

这些是相当多的基本类型,但不要被它们淹没。您将看到特定的具体类,并使用其中的一些基本类型。我们将只讨论Collection接口,然后继续讨论特定的具体类,这些类是集合层次结构的一部分,在考试主题中会提到。

收集界面

Collection接口提供了对所有容器通用的方法,如add()remove()。表 4-2 列出了该接口中最重要的方法。在你使用它们之前,看一看它们。

表 4-2。

Important Methods in the Collection Interface

方法简短描述
boolean add(Element elem)elem添加到底层容器中。
void clear()从容器中移除所有元素。
boolean isEmpty()检查容器是否有任何元素。
Iterator<Element> iterator()返回一个用于遍历容器的Iterator<Element>对象。
boolean remove(Object obj)如果容器中存在obj,则移除该元素。
int size()返回容器中元素的数量。
Object[] toArray()返回一个包含容器中所有元素的数组。

根据底层容器的不同,add()remove()等方法可能会失败。例如,如果容器是只读的,您将无法添加或移除元素。除了这些方法之外,Collection接口中还有许多方法适用于容器中的多个元素(表 4-3 )。

表 4-3。

Methods in the Collection Interface That Apply to Multiple Elements

方法简短描述
boolean addAll(Collection<? extends Element> coll)coll中的所有元素添加到底层容器中。
boolean containsAll(Collection<?> coll)检查coll中给出的所有元素是否都存在于底层容器中。
boolean removeAll(Collection<?> coll)从底层容器中移除也存在于coll中的所有元素。
boolean retainAll(Collection<?> coll)仅当元素也出现在coll中时,才保留底层容器中的元素;它会删除所有其他元素。

具体类

Collection层次结构中的许多接口和抽象类提供了特定具体类实现/扩展的公共方法。具体的类提供了实际的功能,你只需要学习其中的一小部分就可以为 OCPJP 八级考试做好准备。表 4-4 总结了你应该知道的职业特点。

表 4-4。

Important Concrete Classes in Collection Framework

混凝土类简短描述
ArrayList在内部实现为可调整大小的数组。这是使用最广泛的具体类之一。搜索速度快,但插入或删除速度慢。允许重复。
LinkedList在内部实现双向链表数据结构。插入或删除元素很快,但搜索元素很慢。另外,当你需要一个堆栈(LIFO)或队列(FIFO)数据结构时,可以使用LinkedList。允许重复。
HashSet在内部实现为哈希表数据结构。用于存储一组元素—它不允许存储重复的元素。快速搜索和检索元素。它不维护存储元素的任何顺序。
TreeSet内部实现红黑树数据结构。与HashSet一样,TreeSet不允许存储重复项。然而,与HashSet不同的是,它按照排序的顺序存储元素。它使用树数据结构来决定在哪里存储或搜索元素,位置由排序顺序决定。
HashMap在内部实现为哈希表数据结构。存储键和值对。使用散列法寻找一个位置来搜索或存储一对。搜索或插入速度非常快。它不以任何顺序存储元素。
TreeMap内部使用红黑树数据结构实现。与HashMap不同,TreeMap按照排序的顺序存储元素。它使用树数据结构来决定在哪里存储或搜索键,位置由排序顺序决定。
PriorityQueue使用堆数据结构在内部实现。一个PriorityQueue用于基于优先级检索元素。无论插入的顺序如何,当删除元素时,将首先检索优先级最高的元素。

A978-1-4842-1836-5_4_Figbb_HTML.gif有许多旧的java.util类(现在称为遗留集合类型)被新的集合类所取代。其中有一些(括号内为较新的类型):Enumeration(Iterator)Vector(ArrayList)Dictionary(Map)Hashtable(HashMap)。另外,StackProperties是没有直接替换的遗留类。

数组列表类

用于存储一系列元素。您可以使用索引在特定位置插入容器的元素,并在以后检索相同的元素(即,它保持插入顺序)。您可以在列表中存储重复的元素。您需要知道两个具体的类:ArrayListLinkedList

ArrayList实现一个可调整大小的数组。当您创建一个本机数组(比如说,new String[10];)时,数组的大小在创建时就是已知的(固定的)。然而,ArrayList是一个动态数组:它可以根据需要增加大小。在内部,ArrayList分配一块内存,并根据需要增长。因此,在ArrayList中访问数组元素非常快。但是,当您添加或删除元素时,会在内部复制其余的元素;因此添加/删除元素是一项成本很高的操作。

这里有一个简单的例子来访问ArrayList中的元素。您使用一个ArrayList并使用for-each构造来遍历一个集合:

ArrayList<String> languageList = new ArrayList<>();

languageList.add("C");

languageList.add("C++");

languageList.add("Java");

for(String language : languageList) {

System.out.println(language);

}

它打印以下内容:

C

C++

Java

这个for-each相当于下面的代码,它显式地使用了一个Iterator:

for(Iterator<String> languageIter = languageList.iterator(); languageIter.hasNext();) {

String language = languageIter.next();

System.out.println(language);

}

该代码段也将打印与之前的for-each循环代码相同的输出。下面是这个for循环如何工作的逐步描述:

You use the iterator() method to get the iterator for that container. Since languageList is an ArrayList of type <String>, you should create Iterator with String. Name it languageIter.   Before entering the loop, you check if there are any elements to visit. You call the hasNext() method for checking that. If it returns true, there are more elements to visit; if it returns false, the iteration is over and you exit the loop.   Once you enter the body of the loop, the first thing you have to do is call next() and move the iterator. The next() method returns the iterated value. You capture that return value in the language variable.   You print the language value, and then the loop continues.  

这种迭代习惯——您调用iterator(), hasNext()next()方法的方式——学习起来很重要;我们将在示例中广泛使用 for-each 循环或这种习惯用法。

注意,您创建了ArrayList<String>Iterator<String>,而不是仅仅使用ArrayListIterator(也就是说,您提供了这些类的类型信息)。Collection类是泛型类;因此,您需要指定类型参数来使用它们。这里你存储/迭代一个字符串列表,所以你使用<String>

使用迭代器遍历容器时,可以删除元素。让我们创建一个有十个元素的ArrayList<Integer>类型的对象。您将遍历这些元素并删除它们(而不是使用ArrayList中的removeAll()方法)。清单 4-9 显示了代码。有用吗?

Listing 4-9. TestIterator.java

// This program shows the usage of Iterator

import java.util.ArrayList;

import java.util.Iterator;

class TestIterator {

public static void main(String []args) {

ArrayList<Integer> nums = new ArrayList<Integer>();

for(int i = 1; i < 10; i++)

nums.add(i);

System.out.println("Original list " + nums);

Iterator<Integer> numsIter = nums.iterator();

while(numsIter.hasNext()) {

numsIter.remove();

}

System.out.println("List after removing all elements" + nums);

}

}

它打印以下内容:

Original list [1, 2, 3, 4, 5, 6, 7, 8, 9]

Exception in thread "main" java.lang.IllegalStateException

at java.util.AbstractList$Itr.remove(AbstractList.java:356)

at TestIterator.main(Main.java:12)

哎呀!发生了什么事?问题是在调用remove()之前,你还没有调用过next()。在while循环条件中检查hasNext(),使用next()移动到元素,并调用remove()是移除元素的正确习惯用法。如果你没有正确地遵循它,你可能会陷入困境(即,你会得到IllegalStateException)。类似地,如果您两次调用remove()而没有在语句之间插入一个next(),您将得到这个异常。

让我们通过在调用remove()之前调用next()来修复这个程序。以下是代码的相关部分:

Iterator<Integer> numsIter = nums.iterator();

while(numsIter.hasNext()) {

numsIter.next();

numsIter.remove();

}

System.out.println("List after removing all elements " + nums);

它打印出不含任何元素的列表,如预期的那样:

List after removing all elements []

A978-1-4842-1836-5_4_Figbb_HTML.gif记住next()需要在Iterator中调用remove()之前被调用;否则你会得到一个IllegalStateException。类似地,在后续语句中调用remove()而不在这些语句之间调用next()也会导致这个异常。简而言之,当迭代器遍历容器时,对底层容器的任何修改都会导致这个异常。

使用 Arrays.asList()

java.util.Arrays类有一个名为asList()的有用方法,它返回一个固定大小的列表。关于返回的List对象有一个有趣的方面:您不能添加或删除元素,但是您可以修改由asList()方法返回的对象!同样,您通过List所做的修改也反映在原始数组中(参见清单 4-10 )。

Listing 4-10. ArrayAsList.java

import java.util.List;

import java.util.Arrays;

class ArrayAsList {

public static void main(String []args) {

Double [] temperatureArray = {31.1, 30.0, 32.5, 34.9, 33.7, 27.8};

System.out.println("The original array is: " +

Arrays.toString(temperatureArray));

List<Double> temperatureList = Arrays.asList(temperatureArray);

temperatureList.set(0, 35.2);

System.out.println("The modified array is: " +

Arrays.toString(temperatureArray));

}

}

它打印以下内容:

The original array is: [31.1, 30.0, 32.5, 34.9, 33.7, 27.8]

The modified array is: [35.2, 30.0, 32.5, 34.9, 33.7, 27.8]

Arrays类只提供有限的功能,你可能经常想使用Collections类中的方法。为了实现这一点,调用Arrays.asList()方法是一种有用的技术。

TreeSet 类

正如我们在高中数学课上所学的,没有重复的内容。与List不同的是,Set不记得你在哪里插入了元素(也就是说,它不记得插入顺序)。

Set有两个重要的具体类:HashSetTreeSetHashSet用于快速插入和检索元素;它不维护它所保存的元素的任何排序顺序。一个TreeSet按照排序的顺序存储元素(它实现了SortedSet接口)。

给定一个句子,你如何将句子中使用的字母按字母顺序排序?一个TreeSet将这些值按顺序排列,所以你可以使用一个TreeSet容器来解决这个问题(参见清单 4-11 )。

Listing 4-11. TreeSetTest.java

// This program demonstrates the usage of TreeSet class

import java.util.Set;

import java.util.TreeSet;

class TreeSetTest {

public static void main(String []args) {

String pangram = "the quick brown fox jumps over the lazy dog";

Set<Character> aToZee = new TreeSet<Character>();

for(char gram : pangram.toCharArray())

aToZee.add(gram);

System.out.println("The pangram is: " + pangram);

System.out.print("Sorted pangram characters are: " + aToZee);

}

}

它打印以下内容:

The pangram is: the quick brown fox jumps over the lazy dog

Sorted pangram characters are: [ , a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]

盘符是一个至少使用一次字母表中所有字母的句子。你想在集合中存储一个盘符。因为您需要使用容器的引用类型,所以您已经创建了一个CharacterTreeSet

现在,如何从一个String中获取字符?记住数组索引对String s 无效。例如,为了获得第一个字符"t",如果你在程序中使用pangram[0],你将得到一个编译器错误。幸运的是,String有一个名为toCharArray()的方法返回一个char[]。所以,你用这个方法遍历字符串,得到所有的字符。当您将字符添加到TreeSet中时,字符会按排序顺序存储。所以,当你打印集合时,你会得到所有的小写字母。

请注意,在输出中有一个前导逗号。为什么呢?pangram 字符串有许多空白字符。一个空白也被存储在集合中,所以它也被打印出来!

地图界面

一个Map存储键和值对。Map接口没有扩展Collection接口。然而,Map接口中有一些方法可以让实现Collection接口的对象类解决这个问题。还有,Map中的方法名和Collection中的方法非常相似,所以很容易理解和使用MapMap有两个重要的具体类:HashMapTreeMap

  • A HashMap内部使用哈希表数据结构。在HashMap中,搜索(或查找元素)是一种快速操作。然而,HashMap既不会记住您插入元素的顺序,也不会让元素保持任何排序的顺序。
  • A TreeMap内部使用红黑树数据结构。与HashMap不同的是,TreeMap保持元素有序(即,按键排序)。因此,搜索或插入比HashMap要慢一些。
NavigableMap 接口和 TreeMap 类

NavigableMap接口扩展了SortedMap接口。TreeMap类是广泛使用的实现NavigableMap的类。顾名思义,有了NavigableMap,你就可以轻松操控Map了。它有许多方法使导航变得容易。您可以获得与给定键匹配的最接近的值、小于给定键的所有值、大于给定键的所有值,以此类推。让我们看一个例子:列侬、麦卡特尼、哈里森和斯塔尔参加了在线考试。在那次考试中,他们的最高分是 100 分,及格分数是 40 分。如果想查找谁通过了考试等细节,并对考试成绩进行升序或降序排序,NavigableMap(和TreeMap)非常方便(参见清单 4-12 )。

Listing 4-12. NavigableMapTest.java

// This program demonstrates the usage of navigable tree interface and TreeMap class

import java.util.NavigableMap;

import java.util.TreeMap;

public class NavigableMapTest {

public static void main(String []args) {

NavigableMap<Integer, String> examScores = new TreeMap<Integer, String>();

examScores.put(90, "Sophia");

examScores.put(20, "Isabella");

examScores.put(10, "Emma");

examScores.put(50, "Olivea");

System.out.println("The data in the map is: " + examScores);

System.out.println("The data descending order is: " + examScores.descendingMap());

System.out.println("Details of those who passed the exam: " + examScores.tailMap(40));

System.out.println("The lowest mark is: " + examScores.firstEntry());

}

}

它打印以下内容:

The data in the map is: {10=Emma, 20=Isabella, 50=Olivea, 90=Sophia}

The data descending order is: {90=Sophia, 50=Olivea, 20=Isabella, 10=Emma}

Details of those who passed the exam: {50=Olivea, 90=Sophia}

The lowest mark is: 10=Emma

在这个程序中,你有一个NavigableMap<Integer, String>来映射考试分数和人名。您创建一个TreeMap<Integer, String>来实际存储考试分数。默认情况下,TreeMap按升序存储数据。如果您希望数据按降序排列,这很容易:您只需使用descendingMap()方法(或者如果您只对键感兴趣,使用descendingKeySet())。

假设及格分数为 40,您可能希望获得包含考试不及格人员数据的地图。为此,您可以使用键值为 40 的headMap()方法(因为数据是按升序排列的,所以您希望从给定位置获取地图的“头部”部分)。同样,要得到通过考试的人的数据,可以使用tailMap()的方法。

如果您想要高于和低于及格分数的直接分数,您可以分别使用higherEntry()lowerEntry()方法。firstEntry()lastEntry()方法给出了具有最低和最高键值的条目。所以,当你在examScores上使用firstEntry()方法时,你得到艾玛 10 分。如果你使用lastEntry(),你会得到索菲亚,她有 90 分。

Deque 接口和 ArrayDeque 类

Deque(双端队列)是一种数据结构,允许你从两端插入和删除元素。在 Java 6 的java.util.collection包中引入了Deque接口。Deque接口扩展了Queue接口。因此,Queue提供的所有方法在Deque接口中也是可用的。

Deque接口的具体实现有三种:LinkedListArrayDequeLinkedBlockingDeque。让我们用ArrayDeque来了解一下Deque接口的特点。

考虑实现一个特殊的队列(比如说,支付水电费),在这个队列中,客户只能被添加到队列的末尾,并且可以从队列的前面(当客户支付账单时)或者从队列的末尾(当客户对长队感到沮丧并自己离开队列时)被删除。清单 4-13 展示了如何做到这一点。

Listing 4-13. SplQueueTest.java

// This program shows the usage of Deque interface

import java.util.ArrayDeque;

import java.util.Deque;

class SplQueue {

private Deque<String> splQ = new ArrayDeque<>();

void addInQueue(String customer){

splQ.addLast(customer);

}

void removeFront(){

splQ.removeFirst();

}

void removeBack(){

splQ.removeLast();

}

void printQueue(){

System.out.println("Special queue contains: " + splQ);

}

}

class SplQueueTest {

public static void main(String []args) {

SplQueue splQ = new SplQueue();

splQ.addInQueue("Harrison");

splQ.addInQueue("McCartney");

splQ.addInQueue("Starr");

splQ.addInQueue("Lennon");

splQ.printQueue();

splQ.removeFront();

splQ.removeBack();

splQ.printQueue();

}

}

它打印以下内容:

Special queue contains: [Harrison, McCartney, Starr, Lennon]

Special queue contains: [McCartney, Starr]

首先定义一个类——SplQueue——它用基本的四个操作定义了一个类型为ArrayDeque的容器splQ。方法addInQueue()在队列末尾添加一个客户,方法removeBack()从队列末尾移除一个客户,方法removeFront()从队列前面移除一个客户,方法printQueue()简单地打印队列的所有元素。您只需使用来自Deque接口的addLast(), removeFirst()removeLast()方法来实现SplQueue类的方法。在您的main()方法中,您实例化了SplQueue并调用了SplQueue类的addInQueue()方法。在这之后,您从前面移除一个客户,从末尾移除一个客户,并打印移除前后的队列内容。嗯,正如你所期望的那样。

A978-1-4842-1836-5_4_Figbb_HTML.gifArrayListArrayDeque的区别在于,你可以使用索引在数组列表的任意位置添加元素;但是,只能在数组队列的前端或末端添加元素。这使得数组队列中的插入比数组列表更有效;然而,数组队列中的导航比数组列表中的导航开销更大。

可比和比较器接口

认证目标
使用 java.util.Comparator 和 java.lang.Comparable 接口

顾名思义,ComparableComparator接口用于比较相似的对象(例如,在执行搜索或排序时)。假设你有一个容器,里面包含了一个Person对象的列表。现在,你如何比较两个Person物体?有许多可比较的属性,例如 SSN、姓名、驾照号码等等。可以在SSN上比较两个物体,也可以在name上比较一个人;这个要看上下文。因此,比较Person对象的标准不能预先定义;开发人员必须定义这个标准。Java 定义了ComparableComparator接口来实现同样的功能。

Comparable接口只有一个方法compareTo(),声明如下:

int compareTo(Element that)

因为您在一个类中实现了compareTo()方法,所以您有this引用可用。您可以将the current element与传递的Element进行比较,并返回一个int值。int值应该是多少?下面是返回整数值的规则:

return 1 if current object > passed object

return 0 if current object == passed object

return -1 if current object < passed object

现在,一个重要的问题:现在>,< or == mean for an 【 ? Hmm, it is left to you to decide how to compare two objects! But the meaning of comparison should be a natural one; in other words, the comparison should mean natural ordering. For example, you saw how 【 s are compared with each other, based on a numeric order, which is the natural order for 【 types. Similarly, you compare 【 s using lexicographic comparison, which is the natural order for 【 s. For user-defined classes, you need to find the natural order in which you can compare the objects. For example, for a 【 class, 【 might be the natural order for comparing 【 objects. Listing 4-14 实现了一个简单的Student类。

Listing 4-14. ComparatorTest1.java

// This program shows the usage of Comparable interface

import java.util.Arrays;

class Student implements Comparable<Student> {

String id;

String name;

Double cgpa;

public Student(String studentId, String studentName, double studentCGPA) {

id = studentId;

name = studentName;

cgpa = studentCGPA;

}

public String toString() {

return " \n " + id + "  \t  " + name + "  \t  " + cgpa;

}

public int compareTo(Student that) {

return this.id.compareTo(that.id);

}

}

class ComparatorTest1 {

public static void main(String []args) {

Student []students = {  new Student("cs011", "Lennon  ", 3.1),

new Student("cs021", "McCartney", 3.4),

new Student("cs012", "Harrison ", 2.7),

new Student("cs022", "Starr ", 3.7) };

System.out.println("Before sorting by student ID");

System.out.println("Student-ID \t  Name \t  CGPA (for 4.0) ");

System.out.println(Arrays.toString(students));

Arrays.sort(students);

System.out.println("After sorting by student ID");

System.out.println("Student-ID \t  Name \t  CGPA (for 4.0) ");

System.out.println(Arrays.toString(students));

}

}

它打印以下内容:

Before sorting by student ID

Student-ID        Name    CGPA (for 4.0)

[

cs011            Lennon         3.1,

cs021            McCartney      3.4,

cs012            Harrison       2.7,

cs022            Starr          3.7]

After sorting by student ID

Student-ID        Name    CGPA (for 4.0)

[

cs011            Lennon         3.1,

cs012            Harrison       2.7,

cs021            McCartney      3.4,

cs022            Starr          3.7]

您已经实现了Comparable<Student>接口。当您调用sort()方法时,它会调用compareTo()方法来根据ID比较Student对象。因为Student ID是唯一的,所以这是一种自然的比较顺序,效果很好。

现在,您可能需要根据学生获得的累计平均绩点(CGPA)来安排学生。你甚至可能需要根据它们的name来比较Student,如果你需要实现两个或更多的方法来比较两个相似的对象,那么你可以实现Comparator interface。清单 4-15 是一个实现(在Student类中没有变化,所以我们在这里不再生产它)。

Listing 4-15. ComparatorTest2.java

// This program shows the implementation of Comparator interface

import java.util.Arrays;

import java.util.Comparator;

class CGPAComparator implements Comparator<Student> {

public int compare(Student s1, Student s2) {

return (s1.cgpa.compareTo(s2.cgpa));

}

}

class ComparatorTest2 {

public static void main(String []args) {

Student []students = {  new Student("cs011", "Lennon  ", 3.1),

new Student("cs021", "McCartney", 3.4),

new Student("cs012", "Harrison ", 2.7),

new Student("cs022", "Starr ", 3.7) };

System.out.println("Before sorting by CGPA ");

System.out.println("Student-ID \t  Name \t  CGPA (for 4.0) ");

System.out.println(Arrays.toString(students));

Arrays.sort(students, new CGPAComparator());

System.out.println("After sorting by CGPA");

System.out.println("Student-ID \t  Name \t  CGPA (for 4.0) ");

System.out.println(Arrays.toString(students));

}

}

它打印以下内容:

Before sorting by CGPA

Student-ID        Name    CGPA (for 4.0)

[

cs011            Lennon         3.1,

cs021            McCartney      3.4,

cs012            Harrison       2.7,

cs022            Starr          3.7]

After sorting by CGPA

Student-ID        Name    CGPA (for 4.0)

[

cs012           Harrison        2.7,

cs011           Lennon          3.1,

cs021           McCartney       3.4,

cs022           Starr           3.7]

是的,程序打印按 CGPA 排序的Student数据。你没改Student类;该类仍然实现了Comparable<String>接口并定义了compareTo()方法,但是您不需要在程序中使用compareTo()方法。您创建了一个名为CGPAComparator的独立类,并实现了Comparator<Student>接口。您定义了compare()方法,它将两个Student对象作为参数。通过使用来自Double类的compareTo()方法来比较参数s1s2的 CGPA。除了调用sort()方法的方式之外,您没有改变main()方法中的任何东西。您创建一个新的CGPAComparator()对象,并作为第二个参数传递给sort()方法。默认情况下sort()使用compareTo()方法;因为您是显式传递一个Comparator对象,所以它现在使用在CGPAComparator中定义的compare()方法。因此,Student对象现在根据它们的 CGPA 进行比较和排序。

A978-1-4842-1836-5_4_Figbb_HTML.gif大多数类都有比较对象的自然顺序,所以在那些情况下使用Comparable接口。如果你想比较自然顺序之外的对象,或者如果你的类类型没有自然顺序,那么使用Comparator接口。

收集流和过滤器

认证目标
集合流和过滤器

Java 8 中引入的java.util.stream包中提供了新的流 API。这个包中的主要类型是Stream<T>接口,它是对象引用的流。IntStreamLongStreamDoubleStream分别是原始类型intlongdouble的流。

Java 8 中的Co llection接口增加了stream()parallelStream()方法。流是一系列元素。使用stream()方法获得流时,可以执行顺序操作,使用parallelStream()方法可以执行并行操作。(我们将在第十一章中讨论并行流。)由于ListSetDequeQueue等接口扩展了Collection接口,所以可以从实现这些接口的集合类中获得流或并行流。例如,您可以从一个ArrayList对象获得一个流。

流提供了管道功能——您可以过滤、映射和搜索数据。换句话说,流操作可以“链接”在一起,形成一个管道,称为“流管道”。我们将在本节稍后介绍流管道,并在专门讨论流的章节中详细介绍它们(Java Stream API 的第六章)。

流最常见的来源是集合对象,如集合、映射和列表。但是,请注意,我们可以独立于集合使用 streams API。在本章的其余部分,我们将讨论集合如何与流一起使用。

使用 forEach 迭代

认证目标
使用流和列表的 forEach 方法进行迭代

作为 Java 程序员,我们习惯于对集合执行外部迭代。例如,考虑以下字符串列表:

List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");

当我们使用 for 循环遍历这样一个集合时,我们使用的是外部迭代,如:

for(String string : strings) {

System.out.println(string);

}

内部迭代将迭代留给库代码。同样的代码可以转换成下面的等价代码,这些代码使用了 lambda 表达式(清单 4-16 ):

Listing 4-16. InternalIteration.java

import java.util.Arrays;

import java.util.List;

public class InternalIteration {

public static void main(String []args) {

List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");

strings.forEach(string -> System.out.println(string));

}

}

该程序打印:

eeny

meeny

miny

mo

注意,List接口扩展了Iterable接口,后者有一个默认的forEach方法(这个方法是在 Java 8 中添加的)。因此,我们能够通过调用strings对象上的forEach方法来执行内部迭代,并将一个 lambda 表达式作为参数传递给它。

虽然这个例子很简单,但是它说明了 Java 8 方法的一个主要变化:我们正在从外部迭代转移到内部迭代。事实上,整个 Stream API ( 第六章)都是基于内部迭代的概念。

在我们讨论Stream接口和流管道之前,让我们讨论一个与 lambda 函数相关的重要话题:方法引用。

流的方法引用

认证目标
对流使用方法引用

在清单 4-16 中,我们使用了这个 lambda 表达式:

strings.forEach(string -> System.out.println(string));

这段代码有些冗长,因为我们采用了string参数并将其传递给了System.out.println。幸运的是,Java 8 引入了一个称为“方法引用”的特性。方法引用使用“::”运算符。下面是一个使用方法引用的简化表达式:

strings.forEach(System.out::println);

方法引用路由给定的参数。在这种情况下,System.out::println相当于使用 lambda 表达式string -> System.out.println(string)

把下面的语句简化成使用方法引用怎么样?

strings.forEach(string -> System.out.println(string.toUpperCase()));

这段代码中的 lambda 表达式在给定的String对象上调用toUpperCase()方法。因为方法引用只是传递参数,所以您不能直接使用它们来简化这个 lambda 表达式。另一种方法是将这段代码放在一个方法中,并使用该方法的引用(清单 4-17 )。

Listing 4-17. MethodReference.java

import java.util.Arrays;

import java.util.List;

class MethodReference {

public static void printUpperCaseString(String string) {

System.out.println(string.toUpperCase());

}

public static void main(String []args) {

List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");

strings.forEach(MethodReference::printUpperCaseString);

}

}

该程序打印:

EENY

MEENY

MINY

MO

在这种情况下,我们在MethodReference类中引入了一个静态方法。printUpperCaseString调用传递的String参数上的toUpperCase()方法,并打印结果字符串。

总而言之,方法引用有两个主要好处:

方法引用是传递参数的一种方式,因此使用它们通常比等效的 lambda 表达式更方便(代码更简洁)。例如,我们在清单 4-16 中看到了如何将System.out::println用作arg -> System.out.println(arg)的等价物。

方法引用语法使得使用方法作为 lambda 表达式变得更加容易(如清单 4-17 )。

了解流接口

认证目标
描述流接口和流管道

Stream接口是java.util.stream包中提供的最重要的接口。IntStreamLongStreamDoubleStream这三个班级分别是intlongdoubleStream专业。图 4-1 显示了这些流的继承层次。

A978-1-4842-1836-5_4_Fig1_HTML.jpg

图 4-1。

Some important interfaces in java.util.stream package

溪流管道

流操作可以“链”在一起,形成一个“流管道”。蒸汽管道由三部分组成(见图 4-2 ):

A978-1-4842-1836-5_4_Fig2_HTML.jpg

图 4-2。

The stream pipeline with examples

  • Source:创建一个流(从一个集合或数组或使用Stream方法,如of()generate())。
  • 中间操作:可以链接在一起的可选操作(如Stream界面中的map()filter()distinct()sorted()方法)。
  • 终端操作:产生一个结果(如Stream界面中的sum()collect()forEach()reduce()方法)。

下面是一个流管道的例子(清单 4-18 )。

Listing 4-18. StreamPipelineExample.java

import java.util.Arrays;

class StreamPipelineExample {

public static void main(String []args) {

Arrays.stream(Object.class.getMethods())       // source

.map(method -> method.getName())       // intermediate op

.distinct()                            // intermediate op

.forEach(System.out::println);         // terminal operation

}

}

这段代码打印出来

wait

equals

toString

hashCode

getClass

notify

notifyAll

Object.class.getMethods()产生了一个由Object类中的Method对象组成的数组。操作map(method -> method.getName())以数组的形式返回方法的名称(作为Stream的一部分)。注意Object类中的wait()方法是一个重载方法。为了获得唯一的方法名,我们可以使用distinct()操作来删除数组中的重复条目。最后,forEach()终端操作打印出方法的名称。

理解流管道的一种方法是将管道的组件分解成单独的语句。清单 4-18 将零件分解成独立的组件,是清单 4-19 的等效代码。

Listing 4-19. StreamPipelineComponents.java

import java.util.Arrays;

import java.util.stream.Stream;

import java.lang.reflect.Method;

class StreamPipelineComponents {

public static void main(String []args) {

Method[] objectMethods = Object.class.getMethods();

Stream<Method> objectMethodStream = Arrays.stream(objectMethods);

Stream<String> objectMethodNames = objectMethodStream.map(method -> method.getName());

Stream<String> uniqueObjectMethodNames = objectMethodNames.distinct();

uniqueObjectMethodNames.forEach(System.out::println);

}

}

在这种情况下,我们通过对Object.class.getMethod()的结果调用Arrays.stream()方法来获得一个流——这是流的源。map()distinct()方法都将流作为输入,并将(修改后的)流作为输出返回。最后,流上的forEach()方法是管道中的终端操作。

A978-1-4842-1836-5_4_Figbb_HTML.gif不要将流中的mapjava.util.Map接口混淆。map()方法是一种中间操作,它从传入流中获取元素,应用该操作,并生成元素流作为输出;Map接口保存键值对。

溪流源头

流有许多来源,包括流接口、集合和数组中的生成器方法。让我们考虑一个简单的任务,获取一个 1 到 5 的整数值流。

You can use range or iterate factory methods in the IntStream interface. IntStream.range(1, 6) The range() method takes two arguments: it starts from the start value (given as the first argument) and goes on adding 1 to result in stream elements till it reaches the end value (given as the second argument and is excluding that value itself). In this case, we have passed the values 1 and 6, so the reduce() method generates the stream of integer values starting from 1, adds the value 1 and results in values 2, 3, 4, and 5, and stops there because it hit the end value 6. IntStream.iterate(1, i -> i + 1).limit(5) The iterate() method takes two arguments: the initial value (as the first argument) and iteratively calls the given function (as second argument) by using the initial value as the seed. In this case, the first argument is 1, and it iteratively calls i + 1, generating the integer values 2, 3, 4, 5, … This is an infinite stream. We limit the stream to the first five values by calling limit(5) over this infinite stream of integer values.   You can use the stream() method in java.util.Arrays class to create a stream from a given array, as in: Arrays.stream(new int[] {1, 2, 3, 4, 5}) Arrays.stream(new Integer[] {1, 2, 3, 4, 5}) The stream() method was added in the Arrays class in Java 8: // in Arrays class public static IntStream stream(int[] array) { /* returns a stream of integers */ } public static <T> Stream<T> stream(T[] array) { /* returns a stream of T objects */ } Overloaded versions of stream() method takes long[], double[], and T[]. Since we are passing an int[] and the Integer[], the calls stream() method resolve to stream(int []) and stream(T[]) respectively and a integer stream is returned.   We can also create streams using factories and builders. The of() method is a factory method in the Stream interface: Stream.of(1, 2, 3, 4, 5) Stream.of(new Integer[]{1, 2, 3, 4, 5})  

Stream接口中重载的of()方法接受变量参数列表或T类型的元素。此外,您可以使用builder()方法,通过添加每个元素来构建Stream对象,如下所示:

Stream.builder().add(1).add(2).add(3).add(4).add(5).build()

这并不是您可以用来生成整数流的方法的详尽列表—这只是让您知道有许多方法可以获得流。如前所述,Collection接口添加了方法stream()parallelStream()。因此,任何Collection对象都是流的源——您只需要对它调用stream()parallelStream()方法。例如:

List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");

strings.stream().forEach(string -> System.out.println(string));

在这种情况下,我们通过调用stream()方法从List<String>对象获取流。Java 库中还有许多返回流的其他类型,例如:

  • java.nio.file.Files类中的lines()方法
  • java.util.regex.Pattern类中的splitAsStream()方法
  • java.util.Random类中的ints()方法
  • java.lang.String类中的chars()方法

这里有一些关于如何使用它们的简单快捷的方法。

The java.nio.file.Files class has lines() method that returns a Stream<String>. This code prints the contents of the file “FileRead.java” in the current directory. Files.lines(Paths.get("./FileRead.java")).forEach(System.out::println);   The java.util.Pattern class has splitAsStream() method that returns a Stream<String>. This code splits the input string “java 8 streams” based on whitespace and hence prints the strings “java”, “8”, and “streams” on the console. Pattern.compile(" ").splitAsStream("java 8 streams").forEach(System.out::println);   The java.util.Random class has ints() method that returns an IntStream. It generates an infinite stream of random integers; so to restrict the number of integers to 5 integers, we call limit(5) on that stream. new Random().ints().limit(5).forEach(System.out::println);   The String class has chars() method (newly introduced in Java 8 in CharSequence—an interface that String class implements). This method returns an IntStream (why IntStream? Remember that there is no equivalent char specialization for Streams). This code calls sorted() method on this stream, so the stream elements get sorted in ascending order. Because it is a stream of integers, this code uses "%c" to explicitly force the conversion from int to char. "hello".chars().sorted().forEach(ch -> System.out.printf("%c ", ch)); // prints e h l l o  

在这些例子中,我们已经使用了中间操作,如limit()sorted()。现在让我们更详细地讨论这种中间操作。

中间操作

中间操作转换流中的元素。表 4-5 列出了Stream<T>中一些重要的中间操作。我们将在 Streams API 的第六章的中讨论其他中间操作,如flatMap()及其变体。

表 4-5。

Important Intermediate Operations in the Stream Interface

方法简短描述
Stream<T> filter(Predicate<? super T> check)删除check谓词返回 false 的元素。
<R> Stream<R> map(Function<? super T,? extends R> transform)对流中的每个元素应用transform()函数。
Stream<T> distinct()移除流中的重复元素;它使用equals()方法来确定一个元素是否在流中重复。
Stream<T> sorted() Stream<T> sorted(Comparator<? super T> compare)按自然顺序对元素进行排序。重载版本需要一个Comparator——你可以为此传递一个 lambda 函数。
Stream<T> peek(Consumer<? super T> consume)返回流中的相同元素,但也对元素执行传递的consume lambda 表达式。
Stream<T> limit(long size)如果流中的元素比给定的size多,则删除元素。

注意,该表中的所有中间操作都返回一个Stream<T>作为结果。

中间操作是可选的;在流管道中不需要任何中间操作。这里有一个简单的例子:

Stream.of(1, 2, 3, 4, 5).count();

这段代码返回值 5。在这种情况下,Stream.of()方法是流源,count()方法是终端操作。count()方法返回流中元素的数量。

让我们在这个流管道中引入一个中间操作:

Stream.of(1, 2, 3, 4, 5).map(i -> i * i).count();

map()操作将作为参数传递的给定函数应用于流的元素。在这种情况下,它对流中的元素求平方。这段代码还返回值 5。你能看到在这段代码中应用map()方法的检查结果吗?你可以用peek()的方法:

Stream.``of``(1, 2, 3, 4, 5).map(i -> i * i).peek(i -> System.``out

这段代码打印出来

1 4 9 16 25

这个例子还说明了如何将中间操作链接在一起。这之所以成为可能,是因为中间操作返回流。

现在,让我们在调用map()方法之前添加一个peek()方法,以了解它是如何工作的:

Stream.of(1, 2, 3, 4, 5)

.peek(i -> System.out.printf("%d ", i))

.map(i -> i * i)

.peek(i -> System.out.printf("%d ", i))

.count();

这段代码打印出来

1 1 2 4 3 9 4 16 5 25

从这个输出可以看出,流管道正在逐个处理元素。每个元素都映射到它的平方值。peek()方法帮助我们理解流是如何处理元素的。

A978-1-4842-1836-5_4_Figbb_HTML.gifpeek()方法主要用于调试目的。它有助于我们理解元素在管道中是如何转换的。不要在产品代码中使用它。

过滤收藏

认证目标
使用 lambda 表达式筛选集合

Stream接口中的filter()方法用于删除不符合给定条件的元素。这里有一个简单的例子,它使用了Streamfilter()方法来移除奇数整数(列表 4-20 )。

Listing 4-20. EvenNumbers.java

import java.util.stream.IntStream;

class EvenNumbers {

public static void main(String []args) {

IntStream.rangeClosed(0, 10)

.filter(i -> (i % 2) == 0)

.forEach(System.out::println);

}

}

这个程序打印

0

2

4

6

8

10

在这个例子中,我们使用了IntStream类 Stream 对ints的专门化之一。rangeClosed(startValue, endValueInclusiveOfEnd)方法生成一个从startValueendValueInclusiveOfEnd的整数序列。这里,rangeClosed(0, 10)产生整数值 0,1,2,…,9,10(注意值 10)。还有一个类似的方法range(startValue, endValueExclusiveOfEnd),生成一个从startValue开始直到(不包括)endValueExclusiveOfEnd的整数序列。

根据这个rangeClosed()方法的结果,我们对其应用filter()方法。下面是filter()的签名方法:

IntStream filter(IntPredicate predicate)

filter()方法应用给定的谓词来确定该元素是应该作为返回流的一部分被包含还是被删除(即过滤)。java.util.function.IntPredicate函数式接口具有如下签名的函数:

boolean test(int value);

这里我们传递一个 lambda 函数i -> (i % 2) == 0来匹配返回一个boolean值的IntPredicate函数式接口。如果当前正在处理的元素返回 true(即,在这种情况下,它是偶数),那么它是流的一部分,或者它被消除。

或者,可以用IntPredicate functional interface 的函数类型定义一个函数,并将其传递给 filter。

// you can define this static function within EvenNumbers class

public static boolean isEven(int i) {

return (i % 2) == 0;

}

现在,不用将 lambda 函数传递给filter()方法,而是传递一个方法引用,如在filter(EvenNumbers::isEven)中。

通常map()filter()方法一起使用。例如,下面的程序打印偶数的平方(列出 4-21 )。

Listing 4-21. EvenSquares.java

import java.util.stream.IntStream;

class EvenSquares {

public static void main(String []args) {

IntStream.rangeClosed(0, 10)

.map(i -> i * i)

.filter(i -> (i % 2) == 0)

.forEach(System.out::println);

}

}

这个程序打印

0

4

16

36

64

100

但是,这段代码不必计算奇数的平方(奇数的平方总是奇数)。因此,我们可以改变mapfilter操作的顺序,以消除那些不必要的计算:

IntStream.rangeClosed(0, 10)

.filter(i -> (i % 2) == 0)

.map(i -> i * i)       // call map AFTER calling filter

.forEach(System.out::println);

这个输出是一样的。这个简单的例子展示了如何在不改变行为的情况下改变中间操作的顺序。

终端操作

您需要在管道的末端提供一个终端操作。这个终端操作通常会产生一个结果,比如在一个IntStream上调用方法sum()min()max()average()。终端操作也可以执行其他动作,比如用reduce()collect()方法累加元素,或者只是执行一个动作,就像调用forEach()方法一样。表 4-6 列出了Stream<T>中一些重要的终端操作。

表 4-6。

Important Terminal Operations in the Stream Interface

方法简短描述
void forEach(Consumer<? super T> action)为流中的每个元素调用action
Object[] toArray()返回一个在流中有柠檬的Object数组。
Optional<T> min(Comparator<? super T> compare)返回流中的最小值(使用给定的compare函数比较对象)。
Optional<T> max(Comparator<? super T> compare)返回流中的最大值(使用给定的compare函数比较对象)。
long count()返回流中元素的数量。

有许多重要的终端操作,如reduce()collect()findFirst()findAny()anyMatch()allMatch()noneMatch()方法。我们将在后面关于流 API 的第六章的中讨论这些方法(以及本表中提到的Optional<T>)。此外,IntStreamLongStreamDoubleStream具有诸如sum()min()max()average()的方法,分别对int s、long s 和double s 的流进行操作。

下面是一个在Stream接口中使用toArray()方法的例子:

Object [] words = Pattern.``compile

System.``out``.println(Arrays.``stream``(words).mapToInt(str -> Integer.``valueOf

该程序打印:

15

在这个程序中,我们有一个字符串“1 2 3 4 5”并且splitAsStream()返回一个流Strings。我们已经将Strings流转换成一个名为wordsObject数组;然后,我们使用Arrays.stream(words)将数组转换回流(只是为了说明如何将流转换成数组,然后再转换回来!).现在,我们将每个Object条目映射到一个String中,然后映射到一个整数值。最后,我们调用终端操作sum()来获得整数之和为 15。

一旦一个终端操作完成,它所操作的流就被认为是“消耗的”。如果你试图再次“使用”这个流,你将得到一个IllegalStateException(列表 4-22 )。

Listing 4-22. StreamReuse.java

import java.util.stream.IntStream;

public class StreamReuse {

public static void main(String []args) {

IntStream chars =  "bookkeep".chars();

System.out.println(chars.count());

chars.distinct().sorted().forEach(ch -> System.out.printf("%c ", ch));

}

}

变量chars指向从字符串“bookkeep”创建的流。当我们得到chars.count()时,流就被“消耗”了。为什么?因为count()方法是一种终端操作。因为我们试图在下一条语句中再次使用这个流,所以这个程序会因为抛出IllegalStateException而崩溃。

摘要

让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。

创建和使用泛型类

  • 泛型将确保任何添加除指定类型之外的类型元素的尝试都会在编译时被捕获。因此,泛型提供了具有类型安全的泛型实现。
  • Java 7 引入了菱形语法,其中类型参数(在新操作符和类名之后)可以省略。编译器将从类型声明中推断类型。
  • 泛型不是协变的。也就是说,子类型对泛型不起作用;不能将派生的泛型类型参数赋给基类型参数。
  • 避免混合原始类型和泛型类型。在其他情况下,请手动确保类型安全。
  • <?>指定了泛型中的未知类型,被称为通配符。例如,List<?>指的是一列未知数。

创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象

  • 术语集合、集合和集合是不同的。Collectionjava.util.Collection<E>—是集合层次结构中的根接口。Collectionsjava.util.Collections—是一个只包含静态方法的实用程序类。通用术语集合指的是像映射、堆栈和队列这样的容器。
  • 请记住,您不能向由Arrays.asList()方法返回的List添加或移除元素。但是,您可以对返回的List中的元素进行更改,对该List所做的更改会反映到数组中。
  • 一个HashSet用于快速插入和检索元素;它不维护它所保存的元素的任何排序顺序。一个TreeSet按照排序的顺序存储元素(它实现了SortedSet接口)。
  • A HashMap内部使用哈希表数据结构。在HashMap中,搜索(或查找元素)是一种快速操作。然而,HashMap既不会记住您插入元素的顺序,也不会让元素保持任何排序的顺序。与HashMap不同的是,TreeMap保持元素有序(即,按键排序)。因此,搜索或插入比HashMap稍慢。
  • Deque(双端队列)是一种允许你从两端插入和移除元素的数据结构。Deque接口的具体实现有三种:LinkedListArrayDequeLinkedBlockingDeque
  • ArrayListArrayDeque的区别在于,你可以使用索引在数组列表中的任何地方添加元素;但是,只能在数组队列的前端或末端添加元素。

使用 java.util.Comparator 和 java.lang.Comparable 接口

  • 在自然顺序可能的情况下,为您的类实现Comparable接口。如果您想比较自然顺序之外的对象,或者如果您的类类型没有自然顺序,那么创建实现Comparator接口的单独的类。此外,如果你有多种选择来决定顺序,那么就使用Comparator界面。

集合流和过滤器

  • Java 8 中引入的java.util.stream包中提供了新的流 API。这个包中的主要类型是Stream<T>接口,它是对象引用的流。IntStreamLongStreamDoubleStream分别是原始类型intlongdouble的流。
  • 流是一系列元素。在 Java 8 中,Collection接口增加了stream()parallelStream()方法,从这两个方法中可以分别获得顺序流和并行流。

使用流和列表的 forEach 方法进行迭代

  • 在 Java 8 中,我们正从外部迭代转向内部迭代。这是 Java 8 函数式编程方法的一个重大变化。
  • 接口StreamIterable定义了forEach()方法。forEach()方法支持内部迭代。

描述流接口和流管道

  • 流操作可以“链接”在一起,形成一个称为“流管道”的管道。
  • 流管道有开始、中间和结束:源(创建流)、中间操作(由可以链接在一起的可选操作组成)和终端操作(产生结果)。
  • 终端操作可以产生一个结果,累积流元素,或者只是执行一个动作。
  • 一个流只能使用一次。任何重用流的尝试(例如,通过调用中间或终端操作)都将导致抛出一个IllegalStateException

使用 lambda 表达式筛选集合

  • Stream接口中的filter()方法用于删除不符合给定条件的元素。

对流使用方法引用

  • 当 lambda 表达式只是路由给定的参数时,您可以使用方法引用来代替。
  • 因为方法引用是传递参数的一种方式,所以使用它们通常比使用它们的等效 lambda 表达式更方便(因为这会产生更简洁的代码)。

Question TimeChoose the correct option based on this program: import java.util.*; class UtilitiesTest {      public static void main(String []args) {          List<int> intList = new ArrayList<>();          intList.add(10);          intList.add(20);          System.out.println("The list is: " + intList);      } } It prints the following: The list is: [10, 20]   It prints the following: The list is: [20, 10]   It results in a compiler error   It results in a runtime exception     Choose the correct option based on this program: import java.util.*; class UtilitiesTest {      public static void main(String []args) {          List<Integer> intList = new LinkedList<>();          List<Double> dblList = new LinkedList<>();          System.out.println("First type: " + intList.getClass());          System.out.println("Second type:" + dblList.getClass());      } } It prints the following: First type: class java.util.LinkedList Second type:class java.util.LinkedList   It prints the following: First type: class java.util.LinkedList Second type:class java.util.LinkedList   It results in a compiler error   It results in a runtime exception     Choose the correct option based on this program: import java.util.Arrays; class DefaultSorter {     public static void main(String[] args) {          String[] brics = {"Brazil", "Russia", "India", "China"};          Arrays.sort(brics, null);    // LINE A          for(String country : brics) {              System.out.print(country + " ");          }     } } This program will result in a compiler error in line marked with comment LINE A   When executed, the program prints the following: Brazil Russia India China   When executed, the program prints the following: Brazil China India Russia   When executed, the program prints the following: Russia India China Brazil   When executed, the program throws a runtime exception of NullPointerException when executing the line marked with comment LINE A   When executed, the program throws a runtime exception of InvalidComparatorException when executing the line marked with comment LINE A     Choose the correct option based on this code segment: "abracadabra".chars().distinct().peek(ch -> System. out .printf("%c ", ch)).sorted(); It prints: “a b c d r”   It prints: “a b r c d”   It crashes by throwing a java.util.IllegalFormatConversionException   This program terminates normally without printing any output in the console     Choose the correct option based on this code segment: IntStream.rangeClosed(1, 1).forEach(System.out::println); It prints: 1   It crashes by throwing a java.lang.UnsupportedOperationException   It crashes by throwing a java.lang.StackOverflowError   It crashes by throwing a java.lang.IllegalArgumentException   This program terminates normally without printing any output in the console     Choose the correct option based on this program: import java.util.stream.DoubleStream; public class DoubleUse {     public static void main(String []args) {         DoubleStream nums = DoubleStream. of (1.0, 2.0, 3.0).map(i -> -i); // #1         System. out .printf("count = %d, sum = %f", nums.count(), nums.sum());     } } This program results in a compiler error in the line marked with comment #1   This program prints: "count = 3, sum = -6.000000"   This program crashes by throwing a java.util.IllegalFormatConversionException   This program crashes by throwing a java.lang.IllegalStateException     Choose the correct option based on this program: class Consonants {      private static boolean removeVowels(int c) {              switch(c) {              case 'a': case 'e': case 'i': case 'o': case 'u': return true;              }              return false;      }      public static void main(String []args) {              "avada kedavra".chars()                      .filter(Consonants::removeVovels)                      .forEach(ch -> System.out.printf("%c", ch));      } } This program results in a compiler error   This program prints: "aaaeaa"   This program prints: "vd kdvr"   This program prints: "avada kedavra"   This program crashes by throwing a java.util.IllegalFormatConversionException   This program crashes by throwing a java.lang.IllegalStateException     Choose the correct option based on this program: import java.util.*; class DequeTest {     public static void main(String []args) {         Deque<Integer> deque = new ArrayDeque<>();         deque.addAll(Arrays.asList(1, 2, 3, 4, 5));         System.out.println("The removed element is: " + deque.remove()); // ERROR?     } } When executed, this program prints the following: “The removed element is: 5”   When executed, this program prints the following: “The removed element is: 1”   When compiled, the program results in a compiler error of “remove() returns void” for the line marked with the comment ERROR.   When executed, this program throws InvalidOperationException.     Determine the behavior of this program: import java.io.*; class LastError<T> {      private T lastError;      void setError(T t){          lastError = t;          System.out.println("LastError: setError");      } } class StrLastError<S extends CharSequence> extends LastError<String>{      public StrLastError(S s) {      }      void setError(S s){         System.out.println("StrLastError: setError");      } } class Test {      public static void main(String []args) {         StrLastError<String> err = new StrLastError<String>("Error");         err.setError("Last error");      } } It prints the following: StrLastError: setError   It prints the following: LastError: setError   It results in a compilation error   It results in a runtime exception    

答案:

C. It results in a compiler error You cannot specify primitive types along with generics, so List<int> needs to be changed to List<Integer>.   A. It prints the following: First type: class java.util.LinkedList Second type:class java.util.LinkedList Due to type erasure, after compilation both types are treated as same LinkedList type.   C. When executed, the program prints the following: Brazil China India Russia When null is passed as a second argument to the Arrays.sort() method, it means that the default Comparable (i.e., natural ordering for the elements) should be used. The default Comparator results in sorting the elements in ascending order. The program does not result in a NullPointerException or any other exceptions or a compiler error.   D. This program terminates normally without printing any output in the console A stream pipeline is lazily evaluated. Since there is no terminal operation provided (such as count, forEach, reduce, or collect), this pipeline is not evaluated and hence the peek does not print any output to the console.   A. It prints: 1 The rangeClosed(startValue, endValueInclusiveOfEnd) method generates a sequence of integers starting with startValue till (and inclusive of) endValueInclusiveOfEnd. Hence the call IntStream.rangeClosed(1, 1) results in a stream with only one element and the forEach() method prints that value.   D. This program crashes by throwing a java.lang.IllegalStateException A stream is considered “consumed” when a terminal operation is called on that stream. The methods count() and sum() are terminal operations in DoubleStream. When this code calls nums.count(), the underlying stream is already “consumed”. When the printf calls nums.sum(), this program results in throwing java.lang.IllegalStateException due to the attempt to use a consumed stream.   B. This program prints: "aaaeaa" Because the Consonants::removeVowels returns true when there is a vowel passed, only those characters are retained in the stream by the filter method. Hence, this program prints “aaaeaa”.   B. When executed, this program prints the following: “The removed element is: 1”. The remove() method is equivalent to the removeFirst() method, which removes the first element (head of the queue) of the Deque object.   C. It results in a compilation error It looks like the setError() method in StrLastError is overriding setError() in the LastError class. However, it is not the case. At the time of compilation, the knowledge of type S is not available. Therefore, the compiler records the signatures of these two methods as setError(String) in superclass and setError(S_extends_CharSequence) in subclass—treating them as overloaded methods (not overridden). In this case, when the call to setError() is found, the compiler finds both the overloaded methods matching, resulting in the ambiguous method call error. Here is the error message Test.java:22: error: reference to setError is ambiguous, both method setError(T) in LastError and method setError(S) in StrLastError match                 err.setError("Last error");                    ^ where T and S are type-variables: T extends Object declared in class LastError. S extends CharSequence declared in class StrLastError.