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

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

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

    • 类、对象以及它们之间的区别;
    • 接口,它们与类的区别,以及接口和类之间的关系;
    • 方法,如何调用它们,抽象方法(即没有实现的方法),方法参数,方法参数以及如何传递它们;
    • 访问修饰符、静态方法/变量、final 方法/变量;
    • 类和接口的继承,接口的多重继承。
  2. Java核心知识:泛型类型(generics)、集合(lists)、线程。
好吧,让我们开始吧。

一点历史

Lambda 表达式来自函数式编程,又来自数学。20世纪中叶的美国,酷爱数学和各种抽象概念的Alonzo Church在普林斯顿大学工作。是阿隆佐丘奇发明了 lambda 演算,它最初是一组与编程完全无关的抽象思想。艾伦图灵和约翰冯诺依曼等数学家同时在普林斯顿大学工作。万事俱备:Church 提出了 lambda 演算。图灵开发了他的抽象计算机,现在被称为“图灵机”。冯诺依曼提出了一种计算机体系结构,它构成了现代计算机的基础(现在称为“冯诺依曼体系结构”)。当时,Alonzo Church' 他的想法并没有像他的同事们的作品那样广为人知(纯数学领域除外)。然而,不久之后约翰麦卡锡(也是普林斯顿大学的毕业生,在我们的故事发生时,他是麻省理工学院的一名雇员)开始对丘奇的想法产生兴趣。1958 年,他基于这些想法创建了第一个函数式编程语言 LISP。58 年后,函数式编程的思想渗透到 Java 8 中。甚至还不到 70 年……老实说,这不是将数学思想应用到实践中花费的最长时间。麻省理工学院的一名雇员)对丘奇的想法产生了兴趣。1958 年,他基于这些想法创建了第一个函数式编程语言 LISP。58 年后,函数式编程的思想渗透到 Java 8 中。甚至还不到 70 年……老实说,这不是将数学思想应用到实践中花费的最长时间。麻省理工学院的一名雇员)对丘奇的想法产生了兴趣。1958 年,他基于这些想法创建了第一个函数式编程语言 LISP。58 年后,函数式编程的思想渗透到 Java 8 中。甚至还不到 70 年……老实说,这不是将数学思想应用到实践中花费的最长时间。

事件的核心

lambda 表达式是一种函数。您可以将其视为普通的 Java 方法,但具有作为参数传递给其他方法的独特能力。这是正确的。不仅可以将数字、字符串和猫传递给方法,还可以传递其他方法!我们什么时候可能需要这个?这将很有帮助,例如,如果我们想传递一些回调方法。也就是说,如果我们需要我们调用的方法能够调用我们传递给它的其他方法。换句话说,我们有能力在某些情况下传递一个回调,而在其他情况下传递不同的回调。这样我们接收回调的方法就会调用它们。排序是一个简单的例子。假设我们正在编写一些聪明的排序算法,如下所示:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
if语句中,我们调用compare()方法,传入两个要比较的对象,我们想知道这些对象中哪个“更大”。我们假设“大”的先于“小”的。我将“更大”放在引号中,因为我们正在编写一个通用方法,它不仅知道如何按升序排序,而且还知道如何按降序排序(在这种情况下,“大”对象实际上是“小”对象,反之亦然)。要为我们的排序设置特定的算法,我们需要一些机制将其传递给我们的mySuperSort()方法。这样我们就可以在调用时“控制”我们的方法。当然,我们可以编写两个单独的方法——mySuperSortAscend()并且mySuperSortDescend()— 用于按升序和降序排序。或者我们可以将一些参数传递给该方法(例如,一个布尔变量;如果为真,则按升序排序,如果为假,则按降序排序)。但是,如果我们想对一些复杂的东西(例如字符串数组列表)进行排序怎么办?我们的方法如何mySuperSort()知道如何对这些字符串数组进行排序?按尺寸?通过所有单词的累积长度?也许基于数组中的第一个字符串按字母顺序排列?如果我们需要在某些情况下按数组大小对数组列表进行排序,而在其他情况下按每个数组中所有单词的累积长度怎么办?我希望您已经听说过比较器,在这种情况下,我们只需将描述所需排序算法的比较器对象传递给我们的排序方法。因为标准sort()方法是基于与 相同的原理实现的,我将在示例中 mySuperSort()使用。sort()

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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[]> sortByCumulativeWordLength = 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. Dota GTA5 Halo
  2. if then else
  3. I really love Java
这里的数组按每个数组中的单词数排序。单词较少的数组被认为是“较少”。这就是为什么它是第一位的。包含更多单词的数组被认为“更大”并放在最后。如果我们将不同的比较器传递给该sort()方法,例如sortByCumulativeWordLength,那么我们将得到不同的结果:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
现在 are 数组按数组单词中的字母总数排序。在第一个数组中,有 10 个字母,在第二个中有 12 个,在第三个中有 15 个。如果我们只有一个比较器,那么我们不必为它声明一个单独的变量。相反,我们可以在调用方法时简单地创建一个匿名类sort()。是这样的:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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; 
    } 
});
这里我们获取我们的数组列表并调用它的sort()方法,我们用一个方法将一个比较器对象传递给它compare()(它的名字对我们来说并不重要——毕竟,它是这个对象的唯一方法,所以我们不会出错)。此方法有两个我们将使用的参数。如果您在 IntelliJ IDEA 中工作,您可能会看到它可以显着压缩代码,如下所示:

arrays.sort((o1, o2) -> o1.length - o2.length);
这将六行减少为一个短行。6行改写为一小段。有些东西消失了,但我保证这不是什么重要的东西。此代码的工作方式与匿名类的工作方式完全相同。 任务 2.猜猜使用 lambda 表达式重写任务 1 的解决方案(至少,要求 IntelliJ IDEA 将您的匿名类转换为 lambda 表达式)。

让我们谈谈接口

原则上,接口只是抽象方法的列表。当我们创建一个实现某个接口的类时,我们的类必须实现接口中包含的方法(或者我们必须使类抽象)。有包含许多不同方法的接口(例如 List),也有仅包含一种方法的接口(例如ComparatorRunnable)。有些接口没有单一方法(所谓的标记接口,例如Serializable)。只有一种方法的接口也称为功能接口。在 Java 8 中,它们甚至标有特殊的注解:@FunctionalInterface. 正是这些单一方法接口适合作为 lambda 表达式的目标类型。正如我上面所说,lambda 表达式是一种包装在对象中的方法。当我们传递这样一个对象时,我们实际上是在传递这个单一的方法。事实证明,我们不关心调用什么方法。对我们来说唯一重要的是方法参数,当然还有方法的主体。本质上,lambda 表达式是函数式接口的实现。无论我们在何处看到具有单一方法的接口,都可以将匿名类重写为 lambda。如果接口的方法多于或少于一个,那么 lambda 表达式将不起作用,我们将改为使用匿名类甚至普通类的实例。现在是深入研究 lambda 的时候了。:)

句法

一般语法是这样的:

(parameters) -> {method body}
即,方法参数周围的圆括号、“箭头”(由连字符和大于号组成),然后是大括号中的方法主体,一如既往。参数对应于接口方法中指定的参数。如果编译器可以明确地确定变量类型(在我们的例子中,它知道我们正在使用字符串数组,因为我们的List对象是使用String[] 类型化的),那么您不必指明它们的类型。
如果它们不明确,则指出类型。如果不需要,IDEA 会将其着色为灰色。
您可以在此Oracle 教程和其他地方阅读更多内容。这称为“目标类型”。您可以随意命名变量——您不必使用界面中指定的相同名称。如果没有参数,则仅指示空括号。如果只有一个参数,只需指明变量名即可,不带括号。现在我们了解了参数,是时候讨论 lambda 表达式的主体了。在大括号内,您可以像编写普通方法一样编写代码。如果您的代码只有一行,那么您可以完全省略花括号(类似于 if 语句和 for 循环)。如果您的单行 lambda 返回某些内容,则您不必包含return陈述。但是如果你使用大括号,那么你必须显式地包含一个return语句,就像你在普通方法中所做的那样。

例子

示例 1。

() -> {}
最简单的例子。最没有意义的 :),因为它什么都不做。 示例 2。

() -> ""
另一个有趣的例子。它什么都不带,返回一个空字符串(return被省略,因为它是不必要的)。这是同样的事情,但是有return

() -> { 
    return ""; 
}
示例 3. “你好,世界!” 使用 lambda

() -> System.out.println("Hello, World!")
它什么都不接受,什么也不返回(我们不能return在对 的调用之前放置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 表达式保存为一个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. 因此,此代码将创建一个对象,该对象的方法不接受或不返回任何内容。Runnable这与接口的方法完美匹配run()。这就是我们能够将此 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]); 
}
同样,这是一个没有参数和void返回类型的 lambda 表达式(因为没有return语句)。  例 6

x -> x
这里我们获取一个x变量并返回它。请注意,如果只有一个参数,则可以省略它周围的括号。这是同样的事情,但有括号:

(x) -> x
这是一个带有显式返回语句的示例:

x -> { 
    return x;
}
或者像这样带有括号和返回语句:

(x) -> { 
    return x;
}
或者使用类型的明确指示(因此带有括号):

(int x) -> x
例 7

x -> ++x
我们获取x并返回它,但仅在加 1 之后。您可以像这样重写该 lambda:

x -> x + 1
在这两种情况下,我们都省略了参数和方法主体以及语句周围的括号return,因为它们是可选的。示例 6 中给出了带有括号和 return 语句的版本。 示例 8

(x, y) -> 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对象、一个String名称和一个整数年龄。在方法本身中,我们使用传递的名称和年龄来设置猫的变量。因为我们的cat对象是引用类型,它会在 lambda 表达式之外被改变(它会得到传递的名字和年龄)。这是使用类似 lambda 的稍微复杂的版本:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

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

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

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

class Cat implements HasNameAndAge { 
    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='Smokey', age=3}
如您所见,Cat对象有一个状态,然后在我们使用 lambda 表达式后状态发生了变化。Lambda 表达式与泛型完美结合。如果我们需要创建一个Dog也实现了的类,那么我们可以在方法HasNameAndAge中执行相同的操作, 而无需更改 lambda 表达式。 任务 3.编写一个函数式接口,其中包含一个接受数字并返回布尔值的方法。将此类接口的实现编写为 lambda 表达式,如果传递的数字可被 13 整除,则返回 true。 任务 4。Dogmain()使用接受两个字符串并返回一个字符串的方法编写函数式接口。将此类接口的实现编写为返回较长字符串的 lambda 表达式。 任务 5.编写一个函数式接口,其方法接受三个浮点数:a、b 和 c,并返回一个浮点数。将此类接口的实现编写为返回判别式的 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