JavaRush /Java 博客 /Random-ZH /Java 中流行的 lambda 表达式。带有示例和任务。第1部分
Стас Пасинков
第 26 级
Киев

Java 中流行的 lambda 表达式。带有示例和任务。第1部分

已在 Random-ZH 群组中发布
这篇文章适合谁?
  • 对于那些认为自己已经很了解 Java Core,但对 Java 中的 lambda 表达式一无所知的人。或者,也许您已经听说过有关 lambda 的一些内容,但没有了解详细信息。
  • 适合那些对 lambda 表达式有一定了解,但仍然害怕和不习惯使用它们的人。
如果您不属于这些类别之一,您很可能会发现这篇文章无聊、不正确,而且通常“不酷”。在这种情况下,请随意浏览,或者,如果您精通该主题,请在评论中建议我如何改进或补充本文。该材料不具有任何学术价值,更不用说新颖性了。相反,相反:在其中我将尝试尽可能简单地描述复杂的(对某些人来说)事物。我受到解释流 api 的请求的启发而写下这篇文章。我想了想,认为如果不理解 lambda 表达式,我的一些关于“流”的示例将难以理解。那么让我们从 lambda 开始吧。 Java 中流行的 lambda 表达式。 带有示例和任务。 第 1 - 1 部分理解本文需要哪些知识:
  1. 对面向对象编程(以下简称OOP)的理解,即:
    • 了解什么是类和对象,它们之间有什么区别;
    • 了解接口是什么、它们与类有何不同、它们之间的联系(接口和类)是什么;
    • 了解什么是方法,如何调用它,什么是抽象方法(或没有实现的方法),方法的参数/参数是什么,如何将它们传递到那里;
    • 访问修饰符、静态方法/变量、最终方法/变量;
    • 继承(类、接口、接口的多重继承)。
  2. Java 核心知识:泛型、集合(列表)、线程。
好吧,让我们开始吧。

一点历史

Lambda 表达式源自函数式编程和数学。20世纪中叶的美国,有一位在普林斯顿大学工作的阿朗佐·丘奇(Alonzo Church),非常喜欢数学和各种抽象概念。lambda 演算是 Alonzo Church 提出的,起初它只是一些抽象概念的集合,与编程无关。与此同时,阿兰·图灵和约翰·冯·诺依曼等数学家也在同一所普林斯顿大学工作。一切都走到了一起:丘奇提出了 lambda 演算系统,图灵开发了他的抽象计算机,现在被称为“图灵机”。冯·诺依曼提出了计算机体系结构图,它构成了现代计算机的基础(现在称为“冯·诺依曼体系结构”)。当时,阿朗佐·丘奇的思想并没有像他的同事们的工作那样享有盛誉(“纯”数学领域除外)。然而,不久之后,约翰·麦卡锡(也是普林斯顿大学的毕业生,在故事发生时是麻省理工学院的员工)对丘奇的想法产生了兴趣。基于它们,他于 1958 年创建了第一种函数式编程语言 Lisp。58年后,函数式编程的思想以数字8的形式渗透到Java中。甚至还不到70年……事实上,这并不是数学思想在实践中应用的最长时期。

精华

lambda 表达式就是这样一个函数。您可以将其视为 Java 中的常规方法,唯一的区别是它可以作为参数传递给其他方法。是的,不仅可以将数字、字符串和猫传递给方法,还可以传递其他方法!我们什么时候可能需要它?例如,如果我们想传递一些回调。我们需要我们调用的方法能够调用我们传递给它的其他方法。也就是说,这样我们就有机会在某些情况下传输一个回调,而在其他情况下传输另一个回调。我们的方法将接受回调并调用它们。一个简单的例子就是排序。假设我们编写了某种棘手的排序,如下所示:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
在哪里,if我们调用方法compare(),传递两个我们比较的对象,我们想找出这些对象中哪个“更大”。我们将把“多”放在“小”之前。我在引号中写了“更多”,因为我们正在编写一个通用方法,它不仅能够按升序排序,还能够按降序排序(在这种情况下,“更多”将是本质上更小的对象,反之亦然) 。为了准确地设置我们想要的排序规则,我们需要以某种方式将其传递给我们的mySuperSort(). 在这种情况下,我们将能够在调用我们的方法时以某种方式“控制”它。当然,您可以编写两个单独的方法mySuperSortAsc()mySuperSortDesc()按升序和降序排序。或者在方法内部传递一些参数(例如,booleanif true,按升序排序, if ,按false降序排序)。但是,如果我们想要排序的不是一些简单的结构,而是字符串数组列表,该怎么办?我们的方法如何mySuperSort()知道如何对这些字符串数组进行排序?要尺寸吗?按单词总长度?也许按字母顺序排列,取决于数组中的第一行?但是,如果在某些情况下,我们需要按数组的大小对数组列表进行排序,而在另一种情况下,则需要按数组中单词的总长度对数组列表进行排序,该怎么办?我想您已经听说过比较器,在这种情况下,我们只需将比较器对象传递给我们的排序方法,在该方法中我们描述我们想要排序的规则。由于标准方法sort()的实现原理与标准方法相同,因此mySuperSort()在示例中我将使用标准方法sort()
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
结果:
  1. 妈妈洗了相框
  2. 和平工党可能
  3. 我真的很喜欢java
这里,数组按每个数组中的单词数排序。单词数较少的数组被认为“较小”。这就是为什么它在一开始就出现了。单词数较多的被认为是“更多”,并最终出现在最后。如果sort()我们将另一个比较器传递给方法(sortByWordsLength),那么结果将会不同:
  1. 和平工党可能
  2. 妈妈洗了相框
  3. 我真的很喜欢java
现在,数组按照该数组的单词中的字母总数排序。第一种情况有 10 个字母,第二个有 12 个字母,第三个有 15 个字母。如果我们只使用一个比较器,那么我们就不能为它创建一个单独的变量,而只需在调用该方法的时间sort()。像那样:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
结果将与第一种情况相同。 任务1。重写此示例,以便它不按数组中单词数的升序对数组进行排序,而是按降序排序。这一切我们都已经知道了。我们知道如何将对象传递给方法,我们可以根据我们当前的需要将这个或那个对象传递给方法,并且在我们传递这样一个对象的方法内部,我们为其编写实现的方法将被调用。问题来了:lambda 表达式与它有什么关系? 假设 lambda 是一个仅包含一个方法的对象。它就像一个方法对象。包装在对象中的方法。它们只是有一个稍微不寻常的语法(稍后会详细介绍)。 让我们再看一下这个条目
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
在这里,我们获取列表arrays并调用其方法sort(),其中我们通过一个方法传递一个比较器对象compare()(它的名称对我们来说并不重要,因为它是该对象中唯一的一个,我们不会错过它)。该方法需要两个参数,我们接下来将使用这两个参数。如果您使用IntelliJ IDEA,您可能已经看到它如何为您提供此代码以显着缩短:
arrays.sort((o1, o2) -> o1.length - o2.length);
就这样,六行变成了短短的一行。6行被重写为一小段。有些东西消失了,但我保证没有任何重要的东西消失,并且此代码的​​工作方式与匿名类完全相同。 任务2。弄清楚如何使用 lambda 重写问题 1 的解决方案(作为最后的手段,请要求IntelliJ IDEA将您的匿名类转换为 lambda)。

我们来谈谈接口

基本上,接口只是抽象方法的列表。当我们创建一个类并说它将实现某种接口时,我们必须在类中编写接口中列出的方法的实现(或者,作为最后的手段,不编写它,而是使类抽象)。有的接口具有多种不同的方法(例如List),有的接口仅具有一种方法(例如,相同的 Comparator 或 Runnable)。有些接口根本没有单一方法(所谓的标记接口,例如 Serialized)。那些只有一种方法的接口也称为函数式接口。在 Java 8 中,它们甚至用特殊的@FunctionalInterface注释进行标记。它是具有适合 lambda 表达式使用的单一方法的接口。正如我上面所说,lambda 表达式是包装在对象中的方法。当我们在某处传递这样一个对象时,实际上,我们传递的是一个方法。事实证明,这个方法叫什么对我们来说并不重要。对我们来说重要的是该方法采用的参数,实际上是方法代码本身。lambda 表达式本质上是。功能接口的实现。当我们看到一个只有一个方法的接口时,这意味着我们可以使用 lambda 重写这样的匿名类。如果接口有多于/少于一个方法,那么 lambda 表达式将不适合我们,我们将使用匿名类,甚至是常规类。是时候深入研究 lambda 了。:)

句法

一般语法是这样的:
(параметры) -> {тело метода}
也就是说,括号内是方法参数,一个“箭头”(这是连续的两个字符:减号和大于号),之后方法的主体像往常一样放在花括号中。参数与描述方法时在接口中指定的参数相对应。如果编译器可以清楚地定义变量的类型(在我们的例子中,可以肯定我们正在使用字符串数组,因为它是List由字符串数组精确键入的),那么变量的类型String[]不需要被写下来。
如果不确定,请指定类型,如果不需要,IDEA 将以灰色突出显示。
例如,您可以在Oracle 教程中阅读更多内容。这称为“目标打字”。您可以为变量指定任何名称,不一定是接口中指定的名称。如果没有参数,则只需括号。如果只有一个参数,则只是变量名,不带括号。我们已经整理了参数,现在介绍 lambda 表达式本身的主体。在大括号内,像常规方法一样编写代码。如果您的整个代码仅由一行组成,则根本不必编写花括号(就像 if 和循环一样)。如果您的 lambda 返回某些内容,但其主体由一行组成,则return根本不需要编写。但如果你有花括号,那么,像通常的方法一样,你需要显式地写return.

例子

示例 1。
() -> {}
最简单的选择。而且是最无意义的:).因为它什么也没做。 示例 2.
() -> ""
也是一个有趣的选择。它不接受任何内容并返回一个空字符串(return因为不必要而被省略)。相同,但具有return
() -> {
    return "";
}
示例 3. 使用 lambda 的 Hello world
() -> System.out.println("Hello world!")
不接收任何内容,也不返回任何内容(我们不能放在returncall 之前System.out.println(),因为方法中的返回类型println() — void)只是在屏幕上显示一个铭文。非常适合实现接口Runnable。相同的示例更完整:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
或者像这样:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
或者我们甚至可以将 lambda 表达式保存为 type 的对象Runnable,然后将其传递给构造函数thread’а
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
让我们仔细看看将 lambda 表达式保存到变量中的那一刻。接口Runnable告诉我们它的对象必须有一个方法public void run()。根据接口,run 方法不接受任何参数作为参数。它不会返回任何内容(void)。因此,当以这种方式编写时,将使用某种不接受或返回任何内容的方法创建一个对象。run()与界面中的方法非常一致Runnable。这就是为什么我们能够将此 lambda 表达式放入诸如 之类的变量中Runnable实施例4
() -> 42
同样,它不接受任何内容,但返回数字 42。这个 lambda 表达式可以放置在 类型的变量中Callable,因为这个接口只定义了一个方法,它看起来像这样:
V call(),
其中V是返回值的类型(在我们的例子中int)。因此,我们可以存储这样的 lambda 表达式,如下所示:
Callable<Integer> c = () -> 42;
示例 5. 多行中的 Lambda
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
同样,这是一个没有参数及其返回类型的 lambda 表达式void(因为没有return)。 实施例6
x -> x
在这里,我们将一些内容放入变量中х并返回它。请注意,如果只接受一个参数,则不需要写括号。相同,但带括号:
(x) -> x
这是一个明确的选项return
x -> {
    return x;
}
或者像这样,用括号和return
(x) -> {
    return x;
}
或者明确指示类型(并相应地使用括号):
(int x) -> x
实施例7
x -> ++x
我们接受х并退回,但要求1更多。你也可以这样重写:
x -> x + 1
在这两种情况下,我们都不在参数、方法体和单词周围指示括号return,因为这是没有必要的。示例 6 中描述了带括号和回车的选项。 示例 8
(x, y) -> x % y
我们接受一些х并返回除以у的余数。这里已经需要参数周围的括号。仅当只有一个参数时,它们才是可选的。像这样明确指示类型: xy
(double x, int y) -> x % y
实施例9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
我们接受一个 Cat 对象、一个带有名称和整数年龄的字符串。在方法本身中,我们将传递的名称和年龄设置为 Cat。cat由于我们的变量是引用类型,因此 lambda 表达式外部的 Cat 对象将会发生变化(它将接收内部传递的姓名和年龄)。使用类似 lambda 的稍微复杂的版本:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
结果: Cat{name='null',age=0} Cat{name='Murzik',age=3} 可以看到,一开始 Cat 对象只有一个状态,但是使用 lambda 表达式后,状态发生了变化。Lambda 表达式与泛型配合得很好。例如,如果我们需要创建一个类Dog,它也将实现WithNameAndAge,那么在方法中main()我们可以使用 Dog 执行相同的操作,而无需更改 lambda 表达式本身。 任务3。使用接受数字并返回布尔值的方法编写函数接口。以 lambda 表达式的形式编写此类接口的实现,true如果传递的数字可以被 13 整除而没有余数,则返回该 表达式。任务 4。使用接受两个字符串并返回相同字符串的方法编写一个函数接口。以返回最长字符串的 lambda 形式编写此类接口的实现。 任务5。使用接受三个小数的方法编写一个函数接口:abc并返回相同的小数。以返回判别式的 lambda 表达式的形式编写此类接口的实现。谁忘记了,D = b^2 - 4ac任务 6 . 使用任务 5 中的函数式接口,编写一个返回运算结果的 lambda 表达式a * b^cJava 中流行的 lambda 表达式。带有示例和任务。第2部分。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION