Maison  >  Article  >  Java  >  Qu'est-ce qu'une expression lambda ? A quoi ça sert ?

Qu'est-ce qu'une expression lambda ? A quoi ça sert ?

零下一度
零下一度original
2017-07-20 14:16:583292parcourir

Dans cette section, nous discutons d'abord de l'expression Lambda, qu'est-ce que c'est ? A quoi ça sert ?

L'expression Lambda est une nouvelle syntaxe introduite dans Java 8. est un moyen compact de transférer du code Son nom vient du calcul lambda dans le monde universitaire. Nous n'en parlerons pas en détail.

Pour comprendre les expressions Lambda, passons d'abord en revue les interfaces, les classes internes anonymes et la transmission de code.

Transférer le code via des interfaces

Nous avons introduit les interfaces et la programmation orientée interface dans la section 19, la programmation pour les interfaces plutôt que pour des types spécifiques, elle peut réduisez le couplage des programmes, améliorez la flexibilité et améliorez la réutilisabilité. L'interface est souvent utilisée pour transmettre du code Par exemple, dans la Section 59 , nous avons introduit les méthodes de fichier suivantes :

public String[] list(FilenameFilter filter)public File[] listFiles(FilenameFilter filter)
Ce dont list et listFiles ont besoin, ce n'est pas l'objet FilenameFilter, mais les méthodes suivantes qu'il contient :

boolean accept(File dir, String name);
En d'autres termes,

list et listFiles je veux accepter un paragraphe Le code de la méthode est utilisé comme paramètre, mais il n'y a aucun moyen de transmettre directement le code de la méthode lui-même, seule une interface peut être transmise.

Pour un autre exemple, nous avons introduit quelques algorithmes de Collections dans la Section 53 De nombreuses méthodes acceptent un paramètre Comparateur, tel que :

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)
Ce dont ils ont besoin, ce n'est pas l'objet Comparator, mais les méthodes suivantes qu'il contient :

int compare(T o1, T o2);
Cependant, il n'y a aucun moyen de transmettre l'objet Comparator. méthode directement, il suffit de passer une interface.

Nous avons introduit le service d'exécution de tâches asynchrones ExecutorService dans la section 77. Les méthodes de soumission des tâches sont :

<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
Les interfaces appelables et exécutables sont également utilisées pour transférer des tâches. codes.

Pour transmettre du code de comportement via une interface, vous devez transmettre un objet instance qui implémente l'interface. Dans les chapitres précédents, la manière la plus concise consiste à utiliser des classes internes anonymes, telles que :

Trier les fichiers par nom de fichier, le code est :
//列出当前目录下的所有后缀为.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;
    }
});

Soumettre la tâche la plus simple, le code est :
Arrays.sort(files, new Comparator<File>() {

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

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

Expression lambdaSyntaxe

Java 8 fournit une nouvelle syntaxe compacte pour transmettre du code - les expressions Lambda. Pour l'exemple des fichiers listés ci-dessus, le code peut être modifié en :

On peut voir que par rapport aux classes internes anonymes, la transmission du code devient plus intuitive et non plus doit être implémenté. Le code modèle de l'interface ne déclare plus la méthode, ni n'a de nom, mais donne directement le code d'implémentation de la méthode. L'expression Lambda est divisée en deux parties par ->. La première partie correspond aux paramètres de la méthode et le {} suivant est le code de la méthode.
File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {if (name.endsWith(".txt")) {return true;
    }return false;
});
Le code ci-dessus peut être simplifié en :

Lorsque le code principal n'a qu'une seule instruction, les parenthèses et l'instruction return peuvent également être omises, et le le code ci-dessus peut devenir :
File[] files = f.listFiles((File dir, String name) -> {return name.endsWith(".txt");
});

Notez que lorsqu'il n'y a pas de parenthèses, le code principal est une expression, et la valeur de cette expression est la valeur de retour de la fonction A. le point-virgule ne peut pas être ajouté à la fin de l'instruction return.
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
La déclaration du type de paramètre de la méthode peut également être omise. Le code ci-dessus peut être encore simplifié en :

La raison pour laquelle le type de paramètre de la méthode. La méthode peut être omise parce que Java On peut automatiquement en déduire qu'il sait que le type de paramètre accepté par listFiles est FilenameFilter. Cette interface n'a qu'une seule méthode acceptée, et les deux types de paramètres de cette méthode sont File et String.
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
Avec cette simplification, le code est-il beaucoup plus simple et plus clair ?

Le code de tri peut être écrit à l'aide d'expressions Lambda comme :

Le code pour soumettre la tâche peut être écrit en utilisant l'expression Lambda comme :
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

La partie paramètre est vide et écrite comme ().
executor.submit(()->System.out.println("hello"));
Lorsqu'il n'y a qu'un seul paramètre, les parenthèses dans la partie paramètre peuvent être omises. Par exemple, File a également les méthodes suivantes :

FileFilter est. défini comme :
public File[] listFiles(FileFilter filter)

Utilisez FileFilter pour réécrire l'exemple ci-dessus d'énumération de fichiers. Le code peut être :
public interface FileFilter {boolean accept(File pathname);
}

<.>Référence de variable
File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

Semblables aux classes internes anonymes, les expressions Lambda peuvent également accéder aux variables définies en dehors du code principal, mais pour les variables locales, elles ne peuvent accéder qu'aux variables de type final. La différence avec les classes internes anonymes. est-ce que cela ne nécessite pas que la variable soit déclarée finale, mais la variable ne peut pas réellement être réaffectée. Par exemple :

peut accéder à la variable locale msg, mais msg ne peut pas être réaffecté. Si vous écrivez comme ceci :
String msg = "hello world";
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中,具体我们就不探讨了。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn