计算机程序的思维逻辑 (91),思维91

澳门新浦京8455com 7

Lambda表达式是自Java SE
5引入泛型以来最重大的Java语言新特性,本文是2012年度最后一期Java
Magazine中的一篇文章,它介绍了Lamdba的设计初衷,应用场景与基本语法。

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

计算机程序的思维逻辑 (91),思维91

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

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

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

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

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

通过接口传递代码

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

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

list和listFiles需要的其实不是FilenameFilter对象,而是它包含的如下方法:

boolean accept(File dir, String name);

或者说,list和listFiles希望接受一段方法代码作为参数,但没有办法直接传递这个方法代码本身,只能传递一个接口。

再比如,我们在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)等,具体实现比较复杂,我们就不探讨了,感兴趣可以参看

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下,主要的有:

澳门新浦京8455com 1

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

澳门新浦京8455com 2

这些函数有什么用呢?它们被大量使用于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标识,默认方法与抽象方法都是接口的方法,不同在于,它有默认的实现,实现类可以改变它的实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。

澳门新浦京8455com ,在没有默认方法之前,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,它们大大简化了常见的集合数据操作。

(与其他章节一样,本节所有代码位于
)


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

澳门新浦京8455com 3

(91),思维91
​在之前的章节中,我们的讨论基本都是基于Java
7的,从本节开始,我们探讨Java 8的一些特性,主要内…

Lambda表达式,这个名字由该项目的专家组选定,描述了一种新的函数式编程结构,这个即将出现在Java
SE 8中的新特性正被大家急切地等待着。有时你也会听到人们使用诸如闭包,函数直接量,匿名函数,及SAM(Single
Abstract
Method)这样的术语。其中一些术语彼此之间会有一些细微的不同,但基本上它们都指代相同的功能。

澳门新浦京8455com 4

虽然一开始会觉得Lambda表达式看起来很陌生,但很容易就能掌握它。而且为了编写可完全利用现代多核CPU的应用程序,掌握Lambda表达式是至关重要的。


需要牢记的一个关键概念就是,Lambda表达式是一个很小且能被当作数据进行传递的函数。需要掌握的第二个概念就是,理解集合对象是如何在内部进行遍历的,这种遍历不同于当前已有的外部顺序化遍历。

 

在本文中,我们将向你展示Lambda表达式背后的动因,应用示例,当然,还有它的语法。

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

为什么你需要Lambda表达式

程序员需要Lambda表达式的原因主要有三个:

  1. 更紧凑的代码

  2. 通过提供额外的功能对方法的功能进行修改的能力

  3. 更好地支持多核处理

更紧凑的代码

Lambda表达式以一种简洁的方式去实现仅有一个方法的Java类。

例如,如果代码中有大量的匿名内部类–诸如用于UI应用中的监听器与处理器实现,以及用于并发应用中的Callable与Runnable实现–在使用了Lambda表达式之后,将使代码变得非常短,且更易于理解。

修改方法的能力

有时,方法不具备我们想要的一些功能。例如,Collection接口中的contains()方法只有当传入的对象确实存在于该集合对象中时才会返回true。但我们无法去干预该方法的功能,比如,若使用不同的大小写方案也可以认为正在查找的字符串存在于这个集合对象中,我们希望此时contains()方法也能返回true。

简单点儿说,我们所期望做的就是”将我们自己的新代码传入”已有的方法中,然后再调用这个传进去的代码。Lambda表达式提供了一种很好的途径来代表这种被传入已有方法且应该还会被回调的代码。

更好地支持多核处理

当今的CPU具备多个内核。这就意味着,多线程程序能够真正地被并行执行,这完全不同于在单核CPU中使用时间共享这种方式。通过在Java中支持函数式编程语法,Lambda表达式能帮助你编写简单的代码去高效地应用这些CPU内核。

例如,你能够并行地操控大集合对象,通过利用并行编程模式,如过滤、映射和化简(后面将会很快接触到这些模式),就可使用到CPU中所有可用的硬件线程。

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

Lambda表达式概览

在前面提到的使用不同大小写方案查找字符串的例子中,我们想做的就是把方法toLowerCase()的表示法作为第二个参数传入到contains()方法中,为此需要做如下的工作:

  1. 找到一种途径,可将代码片断当作一个值(某种对象)进行处理

  2. 找到一种途径,将上述代码片断传递给一个变量

换言之,我们需要将一个程序逻辑包装到某个对象中,并且该对象可以被进行传递。为了说的更具体点儿,让我们来看两个基本的Lambda表达式的例子,它们都是可以被现有的Java代码进行替换的。

过滤

你想传递的代码片断可能就是过滤器,这是一个很好的示例。例如,假设你正在使用(Java
SE
7预览版中的)java.io.FileFilter去确定目录隶属于给定的路径,如清单1所示,

清单1

File dir = new File("/an/interesting/location/");  
FileFilter directoryFilter = new FileFilter() {  
    public boolean accept(File file) {  
        return file.isDirectory();  
    }  
};  
File[] directories = dir.listFiles(directoryFilter);

在使用Lambda表达式之后,代码会得到极大的简化,如清单2所示,

清单2

File dir = new File("/an/interesting/location/");  
FileFilter directoryFilter = (File f) -> f.isDirectory();  
File[] directories = dir.listFiles(directoryFilter);

赋值表达式的左边会推导出类型(FileFilter),右边则看起来像FileFilter接口中accept()方法的一个缩小版,该方法会接受一个File对象,在判定f.isDirectory()之后返回一个布尔值。

实际上,由于Lambda表达式利用了类型推导,基于后面的工作原理,我们还可以进一步简化上述代码。编译器知道FileFilter只有唯一的方法accept(),所以它必定是该方法的实现。我们还知,accept()方法只需要一个File类型的参数。因此,f必定是File类型的。如清单3所示,

清单3

File dir = new File("/an/interesting/location/");  
File[] directories = dir.listFiles(f -> f.isDirectory());

你可以看到,使用Lambda表达式会大幅降低模板代码的数量。

一旦你习惯于使用Lambda表达式,它会使逻辑流程变得非常易于阅读。在达到这一目的的关键方法之一就是将过滤逻辑置于使用该逻辑的方法的侧边。

事件处理器

UI程序是另一个大量使用匿名内部类的领域。让我们将一个点击监听器赋给一个按钮,如清单4所示,

清单4

Button button = new Button();  
button.addActionListener(new ActionListener() {  
    public void actionPerformed(ActionEvent e) {  
        ui.showSomething();  
    }  
});

这多么代码无非是说”当点击该按钮时,调用该方法”。使用Lambda表达式就可写出如清单5所示的代码,

清单5

ActionListener listener = event -> {ui.showSomething();};  
button.addActionListener(listener);

该监听器在必要时可被复用,但如果它仅需被使用一次,清单6中的代码则考虑了一种很好的方式。

清单6

button.addActionListener(event -> {ui.showSomething();});

在这个例子中,这种使用额外花括号的语法有些古怪,但这是必须的,因为actionPerformed()方法返回的是void。后面我们会看到与此有关的更多内容。

现在让我们转而关注Lambda表达式在编写处理集合对象的新式代码中所扮演的角色,尤其是当针对两种编程风格,外部遍历与内部遍历,之间的转换的时候。

外部遍历 vs. 内部遍历

到目前为止,处理Java集合对象的标准方式是通过外部遍历。之所以称其为外部遍历,是因为要使用集合对象外部的控制流程去遍历集合所包含的元素。这种传统的处理集合的方式为多数Java程序员所熟知,尽管他们并不知道或不使用外部遍历这个术语。

如清单7所示,Java语言为增强的for循环构造了一个外部迭代器,并使用这个迭代器去遍历集合对象,

清单7

List<String> myStrings = getMyStrings();  
for (String myString : myStrings) {  
    if (myString.contains(possible))  
        System.out.println(myString + " contains " + possible);  
}

使用这种方法,集合类代表着全部元素的一个”整体”视图,并且该集合对象还能支持对任意元素的随机访问,程序员可能会有这种需求。

基于这种观点,可通过调用iterator()方法去遍历集合对象,该方法将返回集合元素类型的迭代器,该迭代器是针对同一集合对象的更具限制性的视图。它没有为随机访问暴露任何接口;相反,它纯粹是为了顺序地访问集合元素而设计的。这种顺序本性使得当你试图并发地访问集合对象时就会造成臭名昭著的ConcurrentModificationException。

另一种可选的方案就是要求集合对象要能够在内部管理迭代器(或循环),这种方案就是内部遍历,当使用Lambda表达式时会优先选择内部遍历。

除了新的Lambda表达式语法以外,Lambda项目还包括一个经过大幅升级的集合框架类库。这次升级的目的是为了能更易于编写使用内部遍历的代码,以支持一系列众所周知的函数式编程典范。

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

使用Lambda的函数式编程

曾经,大多数开发者发现他们需要集合能够执行如下一种或几种操作:

  1. 创建一个新的集合对象,但要过滤掉不符合条件的元素。

  2. 对集合中的元素逐一进行转化,并使用转化后的集合。

3.
创建集合中所有元素的某个属性的总体值,例如,合计值与平均值。这样的任务(分别称之为过滤,映射和化简)具有共通的要点:它们都需要处理集合中的每个元素。

程序无论是判定某个元素是否存在,或是判断元素是否符合某个条件(过滤),或是将元素转化成新元素并生成新集合(映射),或是计算总体值(化简),关键原理就是”程序必须处理到集合中的每个元素”。

这就暗示我们需要一种简单的途径去表示用于内部遍历的程序。幸运地是,Java
SE 8为此类表示法提供了构建语句块。

支持基本函数式编程的Java SE 8类

Java SE
8中的一些类意在被用于实现前述的函数式典范,这些类包括Predicate,Mapper和Block–当然,还有其它的一些类–它们都在一个新的java.util.functions包中。

看看Predicate类的更多细节,该类常被用于实现过滤算法;将它作用于一个集合,以返回一个包含有符合谓语条件元素的新集合。何为谓语,有很多种解释。Java
SE 8认为谓语是一个依据其变量的值来判定真或假的方法。

再考虑一下我们之前看过的一个例子。给定一个字符串的集合,我们想判定它是否包含有指定的字符串,但希望字符串的比较是大小写不敏感的。

在Java SE 7中,我们将需要使用外部遍历,其代码将如清单8所示,

清单8

public void printMatchedStrings(List<String> myStrings) {  
    List<String> out = new ArrayList<>();  
    for (String s: myStrings) {  
        if (s.equalsIgnoreCase(possible))  
            out.add(s);  
    }  
    log(out);  
}

而在即将发布的Java SE
8中,我们使用Predicate以及Collections类中一个新的助手方法(过滤器)就可写出更为紧凑的程序,如清单9所示,

清单9

public void printMatchedStrings() {  
    Predicate<String> matched = s -> s.equalsIgnoreCase(possible);  
    log(myStrings.filter(matched));  
}

事实上,如果使用更为通用的函数式编程风格,你只需要写一行代码,如清单10所示,

清单10

public void printMatchedStrings() {  
    log(myStrings.filter(s -> s.equalsIgnoreCase(possible)));  
}

如你所见,代码依然非常的易读,并且我们也体会到了使用内部遍历的好处。

最后,让我们讨论一下Lambda表达式语法的更多细节。

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

Lambda表达式的语法规则

Lambda表达式的基本格式是以一个可被接受的参数列表开头,以一些代码(称之为表达式体/body)结尾,并以箭头(->)将前两者分隔开。

注意:Lambda表达式的语法仍可能会面临改变,但在撰写本文的时候,下面示例中所展示的语法是能够正常工作的。

Lambda表达式非常倚重类型推导,与Java的其它语法相比,这显得极其不同寻常。

让我们进一步考虑之前已经看过的一个示例(请见清单11)。如果看看ActionListener的定义,可以发现它只有一个方法(请见清单12)。

清单11

ActionListener listener = event -> {ui.showSomething();};

清单12

public interface ActionListener {  
    public void actionPerformed(ActionEvent event);  
}

所以,在清单11右侧的Lambda表达式,能够很容易地理解为”这是针对仅声明单个方法的接口的方法定义”。注意,仍然必须要遵守Java静态类型的一般规则;这是使类型推导能正确工作的唯一途径。

据此可以发现,使用Lambda表达式可以将先前所写的匿名内部类代码转换更紧凑的代码。

还需要意识到有另一个怪异的语法。让我们再回顾下上述示例,如清单13所示,

清单13

FileFilter directoryFilter = (File f) -> f.isDirectory();

仅一瞥之,它看起来与ActionListener的示例相似,但让我们看看FileFilter接口的定义(请见清单14)。accept()方法会返回一个布尔值,但并没有一个显式的返回语句。相反,该返回值的类型是从Lambda表达式中推导出来的

清单14

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

这就能解释,当方法返回类型为void时,为什么要进行特别处理了。对于这种情形,Lambda表达式会使用一对额外的小括号去包住代码部分(表达式体/body)。若没有这种怪异的语法,类型推导将无法正常工作–但你要明白,这一语法可能会被改变。

Lambda表达式的表达式体可以包含多条语句,对于这种情形,表达式体需要被小括号包围住,但”被推导出的返回类型”这种语法将不启作用,那么返回类型关键字就必不可少。

最后还需要提醒你的是:当前,IDE似乎还不支持Lambda语法,所以当你第一次尝试Lambda表达式时,必须要格外注意javac编译器抛出的任何警告。

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

结论

Lambda表达式是自Java SE
5引入泛型以来最重大的Java语言新特性。应用得当,Lambda表达式可使你写出简洁的代码,为已有方法增加额外的功能,并能更好地适应多核处理器。到目前为止,我们能肯定的是,你正急切地想去尝试Lambda表达式,所以咱也别啰嗦了…

你可以从Lambda项目的主页中获得包含有Lambda表达式的Java SE
8快照版。同样地,在试用二进制包时,你也应该先阅读一下”Lambda项目状态”的相关文章,可以在此处找到它们。

通过接口传递代码

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

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

list和listFiles需要的其实不是FilenameFilter对象,而是它包含的如下方法:

boolean accept(File dir, String name);

或者说,list和listFiles希望接受一段方法代码作为参数,但没有办法直接传递这个方法代码本身,只能传递一个接口。

再比如,我们在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)等,具体实现比较复杂,我们就不探讨了,感兴趣可以参看

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下,主要的有:

澳门新浦京8455com 5

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

澳门新浦京8455com 6

这些函数有什么用呢?它们被大量使用于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,它们大大简化了常见的集合数据操作。

(与其他章节一样,本节所有代码位于
)


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

澳门新浦京8455com 7

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图