Java 8新特性(一):Lambda表达式

本文首发于一书生VOID的博客。 原文连接:Java 8新特性(一):Lambda表达式html


2014年3月发布的Java 8,有多是Java版本更新中变化最大的一次。新的Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时间API等。这些新特性给Java开发者带来了福音,特别是Lambda表达式的支持,使程序设计更加简化。本篇文章将讨论行为参数化,Lambda表达式,函数式接口等特性。java

行为参数化

在软件开发的过程当中,开发人员可能会遇到频繁的需求变动,使他们不断地修改程序以应对这些变化的需求,致使项目进度缓慢甚至项目延期。行为参数化就是一种能够帮助你应对频繁需求变动的开发模式,简单的说,就是预先定义一个代码块而不去执行它,把它当作参数传递给另外一个方法,这样,这个方法的行为就被这段代码块参数化了。git

为了方便理解,咱们经过一个例子来说解行为参数化的使用。假设咱们正在开发一个图书管理系统,需求是要对图书的做者进行过滤,筛选出指定做者的书籍。比较常见的作法就是编写一个方法,把做者当成方法的参数:github

public List<Book> filterByAuthor(List<Book> books, String author) {
	List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (author.equals(book.getAuthor())) {
			result.add(book);
		}
	}
	return result;
}
复制代码

如今客户须要变动需求,添加过滤条件,按照出版社过滤,因而咱们不得再也不次编写一个方法:express

public List<Book> filterByPublisher(List<Book> books, String publisher) {
	List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (publisher.equals(book.getPublisher())) {
			result.add(book);
		}
	}
	return result;
}
复制代码

两个方法除了名称以外,内部的实现逻辑几乎如出一辙,惟一的区别就是if判断条件,前者判断的是做者,后者判断的是出版社。若是如今客户又要增长需求,须要按照图书的售价过滤,是否是须要再次将上面的方法复制一遍,将if判断条件改成售价? No! 这种作法违背了DRY(Don’t Repeat Yourself,不要重复本身)原则,并且不利于后期维护,若是须要改变方法内部遍历方式来提升性能,意味着每一个filterByXxx()方法都须要修改,工做量太大。app

一种可行的办法是对过滤的条件作更高层的抽象,过滤的条件无非就是图书的某些属性(好比价格、出版社、出版日期、做者等),能够声明一个接口用于对过滤条件建模:ide

public interface BookPredicate {
    public boolean test(Book book);
}
复制代码

BookPredicate接口只有一个抽象方法test(),该方法接受一个Book类型参数,返回一个boolean值,能够用它来表示图书的不一样过滤条件。函数

接下来咱们对以前的过滤方法进行重构,将filterByXxx()方法的第二个参数换成上面定义的接口:性能

public List<Book> filter(List<Book> books, BookPredicate bookPredicate) {
    List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (bookPredicate.test(book)) {
			result.add(book);
		}
	}
	return result;
}
复制代码

将过滤的条件换成BookPredicate的实现类,这里采用了内部类:spa

// 根据做者过滤
final String author = "张三";
List<Book> result = filter(books, new BookPredicate() {
    @Override
    public boolean test(Book book) {
        return author.equals(book.getAuthor());
    }
});

// 根据图书价格过滤
final double price = 100.00D;
List<Book> result = filter(books, new BookPredicate() {
    @Override
    public boolean test(Book book) {
        return price > book.getPrice();
    }
});
复制代码

重构先后有什么区别?咱们将方法中的if判断条件换成了BookPredicate接口定义的test()方法,用于判断是否知足过滤条件,将图书过滤的逻辑交给了BookPredicate接口的实现类,而不是在filter()方法内部实现过滤,而BookPredicate接口又是filter()方法的参数。以上的步骤,就是将行为参数化,也就是将图书过滤的行为(BookPredicate接口的实现类)当作filter()方法的参数。如今,能够删掉全部filterByXxx()的方法,只保留filter()方法,就算后期数据规模很庞大,须要改变集合的遍历方式来提升性能,只须要在filter()方法内部作出相应的修改,而不用去修改其余业务代码。

不过,BookPredicate接口只是针对图书的过滤,若是须要对其余对象集合排序(如:用户),又得从新申明一个接口。有一个办法就是能够用Java的泛型对它作进一步的抽象:

public interface Predicate<T> {
    public boolean test(T t);
}
复制代码

如今你能够把filter()方法用在任何对象的过滤中。

Lambda表达式

虽然咱们对filter()方法进行重构,并抽象了Predicate接口做为过滤的条件,但实际上还须要编写不少内部类来实现Predicate接口。使用内部类的方式实现Predicate接口有不少缺点:首先是代码显得臃肿不堪,可读性差;其次,若是某个局部变量被内部类使用,这个变量必须使用final关键字修饰。在Java 8中,使用Lambda表达式能够对内部类进一步简化:

// 根据做者过滤
List<Book> result = filter(books, book -> "张三".equals(book.getAuthor()));

// 根据图书价格过滤
List<Book> result = filter(books, book -> 100 > book.getPrice());
复制代码

使用Lambda仅仅用一行代码就对内部类进行了转化,并且代码变得更加清晰可读。其中book -> "张三".equals(book.getAuthor())book -> 100 > book.getPrice()就是咱们接下来要研究的Lambda表达式。

Lambda表达式是什么

Lambda表达式(lambda expression)是一个匿名函数,由数学中的λ演算而得名。在Java 8中能够把Lambda表达式理解为匿名函数,它没有名称,可是有参数列表、函数主体、返回类型等。

Lambda表达式的语法以下:

(parameters) -> { statements; }
复制代码

为何要使用Lambda表达式?前面你也看到了,在Java中使用内部类显得十分冗长,要编写不少样板代码,Lambda表达式正是为了简化这些步骤出现的,它使代码变得清晰易懂。

如何使用Lambda表达式

Lambda表达式是为了简化内部类的,你能够把它当成是内部类的一种简写方式,只要是有内部类的代码块,均可以转化成Lambda表达式:

// Comparator排序
List<Integer> list = Arrays.asList(3, 1, 4, 5, 2);
list.sort(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

// 使用Lambda表达式简化
list.sort((o1, o2) -> o1.compareTo(o2));
复制代码
// Runnable代码块
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello Man!");
    }
});

// 使用Lambda表达式简化
Thread thread = new Thread(() -> System.out.println("Hello Man!"));
复制代码

能够看出,只要是内部类的代码块,就可使用Lambda表达式简化,而且简化后的代码清晰易懂。甚至,Comparator排序的Lambda表达式还能够进一步简化:

list.sort(Integer::compareTo);
复制代码

这种写法被称为 方法引用,方法引用是Lambda表达式的简便写法。若是你的Lambda表达式只是调用这个方法,最好使用名称调用,而不是描述如何调用,这样能够提升代码的可读性。

方法引用使用::分隔符,分隔符的前半部分表示引用类型,后面半部分表示引用的方法名称。例如:Integer::compareTo表示引用类型为Integer,引用名称为compareTo的方法。

相似使用方法引用的例子还有打印集合中的元素到控制台中:

list.forEach(System.out::println);
复制代码

函数式接口

若是你的好奇心使你翻看Runnable接口源代码,你会发现该接口被一个@FunctionalInterface的注解修饰,这是Java 8中添加的新注解,用于表示 函数式接口

函数式接口又是什么鬼?在Java 8中,把那些仅有一个抽象方法的接口称为函数式接口。若是一个接口被@FunctionalInterface注解标注,表示这个接口被设计成函数式接口,只能有一个抽象方法,若是你添加多个抽象方法,编译时会提示“Multiple non-overriding abstract methods found in interface XXX”之类的错误。

函数式方法又能作什么?Java8容许你以Lambda表达式的方式为函数式接口提供实现,通俗的说,你能够将整个Lambda表达式做为接口的实现类。

除了Runnable以外,Java 8中内置了许多函数式接口供开发者使用,这些接口位于java.util.function包中,咱们以前使用的Predicate接口,已经被包含在这个包内,他们分别为PredicateConsumerFunction,因为咱们已经在以前的图书过滤的例子中介绍了Predicate的用法,因此接下来主要介绍ConsumerFunction的用法。

Consumer

java.util.function.Consumer<T>定义了一个名叫accept()的抽象方法,它接受泛型T的对象,没有返回(void)。若是你须要访问类型T的对象,并对其执行某些操做,就可使用这个接口。好比,你能够用它来建立一个forEach()方法,接受一个集合,并对集合中每一个元素执行操做:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> consumer) {
    for(T t: list){
        consumer.accept(t);
    }
}

public static void main(String[] args) {
    List<String> list = Arrays.asList("A", "B", "C", "D");
    forEach(list, str -> System.out.println(str));
    // 也能够写成
    forEach(list, System.out::println);
}
复制代码

Function

java.util.function.Function<T, R>接口定义了一个叫做apply()的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。若是你须要定义一个Lambda,将输入对象的信息映射到输出,就可使用这个接口。好比,咱们须要计算一个图书集合中每本书的做者名称有几个汉字(假设这些书的做者都是中国人):

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T s: list){
        result.add(f.apply(s));
    }
    return result;
}

public static void main(String[] args) {
    List<Book> books = Arrays.asList(
        new Book("张三", 99.00D),
        new Book("李四", 59.00D),
        new Book("王老五", 59.00D)
    );
    List<Integer> results = map(books, book -> book.getAuthor().length());
}
复制代码

如今,你应该对Lambda表达式有一个初步的了解了,而且,你可使用Lambda表达式来重构你的代码,提升代码可读性;使用行为参数化来设计你的程序,让程序更灵活。在下一篇文章将会介绍Java 8的另外一个特性——流式数据处理。