目录

本部分接上文,涵盖了嵌套类以及枚举类型部分内容

类和对象

嵌套类

Java 编程语言允许您在另一个类中定义一个类。这样的类被称为嵌套类(nested class),并在这里说明:

class OuterClass {
    ...
    class NestedClass {
        ...
    }
}

术语(Terminology):嵌套类分为两类:静态和非静态。 被声明为静态 (static)的嵌套类称为静态嵌套类(static nested classe)。 非静态(non-static)嵌套类称为内部类(inner classe)。

class OuterClass {
    ...
    static class StaticNestedClass {
        ...
    }
    class InnerClass {
        ...
    }
}

嵌套类是其封闭类(enclosing class)的成员。非静态嵌套类(内部类)可以访问封闭类的其他成员,即使它们被声明为私有。静态嵌套类不能访问封闭类的其他成员。作为 OuterClass 的成员,可以将嵌套类声明为 private,public,protected 或 package private。(回想一下,只能将外部类声明为 public 或 package private。)

为什么使用嵌套类?

使用嵌套类的强有力的理由(compelling reason)包括:

  • 这是一种逻辑上分组类(logically grouping classes)的方法,其只用于一个地方:如果一个类只对另一个类有用,那么将其嵌入到该类中并将它们保持在一起是合乎逻辑的。嵌套这样的「帮助类(helper classes)」使得它们的包更加简化(streamlined)。
  • 它增加封装(encapsulation):考虑两个顶级类,A 和 B,其中 B 需要访问 A 应该声明为私有的成员。通过在类 A 中隐藏类 B,可以将 A 的成员声明为私有,并且 B 可以访问它们。另外,B 本身可以从外界隐藏起来。
  • 它可以导致更易于读取和维护的代码(more readable and naintainable code):在顶层类中嵌套小类可以使代码更接近它的使用位置。

静态嵌套类

与类方法和变量一样,静态嵌套类与其外部类相关联。像静态类方法一样,静态嵌套类不能直接引用其封闭类中定义的实例变量或方法:它只能通过对象引用来使用它们。

注意:静态嵌套类与其外部类(和其他类)的实例成员交互,就像任何其他顶级类一样。 实际上,静态嵌套类在行为上是嵌套在另一个顶级类中的顶级类,以便于打包。

静态嵌套类可以使用包含的类名来访问:

OuterClass.StaticNestedClass

例如,要为静态嵌套类创建一个对象,请使用以下语法:

OuterClass.StaticNestedClass nestedObject =
     new OuterClass.StaticNestedClass();

内部类

与实例方法和变量一样,内部类与其包含的类的实例关联,并可以直接访问该对象的方法和字段。另外,因为内部类与一个实例相关联,所以它不能自己定义任何静态成员。

作为内部类实例的对象存在于外部类的实例中。考虑以下类:

class OuterClass {
    ...
     class InnerClass {
        ...
    }
}

InnerClass 的实例只能存在于 OuterClass 的一个实例中,并且可以直接访问其封闭实例的方法和字段。

要实例化一个内部类,你必须首先实例化外部类。 然后,使用以下语法在外部对象内创建内部对象:

OuterClass.InnerClass innerObject = outerObject.new InnerClass();

有两种特殊的内部类:本地类(local classes)和匿名类(anonymous classes)。

Shadowing

如果特定范围(如内部类或方法定义)中的类型声明(例如成员变量或参数名称)与封闭范围中的另一个声明具有相同的名称,则该声明将 shadow 封闭范围的声明。你不能仅仅通过它的名字引用一个 shadowed 声明。以下示例 ShadowTest 演示了这一点:

public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

以下是这个例子的输出:

x = 23
this.x = 1
ShadowTest.this.x = 0

此示例定义了三个名为 x 的变量:类 ShadowTest 的成员变量,内部类 FirstLevel 的成员变量以及 methodInFirstLevel 方法中的参数。定义为方法 methodInFirstLevel 的参数的变量 x 会 shadow 内部类 FirstLevel 的变量。因此,当在方法 methodInFirstLevel 中使用变量 x 时,它将引用方法参数。要引用内部类 FirstLevel 的成员变量,请使用关键字 this 来表示封闭作用域:

System.out.println("this.x = " + this.x);

通过它们所属的类名引用包含更大范围的成员变量。例如,以下语句从 methodInFirstLevel 方法访问类 ShadowTest 的成员变量:

System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

序列化

内部类的序列化(serialization),包括本地和匿名类,强烈不鼓励。当 Java 编译器编译某些结构(如内部类)时,会创建合成结构(synthetic constructs);这些是在源代码中没有相应构造的类,方法,字段和其他构造。合成结构使 Java 编译器能够在不改变 JVM 的情况下实现新的 Java 语言特性。但是,合成结构在不同的 Java 编译器实现中可能会有所不同,这意味着 .class 文件在不同的实现中也会有所不同。因此,如果序列化一个内部类,然后使用不同的 JRE 实现进行反序列化(deserialize),则可能会遇到兼容性问题。

本地类

本地类(local classes)是在块中定义的类,它是平衡花括号之间的一组零个或多个语句。您通常会在方法的主体中找到定义的本地类。

声明本地类

您可以在任何块中定义本地类。例如,您可以在方法主体,for 循环或 if 子句中定义本地类。

public class LocalClassExample {
  
    static String regularExpression = "[^0-9]";
  
    public static void validatePhoneNumber(
        String phoneNumber1, String phoneNumber2) {
      
        final int numberLength = 10;
        
        // Valid in JDK 8 and later:
       
        // int numberLength = 10;
       
        class PhoneNumber {
            
            String formattedPhoneNumber = null;

            PhoneNumber(String phoneNumber){
                // numberLength = 7;
                String currentNumber = phoneNumber.replaceAll(
                  regularExpression, "");
                if (currentNumber.length() == numberLength)
                    formattedPhoneNumber = currentNumber;
                else
                    formattedPhoneNumber = null;
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }
            
            // Valid in JDK 8 and later:

//            public void printOriginalNumbers() {
//                System.out.println("Original numbers are " + phoneNumber1 +
//                    " and " + phoneNumber2);
//            }
        }

        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
        
        // Valid in JDK 8 and later:

//        myNumber1.printOriginalNumbers();

        if (myNumber1.getNumber() == null) 
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is " + myNumber2.getNumber());

    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }
}

访问封闭类的成员

一个本地类可以访问其封闭类的成员。在前面的示例中,PhoneNumber 构造函数访问成员 LocalClassExample.regularExpression。

另外,本地类可以访问局部变量。但是,本地类只能访问声明为 final 的局部变量。当本地类访问本地变量或封闭块的参数时,它会捕获该变量或参数。 例如,PhoneNumber构造函数可以访问局部变量numberLength,因为它被声明为final; numberLength是一个被捕获的变量。

但是,从 Java SE 8 开始,本地类可以访问封闭块的局部变量和参数,这些变量和参数是final 的或有效 final 的(effectively final)。初始化后永远不会改变其值的变量或参数实际上是有效 final的(effectively final)。例如,假设变量 numberLength 没有声明为 final,并且您在 PhoneNumber 构造函数中添加突出显示的赋值语句,以将有效电话号码的长度更改为 7 位数字:

PhoneNumber(String phoneNumber) {
    numberLength = 7;
    String currentNumber = phoneNumber.replaceAll(
        regularExpression, "");
    if (currentNumber.length() == numberLength)
        formattedPhoneNumber = currentNumber;
    else
        formattedPhoneNumber = null;
}

由于这个赋值语句,变量 numberLength 不再为有效 final。因此,Java 编译器会生成类似于“内部类引用的局部变量必须是 final 或有效 final”的错误消息,其中内部类PhoneNumber 尝试访问 numberLength 变量:

if (currentNumber.length() == numberLength)

从 Java SE 8 开始,如果在方法中声明本地类,则可以访问方法的参数。例如,您可以在PhoneNumber 本地类中定义以下方法:

public void printOriginalNumbers() {
    System.out.println("Original numbers are " + phoneNumber1 +
        " and " + phoneNumber2);
}

方法 printOriginalNumbers 访问方法 validatePhoneNumber 的参数 phoneNumber1 和 phoneNumber2。

Shadowing 和本地类

在一个本地类中的类型声明(比如一个变量) shadow 在封闭范围中具有相同的名字的声明。

本地类与内部类相似

本地类与内部类相似,因为它们不能定义或声明任何静态成员。静态方法中的本地类(例如在静态方法 validatePhoneNumber 中定义的类 PhoneNumber )只能引用封闭类的静态成员。例如,如果您没有将成员变量 regularExpression 定义为 static,那么 Java 编译器会生成类似于“不能从静态上下文中引用非静态变量 regularExpression”的错误。

本地类是非静态的,因为它们可以访问封闭块的实例成员。因此,它们不能包含大多数的静态声明。

你不能在块中声明一个接口; 接口本质上是静态的。例如,以下代码片段不会编译,因为接口 HelloThere 是在方法 greetInEnglish 的主体内部定义的:

public void greetInEnglish() {
    interface HelloThere {
       public void greet();
    }
    class EnglishHelloThere implements HelloThere {
        public void greet() {
            System.out.println("Hello " + name);
        }
    }
    HelloThere myGreeting = new EnglishHelloThere();
    myGreeting.greet();
}

您不能在本地类中声明静态初始值设定项或成员接口。下面的代码片段不会编译,因为方法EnglishGoodbye.sayGoodbye 被声明为静态的。当遇到这个方法定义时,编译器生成一个类似于“修饰符 static 只允许在常量变量声明中使用”的错误:

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static void sayGoodbye() {
            System.out.println("Bye bye");
        }
    }
    EnglishGoodbye.sayGoodbye();
}

本地类可以有静态成员,只要它们是常量变量(constant variable)。(常量变量是基本类型 - primitive type 或声明为 final 并使用编译时常量表达式进行初始化的字符串类型的变量。编译时常量表达式 - compile-time constant expression 通常是可在编译时计算的字符串或算术表达式 - arithmetic expression。)下面的代码摘录编译,因为静态成员 EnglishGoodbye.farewell 是一个常量变量:

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static final String farewell = "Bye bye";
        public void sayGoodbye() {
            System.out.println(farewell);
        }
    }
    EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
    myEnglishGoodbye.sayGoodbye();
}

匿名类

匿名类使您能够使代码更加简洁。它们使您能够同时声明和实例化一个类。除了没有名字之外,它们就像本地的课程。如果您只需要使用一次本地类,请使用它们。

声明匿名类

虽然本地类是类声明,匿名类(anonymous classes)是表达式,这意味着你在另一个表达式中定义类。下面的示例 HelloWorldAnonymousClasses 在局部变量 frenchGreeting 和 spanishGreeting 的初始化语句中使用匿名类,但使用本地类来初始化变量 englishGreeting:

public class HelloWorldAnonymousClasses {
  
    interface HelloWorld {
        public void greet();
        public void greetSomeone(String someone);
    }
  
    public void sayHello() {
        
        class EnglishGreeting implements HelloWorld {
            String name = "world";
            public void greet() {
                greetSomeone("world");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hello " + name);
            }
        }
      
        HelloWorld englishGreeting = new EnglishGreeting();
        
        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };
        
        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hola, " + name);
            }
        };
        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String... args) {
        HelloWorldAnonymousClasses myApp =
            new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }            
}

匿名类语法

如前所述,匿名类是一个表达式。匿名类表达式的语法就像构造函数的调用一样,除了在代码块中包含一个类定义。

考虑 frenchGreeting 对象的实例:

HelloWorld frenchGreeting = new HelloWorld() {
    String name = "tout le monde";
    public void greet() {
        greetSomeone("tout le monde");
    }
    public void greetSomeone(String someone) {
        name = someone;
        System.out.println("Salut " + name);
    }
};

匿名类表达式由以下内容组成:

  • new 运算符
  • 要实现的接口的名称或要扩展的类。在这个例子中,匿名类正在实现接口 HelloWorld。
  • 包含构造函数参数的括号,就像普通的类实例创建表达式一样。注意:当你实现一个接口时,没有构造函数,所以你使用一对空括号,就像这个例子。
  • 主体,这是一个类声明主体。更具体地说,在主体中,方法声明是允许的,但语句不是。

因为匿名类定义是一个表达式,所以它必须是语句的一部分。在这个例子中,匿名类表达式是实例化 frenchGreeting 对象的语句的一部分。(这解释了为什么在右大括号之后有一个分号。)

访问封闭作用域的局部变量,以及声明和访问匿名类的成员

像本地类一样,匿名类可以捕获变量;它们对封闭作用域的局部变量具有相同的访问权限:

  • 匿名类可以访问其封闭类的成员。
  • 匿名类不能访问其封闭范围中没有被声明为 final 或有效 final 的局部变量。
  • 像嵌套类一样,匿名类中的类型声明(例如变量)会隐藏(shadow)封闭范围中具有相同名称的任何其他声明。

匿名类对其成员和本地类也有相同的限制:

  • 您不能在匿名类中声明静态初始值设定项或成员接口。
  • 匿名类可以有静态成员,只要它们是常量变量。

请注意,您可以在匿名类中声明以下内容:

  • 字段
  • 额外的方法(即使他们没有实现任何超类型 - supertype 的方法)
  • 实例初始值设定项
  • 本地类

但是,您不能在匿名类中声明构造函数。

匿名类的例子

匿名类通常用于图形用户界面(GUI)应用程序。

考虑 JavaFX 示例 HelloWorld.java(来自 Hello World 部分,JavaFX 入门的 JavaFX 风格)。本示例创建一个包含 Say’Hello World’ 按钮的框架。

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
 
public class HelloWorld extends Application {
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World!");
        Button btn = new Button();
        btn.setText("Say 'Hello World'");
        btn.setOnAction(new EventHandler<ActionEvent>() {
 
            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });
        
        StackPane root = new StackPane();
        root.getChildren().add(btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
    }
}

在这个例子中,方法调用 btn.setOnAction 指定当选择 Say’Hello World’ 按钮时发生的事情。此方法需要 EventHandler 类型的对象。EventHandler 接口只包含一个方法,句柄。这个例子不是用新类实现这个方法,而是使用一个匿名类表达式。注意这个表达式是传递给 btn.setOnAction 方法的参数。

因为 EventHandler 接口只包含一个方法,所以可以使用 lambda 表达式而不是匿名类表达式。

匿名类对于实现包含两个或更多方法的接口是理想的。以下 JavaFX 示例来自 UI 控件的自定义部分。突出显示的代码将创建一个仅接受数值的文本字段。它通过覆盖从TextInputControl 类继承的 replaceText 和 replaceSelection 方法,重新定义了带有匿名类的 TextField 类的默认实现。

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class CustomTextFieldSample extends Application {
    
    final static Label label = new Label();
 
    @Override
    public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 300, 150);
        stage.setScene(scene);
        stage.setTitle("Text Field Sample");
 
        GridPane grid = new GridPane();
        grid.setPadding(new Insets(10, 10, 10, 10));
        grid.setVgap(5);
        grid.setHgap(5);
 
        scene.setRoot(grid);
        final Label dollar = new Label("$");
        GridPane.setConstraints(dollar, 0, 0);
        grid.getChildren().add(dollar);
        
        final TextField sum = new TextField() {
            @Override
            public void replaceText(int start, int end, String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceText(start, end, text);                     
                }
                label.setText("Enter a numeric value");
            }
 
            @Override
            public void replaceSelection(String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceSelection(text);
                }
            }
        };
 
        sum.setPromptText("Enter the total");
        sum.setPrefColumnCount(10);
        GridPane.setConstraints(sum, 1, 0);
        grid.getChildren().add(sum);
        
        Button submit = new Button("Submit");
        GridPane.setConstraints(submit, 2, 0);
        grid.getChildren().add(submit);
        
        submit.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                label.setText(null);
            }
        });
        
        GridPane.setConstraints(label, 0, 1);
        GridPane.setColumnSpan(label, 3);
        grid.getChildren().add(label);
        
        scene.setRoot(grid);
        stage.show();
    }
 
    public static void main(String[] args) {
        launch(args);
    }
}

Lambda 表达式

匿名类的一个问题是,如果你的匿名类的实现非常简单,比如只包含一个方法的接口,那么匿名类的语法可能看起来很笨重和不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如有人单击某个按钮时应执行的操作。 Lambda 表达式使您可以执行此操作,将功能(functionality)视为方法参数,或将代码视为数据。

前一节「匿名类」向您展示了如何实现一个基类,而不用指定名称。尽管这通常比命名类更简洁,但对于只有一个方法的类,即使是一个匿名类似乎也有点过于繁琐。使用Lambda表达式可以更简洁地表达单方法类(single-method classes)的实例。

Lambda表达式的理想用例(ideal use case)

假设您正在创建一个社交网络应用程序。 您希望创建一项功能,使管理员能够对满足特定条件的社交网络应用程序的成员执行任何类型的操作,例如发送消息。 下表详细描述了这个用例:

Field Description
Name Perform action on selected members
Primary Actor Administrator
Preconditions Administrator is logged in to the system.
Postconditions Action is performed only on members that fit the specified criteria.
Main Success Scenario 1. Administrator specifies criteria of members on which to perform a certain action.
2. Administrator specifies an action to perform on those selected members.
3. Administrator selects the Submit button.
4. The system finds all members that match the specified criteria.
5. The system performs the specified action on all matching members.
Extensions 1a. Administrator has an option to preview those members who match the specified criteria before he or she specifies the action to be performed or before selecting the Submit button.
Frequency of Occurrence Many times during the day.

假设这个社交网络应用程序的成员由以下 Person 类表示:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

假设您的社交网络应用程序的成员存储在 List 实例中。

本节从这个用例的一个天真的方法开始。它利用本地和匿名类的方法进行改进,然后使用 lambda 表达式以高效简洁的方式完成。在 RosterTest 示例中查找本节中描述的代码摘录。

方法1:创建搜索匹配一个特征的成员的方法

一种简单的方法是创建几种方法; 每种方法都搜索匹配一个特征(例如性别或年龄)的成员。以下方法打印大于特定年龄的成员:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

注:列表是一个有序的集合(collection)。集合是将多个元素分组为一个单元的对象。集合用于存储,检索,操作和传输聚合数据。

这种方法可能会使您的应用程序变得脆弱,这是由于引入更新(例如较新的数据类型)导致应用程序无法运行的可能性。假设你升级你的应用程序并改变 Person 类的结构,使其包含不同的成员变量; 也许类用不同的数据类型或算法来记录和测量年龄。你将不得不重写很多你的 API 来适应这个变化。另外,这种方法是不必要的限制;如果你想打印年龄小于某个年龄的成员,例如?

方法2:创建更多的通用搜索方法

下面的方法比printPersonsOlderThan更通用; 它会在指定的年龄范围内打印成员:

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

如果您想打印指定性别的成员,或指定的性别和年龄范围的组合,该怎么办?如果您决定更改Person类并添加关系状态或地理位置等其他属性,该怎么办? 尽管这种方法比printPersonsOlderThan更通用,但试图为每个可能的搜索查询创建一个单独的方法仍然会导致代码变得脆弱。 您也可以将指定您要在其他类中搜索的条件的代码分开。

方法3:在本地类中指定搜索条件代码

以下方法打印与您指定的搜索条件匹配的成员:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

此方法通过调用方法tester.test来检查List参数名单中包含的每个Person实例是否满足CheckPerson参数测试程序中指定的搜索条件。 如果方法tester.test返回一个真值,则在Person实例上调用printPersons方法。

要指定搜索条件,请执行CheckPerson界面:

interface CheckPerson {
    boolean test(Person p);
}

以下类通过指定方法测试的实现来实现CheckPerson接口。 此方法筛选符合美国“选择性服务”条件的成员:如果其Person参数为男性,年龄在18到25之间,则返回真值:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

要使用这个类,你创建一个新的实例并调用printPersons方法:

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

虽然这种方法不那么脆弱,但是如果更改Person的结构,则不必重写方法,但您仍然有其他代码:计划在应用程序中执行的每个搜索的新接口和本地类。 由于CheckPersonEligibleForSelectiveService实现了一个接口,因此可以使用匿名类而不是本地类,并绕过为每个搜索声明新类的需要。

方法4:在匿名类中指定搜索条件代码

接下来调用printPersons方法的一个参数是一个匿名类,它过滤了在美国有资格选择性服务的成员:那些男性和年龄在18到25岁之间的成员:

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

这种方法减少了所需的代码量,因为您不必为每个要执行的搜索创建一个新的类。 但是,考虑到CheckPerson接口只包含一个方法,匿名类的语法是庞大的。 在这种情况下,您可以使用lambda表达式而不是匿名类,如下一节所述。

方法5:使用Lambda表达式指定搜索条件代码

CheckPerson界面是一个功能接口(functional interface)。 功能接口是只包含一个抽象方法的任何接口。 (一个功能接口可能包含一个或多个默认方法或静态方法。)因为一个功能接口只包含一个抽象方法,所以在实现时可以省略该方法的名称。 为此,不要使用匿名类表达式,而要使用lambda表达式,该表达式在以下方法调用中突出显示:

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

您可以使用标准的功能接口来代替CheckPerson接口,从而进一步减少所需的代码量。

方法6:使用带有Lambda表达式的标准函数接口

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。 这是一个功能接口,因为它只包含一个抽象方法。 该方法接受一个参数并返回一个布尔值。 该方法非常简单,可能不值得在应用程序中定义一个。 因此,JDK 定义了几个标准的功能接口,您可以在java.util.function包中找到它们。

例如,您可以使用Predicate接口代替CheckPerson。 此接口包含方法boolean test(T t)

interface Predicate<T> {
    boolean test(T t);
}

Predicate接口是通用接口的一个例子。泛型(Generic)类型(如泛型接口)在尖括号(<>)内指定一个或多个类型参数。 此接口只包含一个类型参数T.当您声明或实例化具有实际类型参数的泛型类型时,您具有参数化(parameterized)类型。 例如,参数化类型Predicate是以下内容:

interface Predicate<Person> {
    boolean test(Person t);
}

此参数化类型包含一个与CheckPerson.boolean test(Person p)具有相同返回类型和参数的方法。 因此,您可以使用Predicate代替CheckPerson,如下面的方法所示:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

因此,以下方法调用与在「方法3:在本地类中指定搜索条件代码」中调用printPersons时相同,以获取有资格进行选择性服务的成员:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

这不是在这个方法中使用lambda表达式的唯一可能的地方。 以下方法建议使用lambda表达式的其他方法。

方法7:在整个应用程序中使用Lambda表达式

重新考虑printPersonsWithPredicate方法,看看你还可以使用lambda表达式:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

此方法检查List参数名单中包含的每个Person实例是否满足Predicate参数测试程序中指定的条件。如果Person实例确实符合由tester指定的条件,则在Person实例上调用printPersron方法。

您可以指定一个不同的操作来执行那些满足由测试程序指定的条件的Person实例,而不是调用printPerson方法。您可以使用lambda表达式来指定此操作。假设你想要一个类似于printPerson的lambda表达式,它需要一个参数(一个Person类型的对象)并返回void。记住,要使用lambda表达式,你需要实现一个功能接口。在这种情况下,您需要一个包含抽象方法的函数接口,该接口可以接受一个Person类型的参数,并返回void。 Consumer接口包含void accept(T t)方法,它具有这些特性。以下方法将调用p.printPerson()替换为调用方法accept的一个Consumer实例:

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

因此,以下方法调用与在「方法3:在本地类中指定搜索条件代码」中调用printPersons时相同,以获取有资格进行选择性服务的成员。 突出显示用于打印成员的lambda表达式:

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

如果你想用你的会员档案做更多的事情,而不是打印出来。 假设你想验证成员的个人资料或检索他们的联系信息? 在这种情况下,您需要一个包含一个返回值的抽象方法的函数接口。 Function<T,R>接口包含方法R apply(T t)。 以下方法检索由参数映射器指定的数据,然后对参数块指定的操作执行操作:

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

以下方法从名单中包含的每个有资格进行选择性服务的成员中检索电子邮件地址,然后将其打印出来:

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);
方法8:更广泛地使用泛型

重新考虑processPersonsWithFunction方法。 以下是它的通用版本,它接受包含任何数据类型元素的集合作为参数:

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

要打印符合选择性服务条件的成员的电子邮件地址,请按如下方式调用processElements方法:

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

此方法调用执行以下操作:

  1. 从收集源获取对象的来源。 在这个例子中,它从集合名单中获得Person对象的来源。 请注意,集合名单(List类型的集合)也是Iterable类型的对象。
  2. 筛选与Predicate对象测试程序匹配的对象。 在此示例中,Predicate对象是一个lambda表达式,用于指定哪些成员有资格参加选择性服务。
  3. 将每个过滤的对象映射到由Function对象映射器指定的值。 在这个例子中,Function对象是一个返回成员的电子邮件地址的lambda表达式。
  4. 对Consumer对象块指定的每个映射对象执行操作。 在这个例子中,Consumer对象是一个lambda表达式,它打印一个字符串,这是Function对象返回的电子邮件地址。

您可以用集合操作替换每个这些操作。

方法9:使用接受Lambda表达式作为参数的聚合操作

以下示例使用汇总操作来打印集合名单中包含的有资格进行选择性服务的成员的电子邮件地址:

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

下表将映射processElements方法执行的每个操作与相应的聚合操作进行映射:

processElements Action Aggregate Operation
Obtain a source of objects Stream stream()
Filter objects that match a Predicate object Stream filter(Predicate<? super T> predicate)
Map objects to another value as specified by a Function object Stream map(Function<? super T,? extends R> mapper)
Perform an action as specified by a Consumer object void forEach(Consumer<? super T> action)

操作过滤器,映射和forEach是聚合(aggregate)操作。 聚合操作处理流中的元素,而不是直接从集合中处理元素(这就是为什么在此示例中调用的第一个方法是流)。 流(stream)是一系列元素。 与集合不同,它不是存储元素的数据结构。 相反,一个流通过一个管道从一个源(例如集合)传送值。 管道(pipeline)是一系列的流操作,在这个例子中是filter-map-forEach。 另外,聚合操作通常接受lambda表达式作为参数,使您能够自定义其行为。

GUI应用程序中的Lambda表达式

要在图形用户界面(GUI)应用程序中处理事件(例如键盘操作,鼠标操作和滚动操作),通常需要创建事件处理程序,这通常涉及实现特定的接口。 事件处理程序接口经常是功能接口; 他们往往只有一个方法。

在JavaFX示例HelloWorld.java(在上一节“匿名类”中讨论)中,可以使用此语句中的lambda表达式替换突出显示的匿名类:

btn.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event) {
        System.out.println("Hello World!");
    }
});

方法调用btn.setOnAction指定当您选择由btn对象表示的按钮时发生的情况。 此方法需要EventHandler类型的对象。 EventHandler接口只包含一个方法void handle(T event)。 这个接口是一个功能接口,所以你可以使用下面突出显示的lambda表达式来代替它:

btn.setOnAction(
  event -> System.out.println("Hello World!")
);
Lambda表达式的语法

一个lambda表达式包含以下内容:

  • 用逗号分隔的括号内的形式参数列表。 CheckPerson.test方法包含一个参数p,它表示Person类的一个实例。

    注意:您可以省略lambda表达式中参数的数据类型。 另外,如果只有一个参数,则可以省略括号。 例如,下面的lambda表达式也是有效的:
p -> p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25
  • 箭头标记,->
  • 一个由单个表达式或语句块组成的主体。 这个例子使用下面的表达式:
p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25

如果指定单个表达式,则Java运行时将评估表达式,然后返回其值。 或者,您可以使用返回语句:

p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}

返回语句不是一个表达式; 在lambda表达式中,必须将括号括在括号({})中。 但是,您不必在大括号中包含void方法调用。 例如,以下是有效的lambda表达式:

email -> System.out.println(email)

请注意,lambda表达式看起来很像一个方法声明; 您可以将lambda表达式视为匿名方法 - 没有名称的方法。

下面的例子Calculator是一个lambda表达式的例子,它有多个形式参数:

public class Calculator {
  
    interface IntegerMath {
        int operation(int a, int b);   
    }
  
    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }
 
    public static void main(String... args) {
    
        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

operateBinary方法对两个整数操作数进行数学运算。 操作本身是由IntegerMath的一个实例指定的。 这个例子定义了两个用lambda表达式 —— 加法和减法的操作。 该示例打印以下内容:

40 + 2 = 42
20 - 10 = 10
访问范围的局部变量

像本地和匿名类一样,lambda表达式可以捕获变量; 它们对封闭范围的本地变量具有相同的访问权限。 但是,与本地和匿名类不同,lambda表达式不存在任何shadowing问题。 Lambda表达式是词汇范围的(lexically scoped)。 这意味着它们不会从超类型继承任何名称,也不会引入新的作用域。 lambda表达式中的声明就像它们在封闭环境中一样被解释。 以下示例LambdaScopeTest演示了这一点:

import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            
            // The following statement causes the compiler to generate
            // the error "local variables referenced from a lambda expression
            // must be final or effectively final" in statement A:
            //
            // x = 99;
            
            Consumer<Integer> myConsumer = (y) -> 
            {
                System.out.println("x = " + x); // Statement A
                System.out.println("y = " + y);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " +
                    LambdaScopeTest.this.x);
            };

            myConsumer.accept(x);

        }
    }

    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

本示例生成以下输出:

x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果在lambda表达式myConsumer的声明中将参数x替换为y,则编译器会生成一个错误:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

编译器生成错误“变量x已在方法methodInFirstLevel(int)中定义”,因为lambda表达式不会引入新的作用域级别。 因此,您可以直接访问封闭范围的字段,方法和局部变量。 例如,lambda表达式直接访问methodInFirstLevel方法的参数x。 要访问封闭类中的变量,请使用关键字this。 在这个例子中,this.x引用成员变量FirstLevel.x。

但是,像本地和匿名类一样,lambda表达式只能访问封闭块的局部变量和参数,它们是final或有效final。 例如,假设您在methodInFirstLevel定义语句之后立即添加以下赋值语句:

void methodInFirstLevel(int x) {
    x = 99;
    // ...
}

由于这个赋值语句,变量FirstLevel.x不再是有效 final。 因此,lambda表达式myConsumer尝试访问FirstLevel.x变量时,Java编译器会生成类似于“从lambda表达式引用的局部变量必须是final或有效final”的错误消息:

System.out.println("x = " + x);
目标类型

你如何确定一个lambda表达式的类型? 回想一下选择的年龄在18到25岁之间的男性成员的lambda表达式:

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25

这个lambda表达式在以下两种方法中使用:

  • 「方法3:在本地类中指定搜索条件代码」中的public static void printPersons(List<Person> roster, CheckPerson tester)
  • 「方法6:使用带有Lambda表达式的标准函数接口」中的public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

当Java运行时调用方法printPersons时,它期望CheckPerson的数据类型,所以lambda表达式是这种类型的。 但是,当Java运行时调用方法printPersonsWithPredicate时,它需要Predicate的数据类型,所以lambda表达式就是这种类型的。 这些方法所期望的数据类型被称为目标类型(target type)。 为了确定lambda表达式的类型,Java编译器使用上下文的目标类型或者找到lambda表达式的情况。 因此,在Java编译器可以确定目标类型的情况下,只能使用lambda表达式:

  • 变量声明
  • 赋值(Assignments)
  • 返回语句
  • 数组初始化器
  • 方法或构造函数参数
  • Lambda表达式体
  • 条件表达式,?:
  • 强制转换表达式(Cast expressions)
目标类型和方法参数

对于方法参数,Java编译器使用另外两种语言功能来确定目标类型:重载解析(overload resolution)和类型参数推断(type argument inference)。

考虑以下两个功能接口(java.lang.Runnable和java.util.concurrent.Callable ):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

Runnable.run方法不会返回值,而Callable.call则可以。

假设您已经重载了方法调用,如下所示:

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

在下面的语句中将调用哪个方法?

String s = invoke(() -> "done");

方法invoke(Callable<T>)将被调用,因为该方法返回一个值; 方法invoke(Runnable)不会。 在这种情况下,lambda表达式() -> "done"的类型是Callable

序列化

如果其目标类型及其捕获的参数是可序列化的,则可以序列化一个lambda表达式。 但是,像内部类一样,lambda表达式的序列化是强烈的不鼓励。

方法引用

您使用lambda表达式来创建匿名方法。 然而,有时候,lambda表达式除了调用一个现有的方法之外什么也不做。 在这些情况下,通过名称来引用现有方法通常会更清楚。 方法引用(Method references)使您可以执行此操作; 他们是紧凑的,易于阅读的lambda表达式已经有一个名称的方法。

请再次考虑Lambda表达式中讨论的Person类:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }
    
    public Calendar getBirthday() {
        return birthday;
    }    

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }}

假设社交网络应用程序的成员包含在一个数组中,并且您希望按年龄对数组进行排序。 您可以使用以下代码(请参阅示例MethodReferencesTest中的本节中介绍的代码摘录):

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
        
Arrays.sort(rosterAsArray, new PersonAgeComparator());

这种调用排序的方法签名如下:

static <T> void sort(T[] a, Comparator<? super T> c)

注意Comparator接口是一个功能接口。 因此,您可以使用lambda表达式而不是定义,然后创建一个实现Comparator的类的新实例:

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

但是,这种比较两个Person实例的出生日期的方法已经以Person.compareByAge的形式存在。 您可以在lambda表达式的主体中调用此方法:

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

因为这个lambda表达式调用了一个现有的方法,所以可以使用方法引用而不是lambda表达式:

Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用Person::compareByAge在语义上与lambda表达式(a, b) -> Person.compareByAge(a, b)相同。 每个都有以下特点:

  • 它的形式参数列表是从Comparator.compare复制的,它是(Person, Person)。
  • 它的主体调用Person.compareByAge方法。
方法引用的种类

有四种方法参考:

Kind Example
Reference to a static method ContainingClass::staticMethodName
Reference to an instance method of a particular object containingObject::instanceMethodName
Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName
Reference to a constructor ClassName::new
引用静态方法

方法引用Person::compareByAge是对静态方法的引用。

引用特定对象的实例方法

以下是对特定对象的实例方法的引用的示例:

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }
        
    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

方法引用myComparisonProvider::compareByName调用作为对象myComparisonProvider的一部分的方法compareByName。 JRE推断方法类型参数,在这种情况下是(Person,Person)。

引用特定类型的任意对象的实例方法

以下是对特定类型任意对象的实例方法的引用示例:

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用String::compareToIgnoreCase等效的lambda表达式将具有形式参数列表(String a, String b),其中a和b是用于更好地描述此示例的任意名称。 方法引用将调用方法a.compareToIgnoreCase(b)。

引用一个构造函数

您可以使用名称new以与静态方法相同的方式引用构造函数。 以下方法将元素从一个集合复制到另一个集合:

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {
        
        DEST result = collectionFactory.get();
        for (T t : sourceCollection) {
            result.add(t);
        }
        return result;
}

功能接口Supplier包含一个get方法,它不接受任何参数,并返回一个对象。 因此,您可以使用lambda表达式调用方法transferElements,如下所示:

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

您可以使用构造函数引用来代替lambda表达式,如下所示:

Set<Person> rosterSet = transferElements(roster, HashSet::new);

Java编译器推断您要创建一个包含Person类型元素的HashSet集合。 或者,您可以指定如下:

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

何时使用嵌套类、本地类、匿名类和 Lambda 表达式

正如「嵌套类」一节所述,嵌套类使您能够对仅在一个地方使用的类进行逻辑分组,增加封装的使用,并创建更具可读性和可维护性的代码。本地类,匿名类和lambda表达式也赋予这些优点;但是,它们旨在用于更具体的情况:

  • 本地类:如果您需要创建一个类的多个实例,访问其构造函数或引入一个新的命名类型(例如,因为您需要稍后调用其他方法),请使用它。
  • 匿名类:如果您需要声明字段或其他方法,请使用它。
  • Lambda表达式:
    • 如果您要封装您想要传递给其他代码的单个行为单元,请使用它。例如,如果您希望在集合的每个元素上执行某个操作,完成某个过程或进程遇到错误,则可以使用lambda表达式。
    • 如果您需要一个功能接口的简单实例,并且没有上述条件适用(例如,您不需要构造函数,命名类型,字段或其他方法),请使用它。
  • 嵌套类:如果您的需求类似于本地类的需求,则希望使类型更广泛可用,并且不需要访问本地变量或方法参数。
    • 如果您需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果您不需要此访问权限,请使用静态嵌套类。

枚举类型

枚举类型是(enum type)一种特殊的数据类型,它使一个变量成为一组预定义的常量。 该变量必须等于为其预定义的值之一。 常见的例子包括罗盘方向(NORTH,SOUTH,EAST和WEST的值)和星期几。

因为它们是常量,枚举类型的字段的名称是大写字母。

在Java编程语言中,您可以使用 enum 关键字定义一个枚举类型。 例如,你可以指定一个星期几的枚举类型为:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY 
}

任何时候你需要表示一组固定的常量时,你应该使用枚举类型。 这包括自然枚举类型,例如太阳系中的行星,以及在编译时知道所有可能值的数据集,例如菜单上的选项,命令行标志等等。

这是一些代码,告诉你如何使用上面定义的日期枚举:

public class EnumTest {
    Day day;
    
    public EnumTest(Day day) {
        this.day = day;
    }
    
    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;
                    
            case FRIDAY:
                System.out.println("Fridays are better.");
                break;
                         
            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;
                        
            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }
    
    public static void main(String[] args) {
        EnumTest firstDay = new EnumTest(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumTest fifthDay = new EnumTest(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumTest sixthDay = new EnumTest(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumTest seventhDay = new EnumTest(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

输出是:

Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.

Java编程语言枚举类型比其他语言中的对应语言更强大。 枚举声明定义了一个类(称为枚举类型)。 枚举类的主体可以包含方法和其他字段。 编译器在创建枚举时会自动添加一些特殊的方法。 例如,它们具有一个静态值方法,该方法返回一个包含声明顺序的所有枚举值的数组。 此方法通常与for-each构造结合使用,以遍历枚举类型的值。 例如,下面的Planet类例子中的代码遍历太阳系中的所有行星。

for (Planet p : Planet.values()) {
    System.out.printf("Your weight on %s is %f%n",
                      p, p.surfaceWeight(mass));
}

注意:所有枚举都隐式扩展java.lang.Enum。 由于一个类只能扩展一个父类,所以Java语言不支持多态继承,因此枚举不能扩展任何其他内容。

在下面的例子中,Planet是一个枚举类型,代表太阳系中的行星。它们具有恒定的质量和半径属性。

每个枚举常量声明为质量和半径参数的值。这些值在创建常量时传递给构造函数。 Java要求在任何字段或方法之前先定义常量。此外,当有字段和方法时,枚举常量的列表必须以分号结尾。

注意:枚举类型的构造函数必须是包私有(package-private)或私有访问。它会自动创建在枚举正文开头定义的常量。你不能自己调用​​一个枚举构造函数。

除了它的属性和构造函数外,Planet还有方法可以让你检索每个星球上物体的表面重力和重量。下面是一个示例程序,它可以将您的体重(在任何单位中)加以计算,并在所有行星上(在同一单元中)打印您的体重:

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

如果从命令行运行参数为175的Planet.class,则会得到以下输出:

$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413

参考

  1. Learning the Java Language