CodeGym /Java 博客 /随机的 /Java 中 lambda 表达式的解释。有例子和任务。第2部分
John Squirrels
第 41 级
San Francisco

Java 中 lambda 表达式的解释。有例子和任务。第2部分

已在 随机的 群组中发布
这篇文章是为谁准备的?
  • 适用于阅读本文第一部分的人;
  • 它适用于那些认为自己已经很了解 Java Core 但对 Java 中的 lambda 表达式一无所知的人。或者他们可能听说过有关 lambda 表达式的一些信息,但缺少详细信息。
  • 适合对lambda表达式有一定了解,但仍然望而却步,不习惯使用的人。
如果您不属于这些类别之一,您可能会觉得这篇文章乏味、有缺陷,或者通常不是您的菜。在这种情况下,请随意继续做其他事情,或者,如果您精通该主题,请在评论中就我如何改进或补充这篇文章提出建议。 Java 中 lambda 表达式的解释。 有例子和任务。 第 2 - 1 部分该材料并没有声称具有任何学术价值,更不用说新颖性了。恰恰相反:我将尝试尽可能简单地描述(对某些人而言)复杂的事物。解释 Stream API 的请求启发了我写这篇文章。我考虑了一下,决定如果不了解 lambda 表达式,我的一些流示例将难以理解。所以我们将从 lambda 表达式开始。

访问外部变量

此代码是否使用匿名类编译?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
不,counter 变量必须是final. 或者如果不是final,那么至少它不能改变它的价值。同样的原则也适用于 lambda 表达式。他们可以从声明的地方访问他们可以“看到”的所有变量。但是 lambda 不得更改它们(为它们分配新值)。然而,在匿名类中有一种方法可以绕过这个限制。只需创建一个引用变量并更改对象的内部状态。这样做时,变量本身不会改变(指向同一个对象)并且可以安全地标记为final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
这里我们的counter变量是对对象的引用AtomicInteger。并且该incrementAndGet()方法用于更改此对象的状态。变量本身的值在程序运行时不会改变。它总是指向同一个对象,这让我们可以用 final 关键字声明变量。以下是相同的示例,但使用了 lambda 表达式:

int counter = 0;
Runnable r = () -> counter++;
由于与具有匿名类的版本相同的原因,这不会编译: counter在程序运行时不得更改。但如果我们这样做,一切都很好:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
这也适用于调用方法。在 lambda 表达式中,您不仅可以访问所有“可见”变量,还可以调用任何可访问的方法。

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
虽然staticMethod()是私有的,但它可以在main()方法内部访问,因此也可以从方法中创建的 lambda 内部调用它main

什么时候执行 lambda 表达式?

你可能会觉得下面的问题太简单了,但你还是应该问:lambda 表达式里面的代码什么时候执行?什么时候创建的?或者什么时候被调用(目前还不知道)?这很容易检查。

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
屏幕输出:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
您可以看到 lambda 表达式是在线程创建之后的最后执行的,并且仅在程序执行到方法时执行run()。当然不是在宣布的时候。通过声明 lambda 表达式,我们仅创建了一个Runnable对象并描述了其run()方法的行为方式。该方法本身的执行时间要晚得多。

方法参考?

方法引用与 lambda 没有直接关系,但我认为在本文中对它们说几句是有意义的。假设我们有一个 lambda 表达式,它不执行任何特殊操作,只是简单地调用一个方法。

x -> System.out.println(x)
它接收一些x只是调用System.out.println(),传入x。在这种情况下,我们可以将其替换为对所需方法的引用。像这样:

System.out::println
没错——末尾没有括号!这是一个更完整的例子:

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

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
在最后一行,我们使用forEach()方法,它接受一个实现接口的对象Consumer。同样,这是一个函数式接口,只有一个void accept(T t)方法。因此,我们编写了一个只有一个参数的 lambda 表达式(因为它是在接口本身中键入的,所以我们不指定参数类型;我们只表示我们将调用它x)。accept()在 lambda 表达式的主体中,我们编写了调用方法时将执行的代码。这里我们简单地显示变量中的结果x。这个相同的forEach()方法遍历集合中的所有元素并调用accept()方法的实现Consumer接口(我们的 lambda),传入集合中的每个项目。正如我所说,我们可以用对所需方法的引用替换这样的 lambda 表达式(一个简单地对不同方法进行分类的表达式)。那么我们的代码将如下所示:

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

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
主要是println()accept()方法的参数要匹配。因为该方法可以接受任何东西(它为所有基元类型和所有对象重载),而不是 lambda 表达式,我们可以简单地将对该方法的println()引用传递给. 然后将获取集合中的每个元素并将其直接传递给方法。对于第一次遇到这种情况的任何人,请注意我们不是在调用(单词之间有点,末尾有括号)。相反,我们正在传递对此方法的引用。如果我们这样写 println()forEach()forEach()println()System.out.println()

strings.forEach(System.out.println());
我们将有一个编译错误。在调用 之前forEach(),Java 看到它System.out.println()正在被调用,因此它知道返回值是void并将尝试传递voidforEach(),而后者期待的是一个Consumer对象。

方法引用的语法

这很简单:
  1. 我们像这样传递对静态方法的引用:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. 我们使用现有对象传递对非静态方法的引用,如下所示:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. 我们使用实现它的类传递对非静态方法的引用,如下所示:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. 我们像这样传递对构造函数的引用:ClassName::new

    当您已经拥有可以完美用作回调的方法时,方法引用非常方便。在这种情况下,不是编写包含方法代码的 lambda 表达式,也不是编写仅调用该方法的 lambda 表达式,我们只是传递对它的引用。就是这样。

匿名类和 lambda 表达式之间有趣的区别

在匿名类中,this关键字指向匿名类的一个对象。但是如果我们在 lambda 中使用它,我们就可以访问包含类的对象。我们实际编写 lambda 表达式的地方。发生这种情况是因为 lambda 表达式被编译到它们所在类的私有方法中。我不建议使用此“功能”,因为它有副作用并且与函数式编程的原则相矛盾。也就是说,这种方法完全符合 OOP。;)

我从哪里得到我的信息,你还应该阅读什么?

当然,我在 Google 上找到了很多东西 :)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION