Java基础知识(22)- Java 面向对象(四)| Effectively final、Lambda 表达式

 

本文上接 "Java基础知识(11)- Java 面向对象(三)",补充以下内容。


1. Effectively final

    Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。从 Java 8 开始,可以不加 final 修饰符,由系统默认添加,Java 将这个功能称为 Effectively final 功能。

    示例代码:

 1         package com.example;
 2 
 3         public class App {
 4             public static void main( String[] args ) {
 5 
 6                 String[] data = {"String 1", "String 2"};
 7 
 8                 // 匿名类方式实现 Out 接口
 9                 Out out = new Out() {
10                     @Override
11                     public void display(String title) {
12                         data[0] = "Data Changed";
13                         System.out.println(title + ": " + data[0]);
14                     }
15 
16                 };
17                 out.display("Anonymous Class");
18             }
19         }
20 
21         interface Out {
22             void display(String title);
23         }


    同样的代码, Java 7 中会出现代码错误,提示 data 必须显式的声明这个变量为 final。Java 8 开始,它不要求程序员必须将访问的局部变量显式的声明为 final。

    final 变量能被显式地初始化并且只能初始化一次。被声明为 final 的对象的引用不能指向不同的对象。但是 final 对象里的数据可以被改变。也就是说 final 对象的引用不能改变,但是里面的值可以改变。
    
    在 Lambda 表达式中,使用局部变量的时候,也要求该变量必须是 final 的,所以 effectively final 在 Lambda 表达式上下文中非常有用。

    Lambda 表达式在编程中是经常使用的,而匿名内部类是很少使用的。那么,我们在 Lambda 编程中每一个被使用到的局部变量都去显示定义成 final 吗?显然这不是一个好方法。所以,Java 8 引入了 effectively final 新概念。

    总结一下,规则没有改变,Lambda 表达式和匿名内部类访问的局部变量必须是 final 的,只是不需要程序员显式的声明变量为 final 的,从而节省时间。


2. Lambda 表达式

    Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的 λ 演算得名,也可称为闭包(Closure)。现在很多语言都支持 Lambda 表达式,如 C++、C#、Java、 Python 和 JavaScript 等。

    Lambda 表达式是推动 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

    1) Lambda 表达式语法

        (参数列表) -> {
            // Lambda 表达式体
        }

        -> 被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:

            左侧:Lambda 表达式的参数列表。
            右侧:Lambda 表达式中所需执行的功能,用 {} 包起来,即 Lambda 体。

        示例代码:

 1             package com.example;
 2 
 3             public class App {
 4                 public static void main( String[] args ) {
 5 
 6                     String[] data = {"String 1", "String 2"};
 7 
 8                     // 匿名类方式实现 Out 接口
 9                     Out out = new Out() {
10                         @Override
11                         public void display(String title) {
12                             data[0] = "Data Changed";
13                             System.out.println(title + ": " + data[0]);
14                         }
15 
16                     };
17                     out.display("Anonymous Class");
18 
19                     // Lambda 表达式实现 Out 接口
20                     Out out2 = (String title) -> {
21                         System.out.println(title + ": " + data[1]);
22                     };
23                     out2.display("Lambda");
24 
25                 }
26             }
27 
28             interface Out {
29                 void display(String title);
30             }


            很显然,实现同样功能,Lambda 表达式比匿名类方式代码更简洁。

    2) Lambda 表达式的特性

        优点:
    
            (1) 代码简洁,开发迅速
            (2) 方便函数式编程
            (3) 非常容易进行并行计算
            (4) Java 引入 Lambda,改善了集合操作(引入 Stream API)

        缺点:

            (1) 代码可读性变差
            (2) 在非并行计算中,很多计算未必有传统的 for 性能要高
            (3) 不容易进行调试

    3) 函数式接口

        Lambda 表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误:

            The target type of this expression must be a functional interface

        这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface,上文的 Out 接口可以修改成如下代码:

            @FunctionalInterface
            interface Out {
                void display();
            }

        在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。

        @FunctionalInterface 注解与 @Override 注解的作用类似。Java 8 中专门为函数式接口引入了一个新的注解 @FunctionalInterface。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
        
        需要注意的是,即使不使用 @FunctionalInterface 注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
 
        提示:Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。

    4) Lambda 表达式的简写方式

        1) 可以省略参数类型,因为就只有一个抽象方法,会自动匹配抽象方法中的参数类型;
        2) 当只有一个参数时,可以省略 ();
        3) 当方法体中只有一句代码语句时,可以省略 {}。如果有返回值,省略的话要同时省略 {} 和 return;

        示例代码:

 1             package com.example;
 2 
 3             public class App {
 4                 public static void main( String[] args ) {
 5 
 6                     String[] data = {"String 1", "String 2"};
 7 
 8                     // 匿名类方式实现 Out 接口
 9                     Out out = new Out() {
10                         @Override
11                         public void display(String title) {
12                             data[0] = "Data Changed";
13                             System.out.println(title + ": " + data[0]);
14                         }
15 
16                     };
17                     out.display("Anonymous Class");
18 
19                     // Lambda 表达式实现 Out 接口
20                     Out out2 = (String title) -> {
21                         System.out.println(title + ": " + data[1]);
22                     };
23                     out2.display("Lambda");
24 
25                     // Lambda 表达式实现 Out 接口,省略参数类型
26                     Out out3 = (title) -> {
27                         System.out.println(title + ": " + data[1]);
28                     };
29                     out3.display("Lambda 2");
30 
31                     // Lambda 表达式实现 Out 接口,当只有一个参数时,可以省略 ()
32                     Out out4 = title -> {
33                         System.out.println(title + ": " + data[1]);
34                     };
35                     out4.display("Lambda 3");
36 
37                     // Lambda 表达式实现 Out 接口,方法体中只有一句代码语句时,可以省略 {}
38                     Out out5 = title -> System.out.println(title + ": " + data[1]);
39                     out5.display("Lambda 4");
40                     
41                 }
42             }
43 
44             @FunctionalInterface
45             interface Out {
46                 void display(String title);
47             }


    5) Lambda 表达式的使用

        (1) 作为参数使用

            Lambda 表达式一种常见的用途就是作为参数传递给方法,这需要声明参数的类型为函数式接口类型。
            
            示例代码如下:

 1                 package com.example;
 2 
 3                 public class App {
 4                     public static void main( String[] args ) {
 5 
 6                         String[] data = {"String 1", "String 2"};
 7 
 8                         // 作为参数使用的 Lambda 表达式
 9                         display(title -> {
10                             System.out.println(title + ": " + data[1]);
11                         }, "Lambda 5");
12 
13                     }
14 
15                     public static void display(Out out, String title) {
16                         out.display(title);
17                     }
18                 }
19 
20                 @FunctionalInterface
21                 interface Out {
22                     void display(String title);
23                 }


        (2) 访问变量

            Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量。

 1             package com.example;
 2 
 3             public class LambdaApp {
 4                 private static String global = "Global Variable";
 5 
 6                 public static void main( String[] args ) {
 7 
 8                     String[] data = {"String 1", "String 2"};
 9 
10                     // 访问局部变量 data 和 成员变量 global
11                     Out out6 = title -> {
12                         System.out.println(title + ": " + data[1] + " | " + global);
13                     };
14                     out6.display("Lambda 7");
15                 }
16             }
17 
18             @FunctionalInterface
19             interface Out {
20                 void display(String title);
21             }

       
        (3) 方法引用

            方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现复杂,复用的地方多,推荐使用方法引用。

            Java 8 之后增加了双冒号::运算符,该运算符用于 “方法引用” ,注意不是调用方法。“方法引用” 虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关。 方法引用的语法格式如下:

                ObjectRef::methodName

            其中,ObjectRef 是类名或者实例名,methodName 是相应的方法名。

            注意:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致,示例代码如下。

 1                 class LambdaDemo {
 2 
 3                     public static void display1(String str) {
 4                         System.out.println(str);
 5                     }
 6 
 7                     public void display2(String str) {
 8                         System.out.println(str);
 9                     }
10                 }


            LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。

 1                 public class App {
 2                     public static void main(String[] args) {
 3 
 4                         String[] data = {"String 1", "String 2"};
 5 
 6                         // 方法引用
 7                         display2(LambdaDemo::display1, "Lambda 8", data[0]);
 8                         
 9                         // 访问成员方法需要先创建对象
10                         LambdaDemo lambdaDemo = new LambdaDemo();
11                         display2(lambdaDemo::display2, "Lambda 9", data[1]);
12                     }
13 
14                     public static void display2(Out out, String title, String str) {
15                         out.display(title + ": " + str);
16                     }
17                 }
18 
19                 @FunctionalInterface
20                 interface Out {
21                     void display(String title);
22                 }

 

posted @ 2022-03-24 11:29  垄山小站  阅读(369)  评论(0)    收藏  举报