【译】java8之lambda表达式

lambda表达式是java8中最重要的特性之一,它让代码变得简洁并且允许你传递行为。曾几何时,Java总是因为代码冗长和缺少函数式编程的能力而饱受批评。随着函数式编程变得越来越受欢迎,Java也被迫开始拥抱函数式编程。否则,Java会被大家逐步抛弃。

Java8是使得这个世界上最流行的编程语言采用函数式编程的一次大的跨越。一门编程语言要支持函数式编程,就必须把函数作为其一等公民。在Java8之前,只能通过匿名内部类来写出函数式编程的代码。而随着lambda表达式的引入,函数变成了一等公民,并且可以像其他变量一样传递。

lambda表达式允许开发者定义一个不局限于定界符的匿名函数,你可以像使用编程语言的其他程序结构一样来使用它,比如变量申明。如果一门编程语言需要支持高阶函数,lambda表达式就派上用场了。高阶函数是指把函数作为参数或者返回结果是一个函数那些函数。

这个章节的代码如下ch02 package.

随着Java8中lambda表达式的引入,Java也支持高阶函数。接下来让我们来分析这个经典的lambda表达式示例--Java中Collections类的一个sort函数。sort函数有两种调用方式,一种需要一个List作为参数,另一种需要一个List参数和一个Comparator。第二种sort函数是一个接收lambda表达式的高阶函数的实例,如下:

List<String> names = Arrays.asList("shekhar", "rahul", "sameer");
Collections.sort(names, (first, second) -> first.length() - second.length());

上面的代码是根据names的长度来进行排序,运行的结果如下:

[rahul, sameer, shekhar]

上面代码片段中的(first,second) -> first.length() - second.length()表达式是一个Comparator<String>的lambda表达式。

  • (first,second)Comparatorcompare方法的参数。

  • first.length() - second.length()比较name字符串长度的函数体。

  • -> 是用来把参数从函数体中分离出来的操作符。

在我们深入研究Java8中的lambda表达式之前,我们先来追溯一下他们的历史,了解它们为什么会存在。

lambda表达式的历史

lambda表达式源自于λ演算.λ演算起源于用函数式来制定表达式计算概念的研究Alonzo Churchλ演算是图灵完整的。图灵完整意味着你可以用lambda表达式来表达任何数学算式。

λ演算后来成为了函数式编程语言强有力的理论基础。诸如 Hashkell、Lisp等著名的函数式编程语言都是基于λ演算.高阶函数的概念就来自于λ演算

λ演算中最主要的概念就是表达式,一个表达式可以用如下形式来表示:

<expression> := <variable> | <function>| <application>
  • variable -- 一个variable就是一个类似用x、y、z来代表1、2、n等数值或者lambda函数式的占位符。

  • function -- 它是一个匿名函数定义,需要一个变量,并且生成另一个lambda表达式。例如,λx.x*x是一个求平方的函数。

  • application -- 把一个函数当成一个参数的行为。假设你想求10的平方,那么用λ演算的方式的话你需要写一个求平方的函数λx.x*x并把10应用到这个函数中去,这个函数程序就会返回(λx.x*x) 10 = 10*10 = 100。但是你不仅可以求10的平方,你可以把一个函数传给另一个函数然后生成另一个函数。比如,(λx.x*x) (λz.z+10) 会生成这样一个新的函数 λz.(z+10)*(z+10)。现在,你可以用这个函数来生成一个数加上10的平方。这就是一个高阶函数的实例。

现在,你已经理解了λ演算和它对函数式编程语言的影响。下面我们继续学习它们在java8中的实现。

在java8之前传递行为

Java8之前,传递行为的唯一方法就是通过匿名内部类。假设你在用户完成注册后,需要在另外一个线程中发送一封邮件。在Java8之前,可以通过如下方式:

sendEmail(new Runnable() {
            @Override
            public void run() {
                System.out.println("Sending email...");
            }
        });

sendEmail方法定义如下:

public static void sendEmail(Runnable runnable)

上面的代码的问题不仅仅在于我们需要把行为封装进去,比如run方法在一个对象里面;更糟糕的是,它容易混淆开发者真正的意图,比如把行为传递给sendEmail函数。如果你用过一些类似Guava的库,那么你就会切身感受到写匿名内部类的痛苦。下面是一个简单的例子,过滤所有标题中包含lambda字符串的task。

Iterable<Task> lambdaTasks = Iterables.filter(tasks, new Predicate<Task>() {
            @Override
            public boolean apply(Task task) {
                return input.getTitle().contains("lambda");
            }
});

使用Java8的Stream API,开发者不用太第三方库就可以写出上面的代码,我们将在下一章chapter 3讲述streams相关的知识。所以,继续往下阅读!

Java 8 Lambda表达式

在Java8中,我们可以用lambda表达式写出如下代码,这段代码和上面提到的是同一个例子。

sendEmail(() -> System.out.println("Sending email..."));

上面的代码非常简洁,并且能够清晰的传递编码者的意图。()用来表示无参函数,比如Runnable接口的中run方法不含任何参数,直接就可以用()来代替。->是将参数和函数体分开的lambda操作符,上例中,->后面是打印Sending email的相关代码。

下面再次通过Collections.sort这个例子来了解带参数的lambda表达式如何使用。要将names列表中的name按照字符串的长度排序,需要传递一个Comparator给sort函数。Comparator的定义如下

Comparator<String> comparator = (first, second) -> first.length() - second.length();

上面写的lambda表达式相当于Comparator接口中的compare方法。compare方法的定义如下:

int compare(T o1, T o2);

T是传递给Comparator接口的参数类型,在本例中names列表是由String组成,所以T代表的是String

在lambda表达式中,我们不需要明确指出参数类型,javac编译器会通过上下文自动推断参数的类型信息。由于我们是在对一个由String类型组成的List进行排序并且compare方法仅仅用一个T类型,所以Java编译器自动推断出两个参数都是String类型。根据上下文推断类型的行为称为类型推断。Java8提升了Java中已经存在的类型推断系统,使得对lambda表达式的支持变得更加强大。javac会寻找紧邻lambda表达式的一些信息通过这些信息来推断出参数的正确类型。

在大多数情况下,javac会根据上下文自动推断类型。假设因为丢失了上下文信息或者上下文信息不完整而导致无法推断出类型,代码就不会编译通过。例如,下面的代码中我们将String类型从Comparator中移除,代码就会编译失败。

Comparator comparator = (first, second) -> first.length() - second.length(); // compilation error - Cannot resolve method 'length()'

Lambda表达式在Java8中的运行机制

你可能已经发现lambda表达式的类型是一些类似上例中Comparator的接口。但并不是每个接口都可以使用lambda表达式,只有那些仅仅包含一个非实例化抽象方法的接口才能使用lambda表达式。这样的接口被称着函数式接口并且它们能够被@FunctionalInterface注解注释。Runnable接口就是函数式接口的一个例子。

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

@FunctionalInterface注解不是必须的,但是它能够让工具知道这一个接口是一个函数式接口并表现有意义的行为。例如,如果你试着这编译一个用@FunctionalInterface注释自己并且含有多个抽象方法的接口,编译就会报出这样一个错Multiple non-overriding abstract methods found。同样的,如果你给一个不含有任何方法的接口添加@FunctionalInterface注解,会得到如下错误信息,No target method found.

下面来回答一个你大脑里一个非常重大的疑问,Java8的lambda表达式是否只是一个匿名内部类的语法糖或者函数式接口是如何被转换成字节码的?

答案是NO,Java8不采用匿名内部类的原因主要有两点:

  1. 性能影响: 如果lambda表达式是采用匿名内部类实现的,那么每一个lambda表达式都会在磁盘上生成一个class文件。当JVM启动时,这些class文件会被加载进来,因为所有的class文件都需要在启动时加载并且在使用前确认,从而会导致JVM的启动变慢。

  2. 向后的扩展性: 如果Java8的设计者从一开始就采用匿名内部类的方式,那么这将限制lambda表达式未来的使发展范围。

使用动态启用

Java8的设计者决定采用在Java7中新增的动态启用来延迟在运行时的加载策略。当javac编译代码时,它会捕获代码中的lambda表达式并且生成一个动态启用的调用地址(称为lambda工厂)。当动态启用被调用时,就会向lambda表达式发生转换的地方返回一个函数式接口的实例。比如,在Collections.sort这个例子中,它的字节码如下:

public static void main(java.lang.String[]);
    Code:
       0: iconst_3
       1: anewarray     #2                  // class java/lang/String
       4: dup
       5: iconst_0
       6: ldc           #3                  // String shekhar
       8: aastore
       9: dup
      10: iconst_1
      11: ldc           #4                  // String rahul
      13: aastore
      14: dup
      15: iconst_2
      16: ldc           #5                  // String sameer
      18: aastore
      19: invokestatic  #6                  // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
      22: astore_1
      23: invokedynamic #7,  0              // InvokeDynamic #0:compare:()Ljava/util/Comparator;
      28: astore_2
      29: aload_1
      30: aload_2
      31: invokestatic  #8                  // Method java/util/Collections.sort:(Ljava/util/List;Ljava/util/Comparator;)V
      34: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      37: aload_1
      38: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      41: return
}

上面代码的关键部分位于第23行23: invokedynamic #7, 0 // InvokeDynamic #0:compare:()Ljava/util/Comparator;这里创建了一个动态启用的调用。

接下来是将lambda表达式的内容转换到一个将会通过动态启用来调用的方法中。在这一步中,JVM实现者有自由选择策略的权利。

这里我仅粗略的概括一下,具体的内部标准见这里 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html.

匿名类 vs lambda表达式

下面我们对匿名类和lambda表达式做一个对比,以此来区分它们的不同。

  1. 在匿名类中,this 指代的是匿名类本身;而在lambda表达式中,this指代的是lambda表达式所在的这个类。

  2. You can shadow variables in the enclosing class inside the anonymous class, 而在lambda表达式中就会报编译错误。(英文部分不会翻译,希望大家一起探讨下,谢谢)

  3. lambda表达式的类型是由上下文决定的,而匿名类中必须在创建实例的时候明确指定。

我需要自己去写函数式接口吗?

Java8默认带有许多可以直接在代码中使用的函数式接口。它们位于java.util.function包中,下面简单介绍几个:

java.util.function.Predicate<T>

此函数式接口是用来定义对一些条件的检查,比如一个predicate。Predicate接口有一个叫test的方法,它需要一个T类型的值,返回值为布尔类型。例如,在一个names列表中找出所有以s开头的name就可以像如下代码这样使用predicate。

Predicate<String> namesStartingWithS = name -> name.startsWith("s");

java.util.function.Consumer<T>

这个函数式接口用于表现那些不需要产生任何输出的行为。Consumer接口中有一个叫做accept的方法,它需要一个T类型的参数并且没有返回值。例如,用指定信息发送一封邮件:

Consumer<String> messageConsumer = message -> System.out.println(message);

java.util.function.Function<T,R>

这个函数式接口需要一个值并返回一个结果。例如,如果需要将所有names列表中的name转换为大写,可以像下面这样写一个Function:

Function<String, String> toUpperCase = name -> name.toUpperCase();

java.util.function.Supplier<T>

这个函数式接口不需要传值,但是会返回一个值。它可以像下面这样,用来生成唯一的标识符

Supplier<String> uuidGenerator= () -> UUID.randomUUID().toString();

在接下来的章节中,我们会学习更多的函数式接口。

Method references

有时候,你需要为一个特定方法创建lambda表达式,比如Function<String, Integer> strToLength = str -> str.length();,这个表达式仅仅在String对象上调用length()方法。可以这样来简化它,Function<String, Integer> strToLength = String::length;。仅调用一个方法的lambda表达式,可以用缩写符号来表示。在String::length中,String是目标引用,::是定界符,length是目标引用要调用的方法。静态方法和实例方法都可以使用方法引用。

Static method references

假设我们需要从一个数字列表中找出最大的一个数字,那我们可以像这样写一个方法引用Function<List<Integer>, Integer> maxFn = Collections::maxmax是一Collections里的一个静态方法,它需要传入一个List类型的参数。接下来你就可以这样调用它,maxFn.apply(Arrays.asList(1, 10, 3, 5))。上面的lambda表达式等价于Function<List<Integer>, Integer> maxFn = (numbers) -> Collections.max(numbers);

Instance method references

在这样的情况下,方法引用用于一个实例方法,比如String::toUpperCase是在一个String引用上调用 toUpperCase方法。还可以使用带参数的方法引用,比如:BiFunction<String, String, String> concatFn = String::concatconcatFn可以这样调用:concatFn.apply("shekhar", "gulati")String``concat方法在一个String对象上调用并且传递一个类似"shekhar".concat("gulati")的参数。

Exercise >> Lambdify me

下面通过一段代码,来应用所学到的。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks);
        for (String title : titles) {
            System.out.println(title);
        }
    }

    public static List<String> taskTitles(List<Task> tasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (task.getType() == TaskType.READING) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

上面这段代码首先通过工具方法getTasks取得所有的Task,这里我们不去关心getTasks方法的具体实现,getTasks能够通过webservice或者数据库或者内存获取task。一旦得到了tasks,我们就过滤所有处于reading状态的task,并且从task中提取他们的标题,最后返回所有处于reading状态task的标题。

下面我们简单的重构下--在一个list上使用foreach和方法引用。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks);
        titles.forEach(System.out::println);
    }

    public static List<String> taskTitles(List<Task> tasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (task.getType() == TaskType.READING) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

使用Predicate<T>来过滤tasks

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks, task -> task.getType() == TaskType.READING);
        titles.forEach(System.out::println);
    }

    public static List<String> taskTitles(List<Task> tasks, Predicate<Task> filterTasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

使用Function<T,R>来将task中的title提取出来。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks, task -> task.getType() == TaskType.READING, task -> task.getTitle());
        titles.forEach(System.out::println);
    }

    public static <R> List<R> taskTitles(List<Task> tasks, Predicate<Task> filterTasks, Function<Task, R> extractor) {
        List<R> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(extractor.apply(task));
            }
        }
        return readingTitles;
    }
}

把方法引用当着提取器来使用。

public static void main(String[] args) {
    List<Task> tasks = getTasks();
    List<String> titles = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Task::getTitle);
    titles.forEach(System.out::println);
    List<LocalDate> createdOnDates = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Task::getCreatedOn);
    createdOnDates.forEach(System.out::println);
    List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Function.identity());
    filteredTasks.forEach(System.out::println);
}

我们也可以自己编写函数式接口,这样可以清晰的把开发者的意图传递给读者。我们可以写一个继承自Function接口的TaskExtractor接口。这个接口的输入类型是固定的Task类型,输出类型由实现的lambda表达式来决定。这样开发者就只需要关注输出结果的类型,因为输入的类型永远都是Task。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, TaskExtractor.identityOp());
        filteredTasks.forEach(System.out::println);
    }

    public static <R> List<R> filterAndExtract(List<Task> tasks, Predicate<Task> filterTasks, TaskExtractor<R> extractor) {
        List<R> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(extractor.apply(task));
            }
        }
        return readingTitles;
    }

}


interface TaskExtractor<R> extends Function<Task, R> {

    static TaskExtractor<Task> identityOp() {
        return t -> t;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容