概述

为什么学

  • 能够看懂公司里的代码
  • 大数量下处理集合效率高
  • 代码可读性高
  • 消灭嵌套地狱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 查询未成年作家的评分在70以上的书籍,受网络影响可能出现重复数据,需去重
List<Book> bookList = new ArrayList<>();
Set<Book> uniqueBookValues = new HashSet<>();
Set<Author> uniqueAuthorValues = new HashSet<>();

// It drives me mad ! :(
for (Author author : authors) {
if (uniqueAuthorValues.add(author)) {
if (author.getAge() < 18) {
List<Book> books = author.getBooks();
for (Book book : books) {
if (book.getScore() > 70) {
if (uniqueBookValues.add(book)) {
bookList.add(book);
}
}
}
}
}
}

System.out.println(bookList);

可优化为

1
2
3
4
5
6
7
8
9
10
// wow it is much easier to read ! :)
List<Book> bookList = authors.stream()
.distinct()
.filter(author -> author.getAge() < 18)
.flatMap(author -> author.getBooks().stream())
.distinct()
.filter(book -> book.getScore() > 70)
.collect(Collectors.toList());

System.out.println(bookList);

函数式编程思想

概念

​ 面向对象思想关注用什么对象完成什么事情,而函数式编程思想类似于数学中的函数,主要关注对数据进行了什么操作。

优点

  • 代码简洁,开发快速
  • 接近自然语言,便于理解
  • 易于并发编程

Lambda表达式

概述

​ Lambda表达式是JDK8中的一个语法糖,可以简化某些匿名内部类。

​ 它是函数式编程思想的一个重要体现方式,让我们不关注是什么对象,而关注对对象进行了什么操作。

核心原则

可推导可省略

基本格式

1
(参数列表) -> {代码}

省略规则

  • 参数类型可省略
  • 方法体只有一行代码时大括号、return和该行代码的分号可省略
  • 方法只有一个参数时小括号可以省略

Stream流

概述

​ JDK8的Stream使用的是函数式编程模式,可以用来对集合或数组进行链状流式的操作。

案例数据准备

​ 定义Author类和Book类,此处省略

常用操作

创建流

  • 单列集合:集合对象.stream()

  • 数组:Arrays.stream(数组)Stream.of(数组)

1
2
3
4
5
Integer[] array = {1, 1, 4, 5, 1, 4};
Stream<Integer> stream1 = Arrays.stream(array);
Stream<Integer> stream2 = Stream.of(array);

Stream<Integer> stream3 = Stream.of(1, 9, 1, 9, 8, 1, 0);
  • 双列集合:转换成单列集合后创建
1
2
3
4
5
6
7
Map<String, Integer> map = new HashMap<>();
map.put("祤麒", 18);
map.put("家辰", 19);
map.put("美羊羊", 17);

Map.Entry<String, Integer> entry = map.entrySet();
Stream<Map.Entry<String, Integer>> stream = entry.stream();

中间操作

filter()

​ 可以对流中元素进行过滤,符合条件的元素才能继续留在流中。

例如:

​ 打印所有姓名长度大于2的作家姓名。

1
2
3
4
List<Author> authors = getAuthors();
authors.stream()
.filter(author -> author.getName().length() > 2)
.forEach(author -> System.out.println(author.getName()));
map()

​ 可以对流中元素进行计算或转换。

例如:

​ 打印所有作家的姓名。

1
2
3
4
List<Author> authors = getAuthors();
authors.stream()
.map(Author::getName)
.forEach(System.out::println);
distinct()

​ 可以去除流中的重复元素。 ==去重==

例如:

​ 打印所有作家的姓名,并且要求其中不能有重复元素。

1
2
3
4
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.forEach(author -> System.out.println(auther.getName()));

注意:distinct方法是依赖于Object的equal()方法来判断是否为相同对象的,因此需记得重写equal()和hashcode()方法。

sorted()

​ 可以对流中的元素进行排序。

例如:

​ 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。

1
2
3
4
5
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted()
.forEach(author -> System.out.println(author.getName()));
1
2
3
4
5
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted((a1, a2) -> a2.getAge()-a1.getAge())
.forEach(author -> System.out.println(author.getName()));

注意:如果调用无参sorted()方法,则需要流中元素实现Comparable接口。(判断逻辑自行在该元素对应的类中书写)

limit()

​ 可以设置流的最大长度m,超出的部分将被抛弃。

例如:

​ 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两位作家的姓名。

1
2
3
4
5
6
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted()
.limit(2)
.forEach(author -> System.out.println(author.getName()));

读者易见,此处也可以用filter()来完成需求,但limit()好处是无需再对流中元素进行遍历。

skip()

​ 跳过流中的前n个元素,返回剩下的元素。

例如:

​ 打印除年龄最大的作家外其他作家的姓名,并且要求不能有重复的元素,并按照年龄降序排序。

1
2
3
4
5
6
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted()
.skip(1L)
.forEach(author -> System.out.println(author.getName()))
flatMap()

​ 把一个对象转换成多个对象作为流中的元素。 ==剥洋葱==

例一:打印所有书籍的名称,并要求去除重复元素。

1
2
3
4
5
List<Author> authors = getAuthors();
authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.forEach(book -> System.out.println(book.getName()));

例二:打印现有的书籍分类。要求去除重复元素,并且不能出现 “哲学,爱情“ 的格式。

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
authors.stream()
.flatMap(author -> author.getBooks.stream())
.distinct()
.flatMap(book -> Arrays.stream(book.getCategory().split(',')))
.distinct()
.forEach(System.out::println);
peek()

​ 与**forEach()**类似,但为中间操作,可继续进行接下来的步骤 ==常用来调试==

​ 让我康康(雾

终结操作

forEach()

​ 对流中的元素进行遍历,对其进行操作。

(上面全是例子随便看吧)

count()

​ 可以用来获取当前流中元素的个数。

例子:打印这些作家所出书籍的数目,注意删除重复元素。

1
2
3
4
5
6
List<Author> authors = getAuthors();
long count = authors.stream()
.flatMap(author -> author.getBooks.stream())
.distinct()
.count();
System.out.println(count);
max() & min()

​ 可以用来获取当前流中的最值。

例子:分别获取这些作家的所出书籍的最高分和最低分并打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Author> authors = getAuthors();
Optional<Integer> max = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.map(book -> book.getScore())
.max((score1, score2) -> score1 - score2);

Optional<Integer> min = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.map(book -> book.getScore())
.min((score1, score2) -> score1 - score2);

System.out.println(max.get());
System.out.println(min.get());
collect()

​ 把当前流转换成一个集合。

例一:获取一个存放所有作者名字的List集合。

1
2
3
4
5
6
List<Author> authors = getAuthors();
List<String> authorNameList = authors.stream()
.map(author -> author.getName())
.collect(Collectors.toList());

System.out.println(authorNameList);

注意:Collectors为java.util.stream包下的Collectors工具类。

例二:获取一个所有书名的Set集合。

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
Set<String> bookNameSet = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getName())
.collect(Collectors.toSet());

System.out.println(bookNameSet);

例三:获取一个Map集合,Map的key为作者名,value为List

1
2
3
4
5
List<Author> authors = getAuthors();
Map<String, List<Book>> map = authors.stream()
.collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));

System.out.println(map);
查找与匹配
anyMatch()

​ 判断是否有任意符合匹配条件的元素,返回true / false.

例子:判断是否有年龄在29以上的作家

1
2
3
4
5
List<Author> authors = getAuthors();
boolean flag = authors.stream()
.anyMatch(author -> author.getAge > 29);

System.out.println(flag);
allMatch()

​ 判断是否所有元素都符合条件,都符合返回ture,否则返回false.

例子:判断是否所有的作家都是成年人

1
2
3
4
5
List<Author> authors = getAuthors();
boolean flag = authors.stream()
.allMatch(author -> author.getAge >= 18);

System.out.println(flag);
noneMatch()

​ 判断是否所有元素都不符合条件,都不符合返回true,否则返回false.

例子:判断所有作家是否都没有超过100岁

1
2
3
4
5
List<Author> authors = getAuthors();
boolean flag = authors.stream()
.noneMatch(author -> author.getAge > 100);

System.out.println(flag);
findAny()

​ 获取流中的任意一个元素(无法保证该元素为第一个)

例子:获取任意一个大于18岁的作家,如果存在就输出他的名字

1
2
3
4
5
6
List<Author> authors = getAuthors();
Optional<Author> optionalAuthor = authors.stream()
.filter(author -> author.getAge() > 18)
.findAny();

optionalAuthor.ifPresent(author -> System.out.println(author.getName()));
findFirst()

​ 获取流中的第一个元素

例子:获取一个年龄最小的作家,并输出他的姓名。

1
2
3
4
5
6
List<Author> authors = getAuthors();
Optional<Author> optionalAuthor = authors.stream()
.sorted((author1, author2) -> author1.getAge() - author2.getAge())
.findFirst();

optionalAuthor.ifPresent(author -> System.out.println(author.getName()));
reduce归并

​ 对流中的数据按照指定计算方式计算出一个结果 ==缩减==

​ reduce作用是把stream中的元素组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算。

​ 内部计算方式如下:

1
2
3
4
5
T result = identity;
for (T element : this stream) {
result = accumulator.apply(result, element);
}
return result;

​ 其中identity就是我们通过方法参数传入的初始值,accumulator的apply()具体进行什么计算也是我们通过方法参数来确定的。

例一:使用reduce求所有作者年龄的和

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
Integer sum = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(0, (result, element) -> result + element);

System.out.println(sum);

例二:使用reduce求所有作者中年龄的最大值

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
Integer max = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(Integer.MIN_VALUE, (result, element) -> result > element ? result : element);

System.out.println(max);

例三:使用reduce求所有作者中年龄的最小值

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
Integer min = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(Integer.MIN_VALUE, (result, element) -> result < element ? result : element);

System.out.println(min);

注意事项

  • 惰性求值(如果无终结操作,只有中间操作不会执行)

  • 一次性(一个流对象经过一次终结操作后就会关闭,无法再次使用)

  • 不影响原数据(正常情况)

Optional

概述

​ 我们在编写代码的时候经常会遇到空指针异常,因此需要做很多非空的判断,尤其是一个对象的某个属性还是对象的情况下,此类判断会倚叠如山,使我们的代码臃肿不堪。

​ JDK8中引入了Optional类,可以使用优雅简洁的方式避免空指针异常。

使用

创建对象

​ Optional与包装类类似,可以把具体数据封装到Optional对象内部。

​ ==(推荐)==一般使用Optional的**静态方法ofNullable()**把数据封装成Optional对象,==数据可为空==。

1
2
Author author = getAuthor();
Optional<Author> authorOptional = Optional.ofNullable(author);

​ 如果确定一个==对象不为空==,则可以使用Optional的**静态方法of()**把数据封装成Optional对象。

1
2
Author author = new Author();
Optional<Author> authorOptional = Optional.ofNullable(author);

​ 传入参数必须不为空,若为空则会抛出异常。

​ 如果确定返回值为null,可使用Optional的**静态方法empty()**将null封装成Optional对象返回。

1
Optional.empty()

安全消费值

​ 获取到Optional对象后,可使用**ifPresent()**方法来消费其中的值。

​ 此方法会判断其中封装数据是否为空,不为空时才会执行具体的消费代码。 ==安全性UPUP==

​ (避免空指针异常)

​ eg.

1
2
3
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());

authorOptional.ifPresent(author -> System.out.println(author.getName()));

获取值

​ ==(不推荐)==可使用get()方法获取数据,但Optional对象封装数据为空时会出现异常。

安全获取值

  • orElseGet()

    获取数据并且设置封装数据为空时的默认值

    1
    2
    3
    Optional<Author> authorOptional =  Optional.ofNullable(getAuthor());

    Author author1 = authorOptional.orElseGet(() -> new Author());
  • orElseThrow()

    获取数据,封装数据为空时创建异常并抛出。

    1
    2
    3
    4
    5
    6
    7
    Optional<Author> authorOptional =  Optional.ofNullable(getAuthor());

    try {
    Author author1 = authorOptional.orElseThrow(() -> new RuntimeException("数据为null"));
    } catch (Throwable throwable) {
    throwable.printStackTrace();
    }

过滤

​ 可使用**filter()**方法对Optional对象中的数据进行过滤,如果原本有数据但不符合过滤条件,会变成无数据的Optional对象。

1
2
3
4
Optional<Author> authorOptional =  Optional.ofNullable(getAuthor());

authorOptional.filter(author -> author.getAge >= 18)
.ifPresent(author -> System.out.println(author.getName()));

判断

​ 可使用**isPresent()**方法判断封装数据是否为空,不为空返回true,反之返回false.

这样写不能体现Optional的好处,推荐使用ifPresent()方法。

1
2
3
4
5
Optional<Author> authorOptional =  Optional.ofNullable(getAuthor());

if (authorOptional.isPresent) {
System.out.println(authorOptional.get().getName());
}

数据转换

​ 可使用**map()**方法对数据进行转换,且转换得到的数据也是Optional的包装对象。

eg. 获取作家的书籍集合

1
2
3
4
Optional<Author> authorOptional =  Optional.ofNullable(getAuthor());
Optional<List<Book>> booksOptional = authorOptional.map(author -> author.getBooks());

booksOptional.ifPresent(books -> books.forEach(book -> System.out.println(book.getName())));

函数式接口

概述

只有一个抽象方法的接口称之为函数式接口(Functional Interface)。

​ JDK的函数式接口都加上了 @FunctionalInterface 注解进行标识。但无论有无此注解,只要接口中只有一个抽象方法,都是函数式接口。

常见函数式接口

  • ​ Consumer 消费接口

  • ​ Function 计算转换接口

  • ​ Predicate 判断接口

  • ​ Supplier 生产接口

常用的默认方法

​ 使用Predicate等接口时可能需要进行判断条件的拼接。

这样只能使用匿名内部类,无法改为lambda表达式,故一般只在自己定义一些使用函数式接口的方法时使用

  • and()

    相当于 &&

  • or()

    相当于 ||

  • negate()

    相当于 !

方法引用

​ 使用lambda表达式时,如果方法体中只调用了一个方法(包括构造方法),可以使用方法引用进一步简化代码。

推荐用法

​ IDEA大法【Alt + Enter】

基本格式

​ 类名或对象名::方法名

语法详解

引用类的静态方法

格式

1
类名::方法名

使用前提

​ 方法体中只有一行代码,并且这行代码调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个静态方法

引用对象的实例方法

格式

1
对象名::方法名

使用前提

​ 方法体中只有一行代码,并且这行代码调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个成员方法

引用类的实例方法

格式

1
类名::方法名

使用前提

​ 方法体中只有一行代码,并且这行代码调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有参数都按照顺序传入了这个成员方法

eg.

1
2
3
private void sort(List<String> names){
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
}

等效于

1
2
3
private void sort(List<String> names){
Collections.sort(names, String::compareTo);
}

构造器引用

​ 如果方法体中的唯一一行代码是构造方法,就可以使用构造器引用。

格式

1
类名::new

使用前提

​ 方法体中只有一行代码,并且这行代码调用了某个类的构造方法,并且我们把要重写的抽象方法中的所有参数都按照顺序传入了这个构造方法

eg.

1
2
3
4
5
6
List<Author> authors = getAuthors();
authors.stream()
.map(author -> author.getName())
.map(name -> new StringBuilder(name))
.map(sb -> sb.append("祤").toString())
.forEach(name -> System.out.println(name));

第4行等价于

1
.map(StringBuilder::new)

顺便把其他几步的lambda表达式改成方法引用:

1
2
3
4
5
6
List<Author> authors = getAuthors();
authors.stream()
.map(Author::getName)
.map(StringBuilder::new)
.map(sb -> sb.append("祤").toString())
.forEach(System.out::println);

高级用法

基本数据类型优化

​ 我们之前用到的很多Stream的方法都使用了泛型,所以涉及到的参数和返回值都是引用数据类型。即使我们操作的是整数、浮点数,但我们实际使用的都是它们的包装类。

​ 众所周知JDK5中引入了自动装箱和自动拆箱,让我们在使用对应的包装类时就像使用基本数据类型一样方便。但在操作大量数据时,重复的拆箱和装箱必然会造成大量时间损耗。

​ 为了对这种情况进行优化,Stream还提供了很多针对基本数据类型的方法。

​ 例如 mapToInt(), mapToDouble(), flatMapToLong() 等等.

​ eg.

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
authors.stream() // Stream<Author>
.map(Author::getAge) // Stream<Integer>
.map(age -> age + 20) // Stream<Integer>
.filter(age -> age >= 18) // Stream<Integer>
.map(age -> age + 5) // Stream<Integer>
.forEach(System.out::println);

​ 可优化为

1
2
3
4
5
6
7
List<Author> authors = getAuthors();
authors.stream() // Stream<Author>
.mapToInt(Author::getAge) // IntStream
.map(age -> age + 20) // IntStream
.filter(age -> age >= 18) // IntStream
.map(age -> age + 5) // IntStream
.forEach(System.out::println);

​ 第3行的mapToInt() 直接将 Stream转为了IntStream,避免了后续大量冗余的拆箱和装箱,在数据量很大时可以节省很多时间。

并行流

​ 当流中有大量元素时,可以使用并行流去提高操作的效率。 ==多线程==

​ 如果自己用代码实现会非常复杂,需要对并发编程有足够的掌握;

​ 而如果我们使用Stream,我们只需要修改一个方法就可以使用并行流了,从而提高了我们的效率。

​ 方法一: stream对象.parallel()

1
2
3
4
List<Author> authors = getAuthors();
authors.stream().parallel() // Here!
.mapToInt(Author::getName)
.forEach(System.out::println);

​ 方法二:集合.parallelStream()

1
2
3
4
List<Author> authors = getAuthors();
authors.parallelStream() // Here!
.mapToInt(Author::getName)
.forEach(System.out::println);