简介

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

行为参数化

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

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

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;
}

现在客户需要变更需求,添加过滤条件,按照出版社过滤,于是我们不得不再次编写一个方法:

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()方法都需要修改,工作量太大。

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

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的实现类,这里采用了内部类:

// 根据作者过滤
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);

更多关于 :: 的详解,请看你竟然不知道Java中可以用 :: 吗?

更多关于Optional的详解,请看使用Java8中的Optional类来消除代码中的null检查

函数式接口

如果你的好奇心使你翻看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中可以用 :: 吗?

推荐阅读:使用Java8中的Optional类来消除代码中的null检查

本文参考链接:博客【一书生VOID】https://lw900925.github.io/java/java8-lambda-expression.html

一篇文章教会你使用Java8中的Lambda表达式的更多相关文章

  1. Java8中的Lambda表达式

    Lambda是什么 Lambda表达式,也可称为闭包,是java8的新特性,作用是取代大部分内部类,优化java代码结构,让代码变得更加简洁紧凑. Lambda的基本语法 (expression)-& ...

  2. Java8中的 lambda 和Stream API

    前言 ​ 由于项目中用到了比较多有关于 Java8 中新的东西,一开始自己只是会写,但是写起来不太顺,然后就在网上找到了一个很好的关于Java8新特性的视频,所以就进行了学习了一下,以下是自己对 la ...

  3. Java8新特性 - Lambda表达式 - 基本知识

    A lambda expression is an unnamed block of code (or an unnamed function) with a list of formal param ...

  4. 一篇文章助你理解Python3中字符串编码问题

    前几天给大家介绍了unicode编码和utf-8编码的理论知识,以及Python2中字符串编码问题,没来得及上车的小伙伴们可以戳这篇文章:浅谈unicode编码和utf-8编码的关系和一篇文章助你理解 ...

  5. 一篇文章教会你利用Python网络爬虫获取电影天堂视频下载链接

    [一.项目背景] 相信大家都有一种头疼的体验,要下载电影特别费劲,对吧?要一部一部的下载,而且不能直观的知道最近电影更新的状态. 今天小编以电影天堂为例,带大家更直观的去看自己喜欢的电影,并且下载下来 ...

  6. 乐字节-Java8新特性-Lambda表达式

    上一篇文章我们了解了Java8新特性-接口默认方法,接下来我们聊一聊Java8新特性之Lambda表达式. Lambda表达式(也称为闭包),它允许我们将函数当成参数传递给某个方法,或者把代码本身当作 ...

  7. java8 快速入门 lambda表达式 Java8 lambda表达式10个示例

    本文由 ImportNew - lemeilleur 翻译自 javarevisited.欢迎加入翻译小组.转载请见文末要求. Java 8 刚于几周前发布,日期是2014年3月18日,这次开创性的发 ...

  8. 将Lambda表达式作为参数传递并解析-在构造函数参数列表中使用Lambda表达式

    public class DemoClass { /// <summary> /// 通过Lambda表达式,在构造函数中赋初始值 /// </summary> /// < ...

  9. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

随机推荐

  1. PHP defined() 函数

    实例 检查某常量是否存在: <?phpdefine("GREETING","Hello you! How are you today?");echo de ...

  2. luogu P6097 子集卷积 FST FWT

    LINK:子集卷积 学了1h多 终于看懂是怎么回事了(题解写的不太清楚 翻了好几篇博客才懂 一个需要用到的性质 二进制位为1个数是i的二进制数s 任意两个没有子集关系.挺显然. 而FST就是利用这个性 ...

  3. python之路第一节-pip的使用

    第一次写博客,一边吃着旺仔冻痴一边学着python,爽~ 我之理解pip 首先,python封装好了大量的函数,这些函数存在各种各样的库中. 那么怎么去向我们可爱的pycharm等软件导入这些库呢,两 ...

  4. setOff与scrollTop区别

    1.offsetTop     : 当前对象到其上级层顶部的距离. 不能对其进行赋值.设置对象到页面顶部的距离请用style.top属性. 2.offsetLeft    : 当前对象到其上级层左边的 ...

  5. django--各个文件的含义

    当你创建项目或者应用后你是不是发现多了很多个文件,现在我们来看看各代表什么意思 与你项目名相同的文件夹:是项目的管理功能目录,这个目录的名称因用户所创建的项目名称的不同而不同 在该目录下还有四个文件: ...

  6. Docker入坑指南之EXEC

    容器启动之后,如果我们需要进入容器内修改配置,比如mysql修改启动配置 我们启动的附加参数是不是shell,这个时候就可以用docker exec了,docker除了对image参数以外,大部分命令 ...

  7. 04-Thread的生命周期

    图示: 说明: 1.生命周期关注两个概念:状态.相应的方法 2.关注:状态a-->状态b:哪些方法执行了(回调方法) 某个方法主动调用:状态a-->状态b 3.阻塞:临时状态,不可以作为最 ...

  8. VMware虚拟机磁盘收缩的几种方法

    原文地址:http://www.cnblogs.com/5201351/p/4290401.html 根据下面转载的内容,我在VMware 12.0.0 build-2985596的ubuntu上做试 ...

  9. JS 图片跟随鼠标移动案例

    css代码 img { position: absolute; /* top: 2px; */ width: 50px; height: 50px; } HTML代码 <img src=&quo ...

  10. 【模式识别与机器学习】——PCA与Kernel PCA介绍与对比

    PCA与Kernel PCA介绍与对比 1. 理论介绍 PCA:是常用的提取数据的手段,其功能为提取主成分(主要信息),摒弃冗余信息(次要信息),从而得到压缩后的数据,实现维度的下降.其设想通过投影矩 ...