Heim  >  Artikel  >  Java  >  Was ist ein Lambda-Ausdruck? Was ist der Nutzen?

Was ist ein Lambda-Ausdruck? Was ist der Nutzen?

零下一度
零下一度Original
2017-07-20 14:16:583293Durchsuche

In diesem Abschnitt besprechen wir zunächst den Lambda-Ausdruck. Was ist das? Was ist der Nutzen?

Lambda-Ausdruck ist eine neue Syntax, die in Java 8 eingeführt wurde. ist eine kompakte Möglichkeit, Code zu übertragen Wir werden nicht näher darauf eingehen.

Um Lambda-Ausdrücke zu verstehen, schauen wir uns zunächst Schnittstellen, anonyme innere Klassen und Codeübergabe an.

Code über Schnittstellen übertragen

Wir haben in Abschnitt 19 Schnittstellen und schnittstellenorientierte Programmierung eingeführt, die Programmierung für Schnittstellen statt für bestimmte Typen. Das ist möglich Reduzieren Sie die Programmkopplung, verbessern Sie die Flexibilität und verbessern Sie die Wiederverwendbarkeit. Schnittstelle wird häufig zum Übertragen von Code verwendet Beispielsweise haben wir in Abschnitt 59 die folgenden Dateimethoden eingeführt:

public String[] list(FilenameFilter filter)public File[] listFiles(FilenameFilter filter)

Was list und listFiles benötigen, ist nicht das FilenameFilter-Objekt, sondern die folgenden darin enthaltenen Methoden:

boolean accept(File dir, String name);

Mit anderen Worten: list und listFiles wollen einen Absatz akzeptieren Der Methodencode wird als Parameter verwendet, es gibt jedoch keine Möglichkeit, den Methodencode selbst direkt zu übergeben, sondern nur eine Schnittstelle.

Als weiteres Beispiel haben wir in Abschnitt 53 einige Algorithmen von Sammlungen vorgestellt. Viele Methoden akzeptieren einen Parameter Komparator, wie zum Beispiel:

public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)public static <T> void sort(List<T> list, Comparator<? super T> c)

Was sie brauchen, ist nicht das Comparator-Objekt, sondern die folgenden darin enthaltenen Methoden:

int compare(T o1, T o2);

Es gibt jedoch keine Möglichkeit, das zu übergeben Methode direkt, nur Kann eine Schnittstelle übergeben.

Wir haben den asynchronen Aufgabenausführungsdienst ExecutorService in Abschnitt 77 eingeführt. Die Methoden zum Übermitteln von Aufgaben sind:

<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

Aufrufbare und ausführbare Schnittstellen werden auch zum Übertragen von Aufgaben verwendet Codes.

Um Verhaltenscode über eine Schnittstelle zu übergeben, müssen Sie ein Instanzobjekt übergeben, das die Schnittstelle implementiert. In den vorherigen Kapiteln ist die Verwendung anonymer innerer Klassen am prägnantesten, wie zum Beispiel:

//列出当前目录下的所有后缀为.txt的文件File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Overridepublic boolean accept(File dir, String name) {if(name.endsWith(".txt")){return true;
        }return false;
    }
});

Dateien nach Dateinamen sortieren, der Code lautet:

Arrays.sort(files, new Comparator<File>() {

    @Overridepublic int compare(File f1, File f2) {return f1.getName().compareTo(f2.getName());
    }
});

Senden Sie die einfachste Aufgabe, der Code lautet:

ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
    @Overridepublic void run() {
        System.out.println("hello world");
    }
});

Lambda-Ausdruck

Syntax

Java 8 Bietet eine neue kompakte Syntax zum Übergeben von Code – Lambda-Ausdrücke. Für das Beispiel der oben aufgeführten Dateien kann der Code wie folgt geändert werden:

File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {if (name.endsWith(".txt")) {return true;
    }return false;
});

Es ist ersichtlich, dass die Codeübergabe im Vergleich zu anonymen inneren Klassen intuitiver wird und nicht mehr Der Vorlagencode der Schnittstelle deklariert die Methode nicht mehr und hat auch keinen Namen, sondern gibt direkt den Implementierungscode der Methode an. Der Lambda-Ausdruck ist durch -> in zwei Teile unterteilt. Der erste Teil sind die Parameter der Methode und der folgende Teil {} ist der Code der Methode.

Der obige Code kann wie folgt vereinfacht werden:

File[] files = f.listFiles((File dir, String name) -> {return name.endsWith(".txt");
});

Wenn der Hauptcode nur eine Anweisung enthält, können die Klammern und die Return-Anweisung auch weggelassen werden Der obige Code kann wie folgt lauten:

File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));

Beachten Sie, dass der Hauptcode ein Ausdruck ist und der Wert dieses Ausdrucks der Rückgabewert der Funktion A ist Am Ende der Return-Anweisung kann kein Semikolon hinzugefügt werden.

Die Parametertypdeklaration der Methode kann auch weggelassen werden. Der obige Code kann weiter vereinfacht werden zu:

File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

Der Grund, warum der Parametertyp der Methode kann weggelassen werden, weil Java Es kann automatisch gefolgert werden, dass der von listFiles akzeptierte Parametertyp FilenameFilter ist. Diese Schnittstelle hat nur eine Methode akzeptiert, und die beiden Parametertypen dieser Methode sind File und String.

Ist der Code durch diese Vereinfachung viel einfacher und klarer?

Der Sortiercode kann mit Lambda-Ausdrücken wie folgt geschrieben werden:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

Der Code zum Senden der Aufgabe kann mit dem Lambda-Ausdruck wie folgt geschrieben werden:

executor.submit(()->System.out.println("hello"));

Der Parameterteil ist leer und wird als () geschrieben.

Wenn nur ein Parameter vorhanden ist, können die Klammern im Parameterteil weggelassen werden. Beispielsweise verfügt File auch über die folgenden Methoden:

public File[] listFiles(FileFilter filter)

FileFilter ist definiert als:

public interface FileFilter {boolean accept(File pathname);
}

Verwenden Sie FileFilter, um das obige Beispiel für die Aufzählung von Dateien neu zu schreiben. Der Code kann sein:

File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

Variablenreferenz

Ähnlich wie anonyme innere Klassen können Lambda-Ausdrücke auch auf Variablen zugreifen, die außerhalb des Hauptcodes definiert sind. Bei lokalen Variablen kann jedoch nur auf Variablen vom endgültigen Typ zugegriffen werden. Der Unterschied zu anonymen inneren Klassen ist das Es ist nicht erforderlich, dass die Variable als endgültig deklariert wird, die Variable kann jedoch nicht tatsächlich neu zugewiesen werden. Beispiel:

String msg = "hello world";
executor.submit(()->System.out.println(msg));

kann auf die lokale Variable msg zugreifen, aber msg kann nicht neu zugewiesen werden. Wenn Sie so schreiben:

String msg = "hello world";
msg = "good morning";
executor.submit(()->System.out.println(msg));

Java编译器会提示错误。

这个原因与匿名内部类是一样的,Java会将msg的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的msg变量。如果允许msg被修改,则程序员可能会误以为Lambda表达式会读到修改后的值,引起更多的混淆。

为什么非要建副本,直接访问外部的msg变量不行吗?不行,因为msg定义在栈中,当Lambda表达式被执行的时候,msg可能早已被释放了。如果希望能够修改值,可以将变量定义为实例变量,或者,将变量定义为数组,比如:

String[] msg = new String[]{"hello world"};
msg[0] = "good morning";
executor.submit(()->System.out.println(msg[0]));

与匿名内部类比较

从以上内容可以看出,Lambda表达式与匿名内部类很像,主要就是简化了语法,那它是不是语法糖,内部实现其实就是内部类呢?答案是否定的,Java会为每个匿名内部类生成一个类,但Lambda表达式不会,Lambda表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会受到影响。

Java利用了Java 7引入的为支持动态类型语言引入的invokedynamic指令、方法句柄(method handle)等,具体实现比较复杂,我们就不探讨了,感兴趣可以参看~briangoetz/lambda/lambda-translation.html,我们需要知道的是,Java的实现是非常高效的,不用担心生成太多类的问题。

Lambda表达式不是匿名内部类,那它的类型到底是什么呢?是函数式接口。

函数式接口

Java 8引入了函数式接口的概念,函数式接口也是接口,但只能有一个抽象方法,前面提及的接口都只有一个抽象方法,都是函数式接口。之所以强调是"抽象"方法,是因为Java 8中还允许定义其他方法,我们待会会谈到。Lambda表达式可以赋值给函数式接口,比如:

FileFilter filter = path -> path.getName().endsWith(".txt");
FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");
Comparator<File> comparator = (f1, f2) -> f1.getName().compareTo(f2.getName());
Runnable task = () -> System.out.println("hello world");

如果看这些接口的定义,会发现它们都有一个注解@FunctionalInterface,比如:

@FunctionalInterfacepublic interface Runnable {public abstract void run();
}

@FunctionalInterface用于清晰地告知使用者,这是一个函数式接口,不过,这个注解不是必需的,不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java编译器会报错,这类似于我们在85节介绍的Override注解。

预定义的函数式接口

接口列表

Java 8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包java.util.function下,主要的有:

对于基本类型boolean, int, long和double,为避免装箱/拆箱,Java 8提供了一些专门的函数,比如,int相关的主要函数有:

这些函数有什么用呢?它们被大量使用于Java 8的函数式数据处理Stream相关的类中,关于Stream,我们下节介绍。

即使不使用Stream,也可以在自己的代码中直接使用这些预定义的函数,我们看一些简单的示例。

Predicate示例

为便于举例,我们先定义一个简单的学生类Student,有name和score两个属性,如下所示,我们省略了getter/setter方法。

static class Student {
    String name;double score;    public Student(String name, double score) {this.name = name;this.score = score;
    }
}

有一个学生列表:

List<Student> students = Arrays.asList(new Student[] {new Student("zhangsan", 89d),new Student("lisi", 89d),new Student("wangwu", 98d) });

在日常开发中,列表处理的一个常见需求是过滤,列表的类型经常不一样,过滤的条件也经常变化,但主体逻辑都是类似的,可以借助Predicate写一个通用的方法,如下所示:

public static <E> List<E> filter(List<E> list, Predicate<E> pred) {
    List<E> retList = new ArrayList<>();for (E e : list) {if (pred.test(e)) {
            retList.add(e);
        }
    }return retList;
}

这个方法可以这么用:

// 过滤90分以上的students = filter(students, t -> t.getScore() > 90);

Function示例

列表处理的另一个常见需求是转换,比如,给定一个学生列表,需要返回名称列表,或者将名称转换为大写返回,可以借助Function写一个通用的方法,如下所示:

public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
    List<R> retList = new ArrayList<>(list.size());for (T e : list) {
        retList.add(mapper.apply(e));
    }return retList;
}

根据学生列表返回名称列表的代码可以为:

List<String> names = map(students, t -> t.getName());

将学生名称转换为大写的代码可以为:

students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));

Consumer示例

在上面转换学生名称为大写的例子中,我们为每个学生创建了一个新的对象,另一种常见的情况是直接修改原对象,具体怎么修改通过代码传递,这时,可以用Consumer写一个通用的方法,比如:

public static <E> void foreach(List<E> list, Consumer<E> consumer) {for (E e : list) {
        consumer.accept(e);
    }
}

上面转换为大写的例子可以改为:

foreach(students, t -> t.setName(t.getName().toUpperCase()));

以上这些示例主要用于演示函数式接口的基本概念,实际中应该使用下节介绍的流API。

方法引用

基本用法
Lambda表达式经常就是调用对象的某个方法,比如:

List<String> names = map(students, t -> t.getName());

这时,它可以进一步简化,如下所示:

List<String> names = map(students, Student::getName);

Student::getName这种写法,是Java 8引入的一种新语法,称之为方法引用,它是Lambda表达式的一种简写方法,由::分隔为两部分,前面是类名或变量名,后面是方法名。方法可以是实例方法,也可以是静态方法,但含义不同。

我们看一些例子,还是以Student为例,先增加一个静态方法:

public static String getCollegeName(){return "Laoma School";
}

静态方法

对于静态方法,如下语句:

Supplier<String> s = Student::getCollegeName;

等价于:

Supplier<String> s = () -> Student.getCollegeName();

它们的参数都是空,返回类型为String。

实例方法

而对于实例方法,它第一个参数就是该类型的实例,比如,如下语句:

Function<Student, String> f = Student::getName;

等价于:

Function<Student, String> f = (Student t) -> t.getName();

对于Student::setName,它是一个BiConsumer,即:

BiConsumer<Student, String> c = Student::setName;

等价于:

BiConsumer<Student, String> c = (t, name) -> t.setName(name);

通过变量引用方法

如果方法引用的第一部分是变量名,则相当于调用那个对象的方法,比如:

Student t = new Student("张三", 89d);
Supplier<String> s = t::getName;

等价于:

Supplier<String> s = () -> t.getName();

而:

Consumer<String> consumer = t::setName;

等价于:

Consumer<String> consumer = (name) -> t.setName(name);

构造方法

对于构造方法,方法引用的语法是<类名>::new,如Student::new,如下语句:

BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);

等价于:

BiFunction<String, Double, Student> s = Student::new;

函数的复合

在前面的例子中,函数式接口都用作方法的参数,其他部分通过Lambda表达式传递具体代码给它,函数式接口和Lambda表达式还可用作方法的返回值,传递代码回调用者,将这两种用法结合起来,可以构造复合的函数,使程序简洁易读。

下面我们会看一些例子,在介绍例子之前,我们先需要介绍Java 8对接口的增强。

接口的静态方法和默认方法

在Java 8之前,接口中的方法都是抽象方法,都没有实现体,Java 8允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体,比如:

public interface IDemo {void hello();public static void test() {
        System.out.println("hello");
    }default void hi() {
        System.out.println("hi");
    }
}

test()就是一个静态方法,可以通过IDemo.test()调用。在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Collection接口有一个对应的单独的类Collections,在Java 8中,就可以直接写在接口中了,比如Comparator接口就定义了多个静态方法。

hi()是一个默认方法,由关键字default标识,默认方法与抽象方法都是接口的方法,不同在于,它有默认的实现,实现类可以改变它的实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。

在没有默认方法之前,Java是很难给接口增加功能的,比如List接口,因为有太多非Java JDK控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java 上运行,必须改写代码,实现新的方法,这显然是无法接受的。函数式数据处理需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新方法,而接口现有的实现类也不需要必须实现它。

看一些例子,List接口增加了sort方法,其定义为:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

Collection接口增加了stream方法,其定义为:

default Stream<E> stream() {return StreamSupport.stream(spliterator(), false);
}

需要说明的是,即使能定义方法体了,接口与抽象类还是不一样的,接口中不能定义实例变量,而抽象类可以。

了解了静态方法和默认方法,我们看一些利用它们实现复合函数的例子。

Comparator中的复合方法

Comparator接口定义了如下静态方法:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

这个方法是什么意思呢?它用于构建一个Comparator,比如,在前面的例子中,对文件按照文件名排序的代码为:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

使用comparing方法,代码可以简化为:

Arrays.sort(files, Comparator.comparing(File::getName));

这样,代码的可读性是不是大大增强了?comparing方法为什么能达到这个效果呢?它构建并返回了一个符合Comparator接口的Lambda表达式,这个Comparator接受的参数类型是File,它使用了传递过来的函数代码keyExtractor将File转换为String进行比较。像comparing这样使用复合方式构建并传递代码并不容易阅读和理解,但调用者很方便,也很容易理解。

Comparator还有很多默认方法,我们看两个:

default Comparator<T> reversed() {return Collections.reverseOrder(this);
}default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);return (Comparator<T> & Serializable) (c1, c2) -> {int res = compare(c1, c2);return (res != 0) ? res : other.compare(c1, c2);
    };
}

reversed返回一个新的Comparator,按原排序逆序排。thenComparing也是一个返回一个新的Comparator,在原排序认为两个元素排序相同的时候,使用提供的other Comparator进行比较。

看一个使用的例子,将学生列表按照分数倒序排(高分在前),分数一样的,按照名字进行排序,代码如下所示:

students.sort(Comparator.comparing(Student::getScore)
                        .reversed()
                        .thenComparing(Student::getName));

这样,代码是不是很容易读?

java.util.function中的复合方法

在java.util.function包中的很多函数式接口里,都定义了一些复合方法,我们看一些例子。

Function接口有如下定义:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);return (T t) -> after.apply(apply(t));
}

先将T类型的参数转化为类型R,再调用after将R转换为V,最后返回类型V。

还有如下定义:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);return (V v) -> apply(before.apply(v));
}

对V类型的参数,先调用before将V转换为T类型,再调用当前的apply方法转换为R类型返回。

Consumer, Predicate等都有一些复合方法,它们大量被用于下节介绍的函数式数据处理API中,具体我们就不探讨了。

Das obige ist der detaillierte Inhalt vonWas ist ein Lambda-Ausdruck? Was ist der Nutzen?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn