6.嵌套类和枚举类型

124 阅读16分钟

嵌套类基础知识

Java 编程语言允许您在另一个类中定义一个类。这样的类称为嵌套类,如下所示:

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

术语: 嵌套类分为两类:非静态类和静态类。非静态嵌套类称为内部类。声明的嵌套类称为静态嵌套类static


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

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

为什么要使用嵌套类?

使用嵌套类的令人信服的理由包括:

  • 这是一种对仅在一个地方使用的类进行逻辑分组的方法:如果一个类只对另一个类有用,那么将其嵌入到该类中并将两者保持在一起是合乎逻辑的。嵌套这样的“帮助程序类”会使它们的包更加精简。
  • 它增加了封装:考虑两个顶级类 A 和 B,其中 B 需要访问否则会声明的 A 成员。通过将类 B 隐藏在类 A 中,可以将 A 的成员声明为私有,而 B 可以访问它们。此外,B本身可以隐藏在外界之外。private
  • 它可以使代码更具可读性和可维护性:将小类嵌套在顶级类中会使代码更接近其使用位置。

内部类

与实例方法和变量一样,内部类与其封闭类的实例相关联,并可以直接访问该对象的方法和字段。此外,由于内部类与实例相关联,因此它本身无法定义任何静态成员。

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

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

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

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

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

有两种特殊的内部类:本地类匿名类

静态嵌套类

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


注意: 静态嵌套类与其外部类(和其他类)的实例成员进行交互,就像任何其他顶级类一样。实际上,静态嵌套类在行为上是一个顶级类,为了方便打包,它已嵌套在另一个顶级类中。内部类和嵌套静态类示例也演示了这一点。


实例化静态嵌套类的方式与顶级类相同:

StaticNestedClass staticNestedObject = new StaticNestedClass();

内部类和嵌套静态类示例

下面的示例 OuterClass 和 TopLevelClass OuterClass 演示了内部类 (InnerClass)、嵌套静态类 (StaticNestedClass) 和顶级类 (TopLevelClass) 的哪些类成员可以访问:

OuterClass.java

public class OuterClass {

    String outerField = "Outer field";
    static String staticOuterField = "Static outer field";

    class InnerClass {
        void accessMembers() {
            System.out.println(outerField);
            System.out.println(staticOuterField);
        }
    }

    static class StaticNestedClass {
        void accessMembers(OuterClass outer) {
            // Compiler error: Cannot make a static reference to the non-static
            //     field outerField
            // System.out.println(outerField);
            System.out.println(outer.outerField);
            System.out.println(staticOuterField);
        }
    }

    public static void main(String[] args) {
        System.out.println("Inner class:");
        System.out.println("------------");
        OuterClass outerObject = new OuterClass();
        OuterClass.InnerClass innerObject = outerObject.new InnerClass();
        innerObject.accessMembers();

        System.out.println("\nStatic nested class:");
        System.out.println("--------------------");
        StaticNestedClass staticNestedObject = new StaticNestedClass();        
        staticNestedObject.accessMembers(outerObject);
        
        System.out.println("\nTop-level class:");
        System.out.println("--------------------");
        TopLevelClass topLevelObject = new TopLevelClass();        
        topLevelObject.accessMembers(outerObject);                
    }
}

TopLevelClass.java

public class TopLevelClass {

    void accessMembers(OuterClass outer) {     
        // Compiler error: Cannot make a static reference to the non-static
        //     field OuterClass.outerField
        // System.out.println(OuterClass.outerField);
        System.out.println(outer.outerField);
        System.out.println(OuterClass.staticOuterField);
    }  
}

此示例打印以下输出:

Inner class:
------------
Outer field
Static outer field

Static nested class:
--------------------
Outer field
Static outer field

Top-level class:
--------------------
Outer field
Static outer field

请注意,静态嵌套类StaticNestedClass与其外部类OuterClass的实例成员outerField进行交互,就像任何其他顶级类一样。静态嵌套类无法直接访问,因为它是封闭类 .Java 编译器在突出显示的语句处生成错误:

static class StaticNestedClass {
    void accessMembers(OuterClass outer) {
       // Compiler error: Cannot make a static reference to the non-static
       //     field outerField
       System.out.println(outerField);
    }
}

若要修复此错误,请通过对象引用进行访问:outerField

System.out.println(outer.outerField);

同样,顶级类也不能直接访问。TopLevelClass``outerField

隐藏

如果特定作用域(如内部类或方法定义)中某个类型的声明(如成员变量或参数名称)与封闭作用域中的另一个声明同名,则该声明将隐藏封闭作用域的声明。不能仅通过其名称来引用隐藏声明。以下示例 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

此示例定义了三个变量,分别是:类的成员变量、内部类的成员变量和方法中的参数。定义为方法参数的变量隐藏了内部类的变量。因此,当您在 method 中使用变量时,它引用 method 参数。要引用内部类的成员变量,请使用关键字来表示封闭范围:x ShadowTest FirstLevel methodInFirstLevel x methodInFirstLevel FirstLevel x methodInFirstLevel FirstLevel this

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

引用成员变量,这些变量用它们所属的类名将较大的作用域括起来。例如,以下语句从该方法访问类的成员变量:ShadowTest methodInFirstLevel

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

序列化

强烈建议不要序列化内部类,包括本地类和匿名类。当 Java 编译器编译某些构造(例如内部类)时,它会创建合成构造;这些是类、方法、字段和其他在源代码中没有相应构造的构造。综合构造使 Java 编译器能够在不更改 JVM 的情况下实现新的 Java 语言功能。但是,合成构造在不同的 Java 编译器实现之间可能会有所不同,这意味着文件在不同的实现之间也可能有所不同。因此,如果序列化内部类,然后使用不同的 JRE 实现对其进行反序列化,则可能会遇到兼容性问题。有关编译内部类时生成的合成构造的更多信息,请参见获取方法参数名称一节中的隐式参数和合成参数部分。.class

内部类示例

要查看正在使用的内部类,首先考虑数组。在下面的示例中,您将创建一个数组,用整数值填充该数组,然后仅按升序输出数组的偶数索引的值。

下面的 DataStructure.java 示例包括:

  • 外部类,它包括一个构造函数,用于创建包含填充有连续整数值(0、1、2、3 等)的数组的实例,以及一个打印具有偶数索引值的数组元素的方法。DataStructure``DataStructure
  • 内部类,用于实现接口,该接口扩展了 Iterator Integer 接口。迭代器用于单步执行数据结构,通常具有测试最后一个元素、检索当前元素和移动到下一个元素的方法。EvenIterator DataStructureIterator < >
  • 实例化对象 () 的方法,然后调用该方法来打印具有偶数索引值的数组元素。main DataStructure ds printEven arrayOfInts
 
public class DataStructure {
    
    // Create an array
    private final static int SIZE = 15;
    private int[] arrayOfInts = new int[SIZE];
    
    public DataStructure() {
        // fill the array with ascending integer values
        for (int i = 0; i < SIZE; i++) {
            arrayOfInts[i] = i;
        }
    }
    
    public void printEven() {
        
        // Print out values of even indices of the array
        DataStructureIterator iterator = this.new EvenIterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
    }
    
    interface DataStructureIterator extends java.util.Iterator<Integer> { } 

    // Inner class implements the DataStructureIterator interface,
    // which extends the Iterator<Integer> interface
    
    private class EvenIterator implements DataStructureIterator {
        
        // Start stepping through the array from the beginning
        private int nextIndex = 0;
        
        public boolean hasNext() {
            
            // Check if the current element is the last in the array
            return (nextIndex <= SIZE - 1);
        }        
        
        public Integer next() {
            
            // Record a value of an even index of the array
            Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);
            
            // Get the next even element
            nextIndex += 2;
            return retValue;
        }
    }
    
    public static void main(String s[]) {
        
        // Fill the array with integer values and print out only
        // values of even indices
        DataStructure ds = new DataStructure();
        ds.printEven();
    }
}

输出为:

0 2 4 6 8 10 12 14 

请注意,该类直接引用对象的实例变量。EvenIterator arrayOfInts DataStructure

可以使用内部类来实现帮助程序类,例如此示例中所示的帮助程序类。若要处理用户界面事件,必须知道如何使用内部类,因为事件处理机制会广泛使用它们。

本地类和匿名类

还有另外两种类型的内部类。可以在方法的主体中声明内部类。这些类称为本地类。还可以在方法的主体中声明内部类,而无需命名该类。这些类称为匿名类

修饰符

可以对内部类使用与外部类的其他成员相同的修饰符。例如,可以使用访问说明符 、 和 来限制对内部类的访问,就像使用它们来限制对其他类成员的访问一样。private public protected

本地课程

局部类是在中定义的类,其 是平衡大括号之间的一组零个或多个语句。 通常,您会发现在 方法。

本节包括以下主题:

声明本地类

您可以在任何块中定义局部类(有关更多信息,请参见表达式、语句和块)。 例如,可以在方法主体、循环或子句中定义局部类。for if

以下示例 LocalClassExample 验证两个电话号码。它定义了 方法中的本地类:PhoneNumber validatePhoneNumber

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

该示例通过以下方式验证电话号码 首先从电话号码中删除除 数字 0 到 9。之后,它会检查电话号码是否 正好包含十位数字(电话号码的长度 北美)。此示例打印以下内容:

First number is 1234567890
Second number is invalid

访问封闭类的成员

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

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

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

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

因为这个任务 语句,则该变量不再是有效的最终变量。因此,Java 编译器会生成类似于 “从内部类引用的局部变量必须是 final 或有效的 final”,其中内部类尝试访问该变量:numberLength PhoneNumber numberLength

if (currentNumber.length() == numberLength)

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

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

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

影子和本地类

局部类中类型(如变量)的声明 封闭作用域中具有相同名称的影子声明。有关详细信息,请参阅重影

局部类类似于内部类

局部类类似于内部类,因为它们不能定义或声明任何静态成员。静态方法中的局部类,例如在静态方法中定义的类,只能引用封闭类的静态成员。例如,如果未将成员变量定义为静态变量,那么 Java 编译器将生成类似于“无法从静态上下文引用非静态变量”的错误。PhoneNumber validatePhoneNumber regularExpression 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();
    }

不能声明静态 初始值设定项或本地类中的成员接口。以下 代码摘录不会编译,因为该方法已声明。编译器生成错误 与“修饰符”相似 'static' is only allowed in Constant 变量声明“,当它遇到这个方法时 定义:EnglishGoodbye.sayGoodbye static

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

本地类可以有 静态成员,前提是它们是常量变量。(常量变量是基元类型或类型的变量,声明为 final,并使用编译时常量表达式进行初始化。编译时常量表达式通常是可以在编译时计算的字符串或算术表达式。有关详细信息,请参阅了解类成员。这 以下代码摘录进行编译,因为静态成员是 常量变量:String 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();
    }

匿名类

匿名类使你能够使代码更加简洁。 它们使您能够同时声明和实例化类 时间。它们类似于本地类,只是它们没有 名字。如果只需要使用一次本地类,请使用它们。

本节包括以下主题:

声明匿名类

本地类是类声明,而匿名类 是表达式,这意味着您在另一个中定义类 表达。以下示例 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
  • 与嵌套类一样,匿名类中类型(如变量)的声明会隐藏封闭作用域中具有相同名称的任何其他声明。有关详细信息,请参阅重影

匿名类也具有与本地类相同的限制 关于其成员的类:

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

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

  • 领域
  • 额外的方法(即使它们没有实现超类型的任何方法)
  • 实例初始值设定项
  • 本地类

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

匿名类的示例

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

考虑 JavaFX 示例 HelloWorld.java(摘自 JavaFX 入门中的 Hello World, JavaFX 样式部分)。这 sample 创建一个包含 “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();
    }
}

在此示例中,方法调用指定选择“Say 'Hello World' ”按钮时发生的情况。此方法需要 类型的对象。该接口仅包含一个方法,即句柄。该示例使用匿名类表达式,而不是使用新类实现此方法。请注意,此表达式是传递给该方法的参数。btn.setOnAction``EventHandler<ActionEvent>``EventHandler<ActionEvent>``btn.setOnAction

由于该接口仅包含一种方法,因此您可以使用 lambda 表达式,而不是匿名类表达式。请参阅 部分 Lambda 表达式了解更多信息 信息。EventHandler<ActionEvent>

匿名类非常适合实现包含两个或多个方法的接口。以下 JavaFX 示例摘自 UI 控件的定制部分。突出显示的代码将创建一个仅接受数值的文本字段。它通过重写从类继承的 和 方法,使用匿名类重新定义类的默认实现。TextField``replaceText``replaceSelection``TextInputControl

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 表达式使您能够执行此操作,将功能视为方法参数,或将代码视为数据。

上一节“匿名类”演示了如何在不为其命名的情况下实现基类。 尽管这通常比命名类更简洁,但对于类 只有一种方法,即使是匿名类似乎也有点 过度和繁琐。Lambda 表达式允许您表示 单方法类更紧凑。

本节包括以下主题:

Lambda 表达式的理想使用案例

假设您正在创建一个社交网络应用程序。你 想要创建使管理员能够执行的功能 对 满足特定条件的社交网络应用程序。下表详细介绍了此用例:

描述
名字对所选成员执行操作
主要参与者管理员
前提 条件管理员已登录到系统。
后置条件仅对符合指定条件的成员执行操作。
主要成功方案1. 管理员指定要对其执行特定操作的成员的条件。
  1. 管理员指定要对这些选定成员执行的操作。
  2. 管理员选择 “提交”按钮。
  3. 系统将查找与指定条件匹配的所有成员。
  4. 系统对所有匹配成员执行指定的操作。 | | 扩展 | 1一个。管理员可以选择在指定要执行的操作之前或选择 “提交”按钮之前预览符合指定条件的成员。 | | 发生频率 | 白天很多次。 |

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

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

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

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

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

本节首先介绍此用例的幼稚方法。它使用本地和匿名类改进了这种方法,然后使用 lambda 表达式以高效简洁的方法结束。在示例 RosterTest 中找到本节中描述的代码摘录。

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

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

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

注意列表是有序集合集合是一个对象 将多个元素组合成一个单元。集合是 用于存储、检索、操作和通信聚合 数据。有关集合的详细信息,请参阅集合跟踪。

此方法可能会使应用程序变得脆弱,即应用程序可能由于引入更新(例如较新的数据类型)而无法正常工作。假设您升级了应用程序并更改了类的结构,使其包含不同的成员变量;也许该类使用不同的数据类型或算法记录和测量年龄。您必须重写大量 API 以适应此更改。此外,这种方法具有不必要的限制性;如果你 例如,想要打印小于特定年龄的成员?Person

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

此方法通过调用方法检查参数中包含的每个实例是否满足参数中指定的搜索条件。如果该方法返回一个值,则在实例上调用该方法。Person``List``roster``CheckPerson``tester``tester.test``tester.test``true``printPersons``Person

若要指定搜索条件,请实现以下接口:CheckPerson

interface CheckPerson {
    boolean test(Person p);
}

以下类通过指定方法的实现来实现接口。此方法筛选有资格在美国服兵役的成员:如果其参数为男性且年龄在 18 到 25 岁之间,则返回一个值:CheckPerson``test``true``Person

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:在匿名类中指定搜索条件代码

以下调用该方法的参数之一是匿名类,该类筛选有资格在美国服兵役的成员:年龄在 18 至 25 岁之间的男性:printPersons

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

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

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

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

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

有关如何定义 lambda 表达式的信息,请参阅 Lambda 表达式的语法

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

方法 6:将标准函数接口与 Lambda 表达式结合使用

重新考虑界面:CheckPerson

interface CheckPerson {
    boolean test(Person p);
}

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

例如,您可以使用该接口代替 .这 接口包含以下方法:Predicate<T>``CheckPerson``boolean test(T t)

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

该接口是泛型接口的一个示例。(有关泛型的详细信息,请参阅泛型(已更新)课程。泛型类型(如泛型接口)在尖括号 () 内指定一个或多个类型参数。此接口仅包含一个类型参数 。使用实际类型参数声明或实例化泛型类型时,将具有参数化类型。例如,参数化类型如下:Predicate<T>``<>``T``Predicate<Person>

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

此参数化类型包含一个方法,该方法的返回类型和参数与 相同。因此,您可以使用以下方法代替:CheckPerson.boolean test(Person p)``Predicate<T>``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 表达式

重新考虑该方法,看看您还可以在哪些地方使用 lambda 表达式:printPersonsWithPredicate

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

此方法检查参数中包含的每个实例是否满足参数中指定的条件。如果实例确实满足 指定的条件,则在实例上调用该方法。Person``List``roster``Predicate``tester``Person``tester``printPerson``Person

您可以指定要对满足 指定的条件的实例执行的其他操作,而不是调用该方法。您可以使用 lambda 表达式指定此操作。假设您想要一个类似于 的 lambda 表达式,该表达式接受一个参数(类型的对象)并返回 void。请记住,要使用 lambda 表达式,您需要实现一个函数式接口。在这种情况下,您需要一个函数接口,其中包含一个抽象方法,该方法可以接受一个类型的参数并返回 void。该接口包含具有这些特征的方法。这 以下方法将调用替换为 的实例调用该方法:printPerson``Person``tester``printPerson``Person``Person``Consumer<T>``void accept(T t)``p.printPerson()``Consumer<Person>``accept

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:在本地类中指定搜索条件代码以获取有资格获得选择性服务的成员时调用的方法相同。lambda 表达式用于 突出显示打印成员:printPersons

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

如果您想对会员的个人资料做更多的事情,而不是打印出来,该怎么办?假设您要验证成员的配置文件或检索 他们的联系信息?在这种情况下,您需要一个功能 包含返回值的抽象方法的接口。 该接口包含 方法。以下方法检索数据 由参数指定,并且 然后对其执行由 参数:Function<T,R>``R apply(T t)``mapper``block

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

以下方法从每个成员那里检索电子邮件地址 包含在谁有资格服兵役和 然后打印它:roster

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. 从集合中获取对象的源。在此示例中,它从集合中获取对象的源。请注意,集合是 类型的集合,也是 类型的对象。source``Person``roster``roster``List``Iterable
  2. 筛选与对象匹配的对象。在此示例中,该对象是一个 lambda 表达式,用于指定哪些成员有资格获得选择性服务。Predicate``tester``Predicate
  3. 将每个筛选的对象映射到对象指定的值。在此示例中,对象是一个 lambda 表达式,用于返回成员的电子邮件地址。Function``mapper``Function
  4. 对对象指定的每个映射对象执行操作。在此示例中,对象是一个 lambda 表达式,它输出一个字符串,该字符串是该对象返回的电子邮件地址。Consumer``block``Consumer``Function

您可以将其中每个操作替换为聚合操作。

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

以下示例使用聚合操作打印集合中包含的有资格服兵役的成员的电子邮件地址:roster

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行动聚合操作
获取对象源Stream<E> stream()
筛选与对象匹配的对象PredicateStream<T> filter(Predicate<? super T> predicate)
将对象映射到对象指定的另一个值Function<R> Stream<R> map(Function<? super T,? extends R> mapper)
执行对象指定的操作Consumervoid forEach(Consumer<? super T> action)

运算 、 和 是聚合运算。聚合操作处理来自流的元素,而不是直接从集合中处理元素(这就是此示例中调用的第一个方法是 的原因)。是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源(如集合)传输值。管道是一系列流操作,在此示例中为 - -。此外,聚合操作通常接受 lambda 表达式作为参数,使您能够自定义它们的行为方式。filter``map``forEach``stream``filter``map``forEach

有关聚合操作的更全面讨论,请参阅聚合操作课程。

GUI 应用程序中的 Lambda 表达式

在图形用户界面 (GUI) 应用程序(如键盘)中处理事件 操作、鼠标操作和滚动操作,通常由您创建 事件处理程序,通常涉及实现特定的 接口。通常,事件处理程序接口是正常运行的 接口;他们往往只有一种方法。

在 JavaFX 示例 HelloWorld.java(在上一节匿名类中讨论)中,您可以 将突出显示的匿名类替换为此处中的 lambda 表达式 陈述:

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

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

方法调用指定什么 当您选择对象所表示的按钮时发生。此方法需要 类型的对象。该接口仅包含一种方法 . 此接口是一个函数接口,因此您可以使用以下突出显示的 lambda 表达式来替换它:btn.setOnAction``btn``EventHandler<ActionEvent>``EventHandler<ActionEvent>``void handle(T event)

        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
    
  • 箭头令牌,->

  • 一个 body,由单个表达式或语句块组成。此示例使用以下表达式:

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

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

    email -> System.out.println(email)
    

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

以下示例 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));    
    }
}

该方法执行 对两个整数操作数的数学运算。操作 本身由 的实例指定。该示例使用 lambda 表达式定义了两个操作,以及 .示例打印 以下内容:operateBinary``IntegerMath``addition``subtraction

40 + 2 = 42
20 - 10 = 10

访问封闭作用域的局部变量

与本地类和匿名类一样,lambda 表达式可以捕获变量;它们对封闭作用域的局部变量具有相同的访问权限。但是,与本地类和匿名类不同,lambda 表达式没有任何阴影问题(有关更多信息,请参阅阴影)。Lambda 表达式按词法范围划分。这意味着它们不会从超类型继承任何名称,也不会引入新的范围界定级别。lambda 表达式中的声明的解释方式与封闭环境中的声明相同。以下示例 LambdaScopeTest 演示了这一点:

 
import java.util.function.Consumer;
 
public class LambdaScopeTest {
 
    public int x = 0;
 
    class FirstLevel {
 
        public int x = 1;
        
        void methodInFirstLevel(int x) {

            int z = 2;
             
            Consumer<Integer> myConsumer = (y) -> 
            {
                // The following statement causes the compiler to generate
                // the error "Local variable z defined in an enclosing scope
                // must be final or effectively final" 
                //
                // z = 99;
                
                System.out.println("x = " + x); 
                System.out.println("y = " + y);
                System.out.println("z = " + z);
                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
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

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

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

编译器生成错误“Lambda 表达式的参数 x 无法重新声明在封闭作用域中定义的另一个局部变量”,因为 lambda 表达式未引入新的范围级别。因此,您可以直接访问封闭范围的字段、方法和局部变量。例如,lambda 表达式直接访问方法的参数。要访问封闭类中的变量,请使用关键字 。在此示例中,引用成员变量 。x``methodInFirstLevel``this``this.x``FirstLevel.x

但是,与本地类和匿名类一样,lambda 表达式只能访问封闭块的局部变量和参数,这些变量和参数是最终的或有效的最终变量。在此示例中,变量实际上是 final;其值在初始化后永远不会更改。但是,假设您在 lambda 表达式中添加以下赋值语句:z``myConsumer

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

由于此赋值语句,该变量实际上不再是最终变量。因此,Java 编译器会生成类似于“在封闭作用域中定义的局部变量 z 必须是 final 或有效 final”的错误消息。z

目标类型

如何确定 lambda 表达式的类型?召回 选择男性成员的 lambda 表达式,以及 18 至 25 岁之间:

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

此 lambda 表达式用于以下两种方法:

当 Java 运行时调用该方法时,它需要数据类型 ,因此 lambda 表达式属于此类型。然而 当 Java 运行时调用该方法时, 它需要数据类型 , 因此,lambda 表达式属于此类型。数据类型 这些方法 expect 称为目标类型。确定 lambda 的类型 表达式,Java 编译器使用上下文的目标类型 或找到 lambda 表达式的情况。它遵循 您只能在以下情况下使用 lambda 表达式 Java 编译器可以确定目标类型:printPersons``CheckPerson``printPersonsWithPredicate``Predicate<Person>

  • 变量声明
  • 作业
  • 返回语句
  • 数组初始值设定项
  • 方法或构造函数参数
  • Lambda 表达式正文
  • 条件表达式,?:
  • 强制转换表达式

目标类型和方法参数

对于方法参数,Java 编译器确定目标 具有其他两种语言功能的类型:重载分辨率和 类型参数推理。

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

public interface Runnable {
    void run();
}

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

该方法不返回值,而返回值。Runnable.run``Callable<V>.call

假设您重载了该方法,如下所示 (有关重载方法的更多信息,请参阅定义方法):invoke

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

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

以下语句中将调用哪个方法?

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

该方法将是 调用,因为该方法返回一个值;该方法没有。在本例中,lambda 表达式的类型为 。invoke(Callable<T>)``invoke(Runnable)``() -> "done"``Callable<T>

序列化

如果 lambda 表达式的目标类型及其捕获的参数是可序列化的,则可以序列化该表达式。但是,与内部类一样,强烈建议不要序列化 lambda 表达式。

方法参考

您可以使用 lambda 表达式创建匿名方法。但是,有时 lambda 表达式除了调用现有方法外什么都不做。在这些情况下,按名称引用现有方法通常更清晰。方法引用使您能够执行此操作;它们是紧凑、易于阅读的 lambda 表达式,适用于已具有名称的方法。

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

public class Person {

    // ...
    
    LocalDate birthday;
    
    public int getAge() {
        // ...
    }
    
    public LocalDate 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());

此调用的方法签名如下:sort

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

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

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

但是,这种比较两个实例的出生日期的方法已经存在,如 .您可以改为在 lambda 表达式的正文中调用此方法:Person``Person.compareByAge

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

由于此 lambda 表达式调用现有方法,因此您可以使用方法引用而不是 lambda 表达式:

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

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

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

方法引用的种类

有四种方法引用:

语法例子
对静态方法的引用ContainingClass::staticMethodNamePerson::compareByAge MethodReferencesExamples::appendStrings
对特定对象的实例方法的引用containingObject::instanceMethodNamemyComparisonProvider::compareByName myApp::appendStrings2
对特定类型的任意对象的实例方法的引用ContainingType::methodNameString::compareToIgnoreCase String::concat
对构造函数的引用ClassName::newHashSet::new

下面的示例 MethodReferencesExamples 包含前三种类型的方法引用的示例:

import java.util.function.BiFunction;

public class MethodReferencesExamples {
    
    public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
        return merger.apply(a, b);
    }
    
    public static String appendStrings(String a, String b) {
        return a + b;
    }
    
    public String appendStrings2(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        
        MethodReferencesExamples myApp = new MethodReferencesExamples();

        // Calling the method mergeThings with a lambda expression
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", (a, b) -> a + b));
        
        // Reference to a static method
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));

        // Reference to an instance method of a particular object        
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", myApp::appendStrings2));
        
        // Reference to an instance method of an arbitrary object of a
        // particular type
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", String::concat));
    }
}

所有语句都打印相同的内容:System.out.println()``Hello World!

BiFunction 是 java.util.function 包中的众多功能接口之一。函数接口可以表示接受两个参数并生成结果的 lambda 表达式或方法引用。BiFunction

对静态方法的引用

该方法引用静态方法,并且是对静态方法的引用。Person::compareByAge``MethodReferencesExamples::appendStrings

对特定对象的实例方法的引用

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

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

方法引用调用作为对象一部分的方法。JRE 推断方法类型参数,在本例中为 .myComparisonProvider::compareByName``compareByName``myComparisonProvider``(Person, Person)

同样,方法引用调用作为对象一部分的方法。JRE 推断方法类型参数,在本例中为 .myApp::appendStrings2``appendStrings2``myApp``(String, String)

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

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

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

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

同样,方法引用将调用该方法。String::concat``a.concat(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;
}

函数接口包含一个不带参数并返回对象的方法。因此,您可以使用 lambda 表达式调用该方法,如下所示:Supplier``get``transferElements

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

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

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

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

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

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

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

  • 本地类:如果需要创建多个类,请使用它 类的实例,访问其构造函数,或引入一个名为 type(因为,例如,您需要调用其他方法 稍后)。

  • 匿名类:如果需要声明字段或其他方法,请使用它。

  • Lambda 表达式

    • 如果要封装要传递给其他代码的单个行为单元,请使用它。例如,如果要在集合的每个元素上执行特定操作、在进程完成时或进程遇到错误时,则可以使用 lambda 表达式。
    • 如果需要函数接口的简单实例,并且上述条件均不适用(例如,不需要构造函数、命名类型、字段或其他方法),请使用它。
  • 嵌套类:如果您的要求与嵌套类相似,请使用它 的局部类,您希望使类型更广泛 可用,并且您不需要访问局部变量或 方法参数。

    • 如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果不需要此访问权限,请使用静态嵌套类。

枚举类型

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

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

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

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

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

下面是一些代码,演示如何使用上面定义的枚举:Day

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 构造结合使用,以循环访问枚举类型的值。例如,下面类示例中的这段代码循环访问太阳系中的所有行星。enum``values``Planet

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

注意: 所有枚举都隐式扩展。因为一个类只能扩展一个父类(请参阅声明类),所以 Java 语言不支持状态的多重继承(请参阅状态、实现和类型的多重继承),因此枚举不能扩展其他任何内容。java.lang.Enum


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

每个枚举常量都使用质量和半径参数的值进行声明。创建常量时,这些值将传递给构造函数。Java 要求先定义常量,然后再定义任何字段或方法。此外,当存在字段和方法时,枚举常量列表必须以分号结尾。


注意: 枚举类型的构造函数必须是 package-private 或 private access。它会自动创建在枚举正文开头定义的常量。您不能自己调用枚举构造函数。


除了其属性和构造函数之外,还具有允许您检索每个行星上物体的表面重力和重量的方法。下面是一个示例程序,它计算您在地球上的重量(以任何单位),并计算并打印您在所有行星上的重量(以同一单位):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