计算机程序的思惟逻辑 (91) - Lambda表达式

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

在以前的章节中,咱们的讨论基本都是基于Java 7的,从本节开始,咱们探讨Java 8的一些特性,主要内容包括:java

  • 传递行为代码 - Lambda表达式
  • 函数式数据处理 - 流
  • 组合式异步编程 - CompletableFuture
  • 新的日期和时间API

本节,咱们先讨论Lambda表达式,它是什么?有什么用呢?git

Lambda表达式是Java 8新引入的一种语法, 是一种紧凑的传递代码的方式,它的名字来源于学术界的λ演算,具体咱们就不探讨了。程序员

理解Lambda表达式,咱们先回顾一下接口、匿名内部类和代码传递。github

经过接口传递代码

咱们在19节介绍过接口以及面向接口的编程,针对接口而非具体类型进行编程,能够下降程序的耦合性、提升灵活性、提升复用性。接口常被用于传递代码,好比,在59节,咱们介绍过File的以下方法:算法

public String[] list(FilenameFilter filter)
public File[] listFiles(FilenameFilter filter)
复制代码

list和listFiles须要的其实不是FilenameFilter对象,而是它包含的以下方法:编程

boolean accept(File dir, String name);
复制代码

或者说,list和listFiles但愿接受一段方法代码做为参数,但没有办法直接传递这个方法代码自己,只能传递一个接口。swift

再好比,咱们在53节介绍过Collections的一些算法,不少方法都接受一个参数Comparator,好比:数组

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) 复制代码

它们须要的也不是Comparator对象,而是它包含的以下方法:微信

int compare(T o1, T o2);
复制代码

可是,没有办法直接传递方法,只能传递一个接口。

咱们在77节介绍过异步任务执行服务ExecutorService,提交任务的方法有:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
复制代码

Callable和Runnable接口也用于传递任务代码。

经过接口传递行为代码,就要传递一个实现了该接口的实例对象,在以前的章节中,最简洁的方式是使用匿名内部类,好比:

//列出当前目录下的全部后缀为.txt的文件
File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Override
    public boolean accept(File dir, String name) {
        if(name.endsWith(".txt")){
            return true;
        }
        return false;
    }
});
复制代码

将files按照文件名排序,代码为:

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

    @Override
    public int compare(File f1, File f2) {
        return f1.getName().compareTo(f2.getName());
    }
});
复制代码

提交一个最简单的任务,代码为:

ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello world");
    }
});
复制代码

Lambda表达式

语法

Java 8提供了一种新的紧凑的传递代码的语法 - Lambda表达式。对于前面列出文件的例子,代码能够改成:

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

能够看出,相比匿名内部类,传递代码变得更为直观,再也不有实现接口的模板代码,再也不声明方法,也名字也没有,而是直接给出了方法的实现代码。Lambda表达式由->分隔为两部分,前面是方法的参数,后面{}内是方法的代码。

上面代码能够简化为:

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

当主体代码只有一条语句的时候,括号和return语句也能够省略,上面代码能够变为:

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

注意,没有括号的时候,主体代码是一个表达式,这个表达式的值就是函数的返回值,结尾不能加分号;,也不能加return语句。

方法的参数类型声明也能够省略,上面代码还能够继续简化为:

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

之因此能够省略方法的参数类型,是由于Java能够自动推断出来,它知道listFiles接受的参数类型是FilenameFilter,这个接口只有一个方法accept,这个方法的两个参数类型分别是File和String。

这样简化下来,代码是否是简洁清楚多了?

排序的代码用Lambda表达式能够写为:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName())); 
复制代码

提交任务的代码用Lambda表达式能够写为:

executor.submit(()->System.out.println("hello"));
复制代码

参数部分为空,写为()。

当参数只有一个的时候,参数部分的括号能够省略,好比,File还有以下方法:

public File[] listFiles(FileFilter filter)
复制代码

FileFilter的定义为:

public interface FileFilter {
    boolean accept(File pathname);
}
复制代码

使用FileFilter重写上面的列举文件的例子,代码能够为:

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

变量引用

与匿名内部类相似,Lambda表达式也能够访问定义在主体代码外部的变量,但对于局部变量,它也只能访问final类型的变量,与匿名内部类的区别是,它不要求变量声明为final,但变量事实上不能被从新赋值。好比:

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

能够访问局部变量msg,但msg不能被从新赋值,若是这样写:

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)等,具体实现比较复杂,咱们就不探讨了,感兴趣能够参看cr.openjdk.java.net/~briangoetz…,咱们须要知道的是,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,好比:

@FunctionalInterface
public 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中,具体咱们就不探讨了。

小结

本节介绍了Java 8中的一些新概念,包括Lambda表达式、函数式接口、方法引用、接口的静态方法和默认方法等。

最重要的变化是,传递代码变的简单了,函数变为了代码世界的一等公民,能够方便的被做为参数传递,被做为返回值,被复合利用以构建新的函数,看上去,这些只是语法上的一些小变化,但利用这些小变化,却能使得代码更为通用、更为灵活、更为简洁易读,这,大概就是函数式编程的奇妙之处吧。

下一节,咱们来探讨Java 8引入的函数式数据处理API,它们大大简化了常见的集合数据操做。

(与其余章节同样,本节全部代码位于 github.com/swiftma/pro…,位于包shuo.laoma.java8.c91下)


未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索