Java 8引入函数式编程,好处:
- 代码简洁,意图明确,使用stream接口而不是for循环。
- 多核友好,parallel()方法。
相关知识
高阶函数
高阶函数就是接收函数参数的函数,能够根据传入的函数参数调节自己的行为。类似C语言中接收函数指针的函数。最经典的就是接收排序比较函数的排序函数。在Java8之前,就是那些可以接收回调接口作为参数的方法;接收 Function, Consumer, Supplier 作为参数的函数都是高阶函数。高阶函数使得函数的能力更加灵活多变。
面向接口编程
面向接口的编程,针对接口而非具体类型进行编程,可以降低程序的耦合性、提高灵活性、提高复用性。
接口常被用于传递代码,通过接口传递行为代码,就要传递一个实现该接口的实例对象,可以使用匿名内部类。
Lambda
Wikipedia:
a function (or a subroutine) defined, and possibly called, without being bound to an identifier。
一个不用被绑定到一个标识符上,并且可能被调用的函数。
Lambda表达式本质上是一个匿名方法,可以这样理解:一段带有输入参数的可执行语句块。一种紧凑的传递代码的方式。
需求:列出当前目录下的所有后缀为.txt的文件。可以使用匿名内部类:
// 列出当前目录下的所有后缀为.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;
}
});
匿名内部类其他常用场景:Comparator 实现排序比较;Runnable/Callable 运行线程;
使用Lambda方式改写:
File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {
if (name.endsWith(".txt")) {
return true;
}
return false;
});
相比匿名内部类,传递代码变得更为直观,不再有实现接口的模板代码,不再声明方法,名字也没有,而是直接给出方法的实现代码。Lambda表达式由->
分隔为两部分,前面是方法参数,后面{}内是方法代码。
if……else 简化:
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语句。
进一步,方法的参数类型声明也可以省略,借助于自动推断(listFiles接受的参数类型是FilenameFilter接口,只有一个方法accept,其两个参数类型分别是File和String):
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
方法参数部分为空,写为():
函数式接口
JDK 1.8内置很多函数式接口:Comparator,Runnable。对这些现成的接口进行实现,可以通过@FunctionalInterface
来启用Lambda功能支持。
谓词链
谓词,即Predicate函数接口,JDK8引入,用于filter()操作中,该函数接口提供and(), or(), Predicate.negate()
方法;
实例
// 最简单的过滤,使用一个谓词
List<String> names = Arrays.asList("Adam", "Alexander", "John", "Tom");
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
// 两个谓词使用两个filter过滤
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.filter(name -> name.length() < 5)
.collect(Collectors.toList());
// 两个谓词使用与运算符
List<String> result = names.stream()
.filter(name -> name.startsWith("A") && name.length() < 5)
.collect(Collectors.toList());
// 使用and(), or(), negate()方法
Predicate<String> predicate1 = str -> str.startsWith("A");
Predicate<String> predicate2 = str -> str.length() < 5;
List<String> result = names.stream()
.filter(predicate1.and(predicate2))
.collect(Collectors.toList());
// 组合
List<String> result = names.stream().filter(((Predicate<String>) name -> name.startsWith("A"))
.and(name -> name.length() < 5))
.collect(Collectors.toList());
// 组合一组
List<Predicate<String>> allPredicates = new ArrayList<>();
allPredicates.add(str -> str.startsWith("A"));
allPredicates.add(str -> str.contains("d"));
allPredicates.add(str -> str.length() > 4);
List<String> result = names.stream()
.filter(allPredicates.stream().reduce(x -> true, Predicate::and))
.collect(Collectors.toList());
List<String> result1 = names.stream()
.filter(allPredicates.stream().reduce(x -> false, Predicate::or))
.collect(Collectors.toList());
stream
Java函数式编程的主角,stream并不是某种数据结构,它只是数据源的一种视图。数据源可以是一个数组,Java容器或I/O channel等。stream通常不会手动创建,而是调用对应的工具方法来创建一个流对象:
- 调用
Collection.stream()
或者Collection.parallelStream()
方法; - 调用
Arrays.stream(T[] array)
方法。
IntStream, LongStream, DoubleStream对应三种基本类型(int, long, double),Stream对应所有剩余类型的stream视图。为不同数据类型设置不同stream接口,可以提高性能,增加特定接口函数。四个Stream接口都是继承自BaseStream接口,是并列平行的关系。
Q:考虑到四个子接口中的方法名大部分是一样的,为什么不把IntStream等设计成Stream的子接口?
A:这些方法的名字虽然相同,但是返回类型不同,如果设计成父子接口关系,这些方法将不能共存,因为Java不允许只有返回类型不同的方法重载。
stream v.s collections
大部分情况下stream是容器调用Collection.stream()方法得到的;但两者有以下不同:
- 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
- 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
- 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:
- 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
- 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。
Stream接口的部分常见方法
中间操作(Intermediate operations):指的是仍然返回 Stream 类型的操作,如 filter, map, limit, sorted 等。中间操作构成是一个管道操作,中间操作不产生任何结果。
终止操作(Terminal operations):指的就是返回非 Stream 类型的操作,包括返回值为 void 的操作,如 findFirst, forEach, count, collect 等。
操作类别 | 方法名 |
中间操作 | concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() |
结束操作 | allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
区分两者的最简单的方法,看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。 |
Stream 的中间操作在未遇到终止操作前是不会处理,它们是懒操作的。中间与终止操作类似于构造者模式,中间操作只是准备部件,在执行终止操作的时候才按需执行前面的中间操作,未执行 build() 方法前什么也不是。
filter是中间操作,findFirst 是终止操作,filter 看到 findFirst 后才开始执行,所以 filter 知道只需要返回一个元素就够,所以找到一个元素也是立即返回,不再对后面的 2, 5, 6, 3 进行对比。
这种立即返回的操作也叫做短路操作,和用 && 和 || 进行 boolean 操作类似。Stream 的短路操作包括:allMatch,noneMatch,findFirst,findAny和limit。
方法使用
stream跟函数接口关系非常紧密,没有函数接口stream就无法工作。函数接口是指内部只有一个抽象方法的接口。通常函数接口出现的地方都可以使用Lambda表达式。
reduction operation,fold operation,折叠操作,是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。Stream类库有两个通用的规约操作reduce()和collect(),以及专用规约操作,比如sum()、max()、min()、count()等。
forEach()
ForEach接受一个function接口类型的变量,用来执行对每一个元素的操作。方法签名为void forEach(Consumer<? super E> action)
,作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历/迭代。由于forEach()是结束方法/中止操作,上述代码会立即执行,输出所有字符串。
distinct()
函数原型为Stream<T> distinct()
,作用是返回一个去除重复元素之后的Stream。
filter()
中间操作,接受一个predicate接口类型的变量,并将所有流对象中的元素进行过滤。函数原型为Stream<T> filter(Predicate<? super T> predicate)
,作用是返回一个只包含满足predicate条件元素的Stream。
中间操作,只调用filter()不会有实际计算,因此也不会输出任何信息。
sorted()
中间操作,返回一个排过序的流对象的视图。排序函数有两个,自然顺序排序、自定义比较器排序,函数原型分别为Stream<T> sorted()
和Stream<T> sorted(Comparator<? super T> comparator)
。sorted只是创建一个流对象排序的视图,而不会改变原来集合中元素的顺序。
map()
map是一个对于流对象的中间操作,函数原型为<R> Stream<R> map(Function<? super T,? extends R> mapper)
,作用是返回一个对当前所有元素执行执行mapping之后的结果组成的Stream,对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。map()方法可以无限级联。对于带泛型结果的流对象,具体的类型还要由传递给map的泛型方法来决定。
match()
match匹配操作有多种不同的类型,都是用来判断某一种规则是否与流对象相互吻合的。所有的匹配操作都是终结操作,只返回一个boolean类型的结果。包括anyMatch(), allMatch(), noneMatch()
。
flatMap()
函数原型为<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
,作用是对每个元素执行mapper指定的操作,并用所有mapper返回的Stream中的元素组成一个新的Stream作为最终返回结果。flatMap()
相当于把原stream中的所有元素都摊平之后组成的Stream,转换前后元素的个数和类型都可能会改变。
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4, 5));
stream.flatMap(Collection::stream).forEach(System.out::println);
原来的stream中有两个元素,分别是两个List<Integer>
,执行flatMap()
之后,将每个List都摊平成一个个的数字,所以会新产生一个由5个数字组成的Stream。
@Data
public class Product {
private List<Integer> productIdList;
}
// productList = List<Product>, 求所有的 productId
productList.stream().map(Product::getProductIdList).flatMap(List::stream).distinct().collect(Collectors.toList());
reduce()
reduce操作可以实现从一组元素中生成一个值,sum(), max(), min(), count()
等都是reduce操作,reduce()
方法定义有三种重写形式:
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner);
多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。
举例:求出一组单词的长度之和,操作对象输入类型是String,而结果类型是Integer。
// 求单词长度之和
Stream<String> stream = Stream.of("learning", "java", "8", "stream");
// 初始值
Integer lengthSum = stream.reduce(0,
// 累加器
(sum, str) -> sum + str.length(),
// 部分和拼接器,并行执行时才会用到
(a, b) -> a + b);
System.out.println(lengthSum);
// 求和方法二,此处注意不要使用上面经过reduce操作过的stream流,否则抛错java.lang.IllegalStateException: stream has already been operated upon or closed
Stream<String> stream2 = Stream.of("learning", "java", "8", "stream");
Integer lengthSum2 = stream2.mapToInt(String::length).sum();
System.out.println(lengthSum2);
总结
reduce()用于生成一个值,collect()用于从Stream生成一个集合或者Map等复杂的对象。
collect()
collect()是Stream接口方法中最灵活的一个,如果你发现某个功能在Stream接口中没找到,十有八九可以通过collect()方法实现。
// 将Stream转换成容器或Map
Stream<String> stream1 = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));
问题
Java9
在 Java 9 中Stream 接口中添加 4 个新的方法:dropWhile, takeWhile, ofNullable, iterate重载方法,可以让你提供一个 Predicate (判断条件)来指定什么时候结束迭代。
向控制台打印 1 到 99:
IntStream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);
第二个参数是一个 Lambda,它会在当前 IntStream 中的元素到达 100 的时候返回 true。
option
很多语言为了处理 null 检查添加特殊的语法,即空合并运算符。在像 Groovy 或 Kotlin 这样的语言中也被称为 Elvis 运算符。
Java 8引入Optional 模板类,可以用它来封装可能为空的引用,应付NPE问题;从本质上来说,该类属于包含可选值的封装类(wrapper class),因此它既可以包含对象也可以仅仅为空。Optional 的方法中基本都是内部调用 isPresent() 判断,真时处理值,假时什么也不做。
Java 9中Optional 类增加三种方法:or()、ifPresentOrElse()、stream()。
问题引出
在 Java 8 之前,凡涉及到访问对象方法或者对象属性的操作,都可能产生NPE:
String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();
为了预防NPE需要这么写:
if (user != null) {
Address address = user.getAddress();
if (address != null) {
Country country = address.getCountry();
if (country != null) {
String isocode = country.getIsocode();
if (isocode != null) {
isocode = isocode.toUpperCase();
}
}
}
}
拓展,针对NPE问题:
- Groovy 提供一种安全的属性或方法访问操作符
?.
:user?.getUsername()?.toUpperCase();
- Swift 也有类似的语法,只作用在 Optional 的类型上
- typescript提供
?
符:const rows = dataJson?.config?.keys?.map((r: any) => ({...r, columnName: r.col})) ?? [];
参考TypeScript ?: and !:、TypeScript ? and ??
入门
empty创建一个空的option实例:
@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() {
Optional<User> emptyOpt = Optional.empty();
emptyOpt.get();
}
用 of() 和 ofNullable(),来创建包含一个值的Optional 对象。
两种方法的区别在于:如果你将 null 值作为参数传入 of() 方法,那么该方法会抛出一个 空指针异常。
@Test(expected = NullPointerException.class)public void whenCreateOfEmptyOptional_thenNullPointerException() {
Optional<User> opt = Optional.of(user);
}
NPE问题并没有得到彻底解决。因此,只有当对象不为 null 时, of()的方法才可行。如果对象既可能为 null ,也可能为非 null ,就必须选择 ofNullable()。
Optional<User> opt = Optional.ofNullable(user);
构造方法
Optional 的三种构造方式:
Optional.of(obj);
Optional.ofNullable(obj);
Optional.empty(); // 明确的
-
Optional.of(obj)
:要求传入的 obj 不能是 null 值的,否则还没开始进入角色就倒在NPE异常上。 -
Optional.ofNullable(obj)
:以一种智能的,宽容的方式来构造一个 Optional 实例。传 null 进到就得到Optional.empty()
,非 null 就调用 Optional.of(obj)
-
Optional.empty()
:
访问 Optional 对象的值
使用Optional.get()方法; 在值为 null 时依旧会抛出异常。为避免出现异常,需要先检验其中是否存在值。
isPresent() & ifPresent(), 第二个方法带有一个 Consumer 参数,在对象不为空时执行 λ 表达式:
返回默认值
用于返回对象值或在对象为空时返回默认值。
orElse
orElseGet, 如果不存在,则其执行 Supplier 函数接口(作为其收到的一个参数),并返回执行结果;
区别
- 当对象为空时,二者在表现上并无差别,都是代之以返回默认值。
- 当对象为空时,orElse()方法仍然会创建默认的 User 对象。orElseGet()方法将不再创建 User 对象。
返回异常
ElseThrow():在对象为空时,直接抛出一个异常(可以自定义异常),而不是返回一个替代值。
对值进行转换
map():Map() 将 Function 参数作为值,然后返回 Optional 中经过封装的结果。
flatMap():也是将 Function 参数作为 Optional 值,但它后面是直接返回结果。
对值进行过滤
filter():将predicate 作为参数,当测试评估为真时,返回实际值。否则,当测试为假时,返回值则为空 Optional。
对 Optional 类的方法进行链接
String result = Optional.ofNullable(user)
.flatMap(User::getAddress)
.flatMap(Address::getCountry)
.map(Country::getIsocode)
.map(String::toUpperCase)
.orElse("default");
代码比先前冗长的条件驱动(conditional-driven)版本要简洁许多。
拓展:
针对上面这种嵌套null检查问题,另一种解决方法就是利用一个 supplier 函数来解决嵌套路径的问题:
User user = new User();
resolve(() -> user.getAddress().getCountry().getIsocode());
.ifPresent(System.out::println);
调用user.getAddress().getCountry().getIsocode()
可能会抛出一个 NPE异常。在这种情况下,该异常将会被捕获,而该方法会返回 Optional.empty()
。
public static <T> Optional<T> resolve(Supplier<T> resolver) {
try {
T result = resolver.get();
return Optional.ofNullable(result);
}
catch (NullPointerException e) {
return Optional.empty();
}
}
和stream一样,和option类似的API:OptionalDouble、OptionalInt、OptionalLong 都是 final 类,Option支持泛型,这三个不支持,其他方法大多类似;
Java 9 新增特性
or():同 orElse() 和 orElseGet() 类似,都是在对象为空时提供替换功能
ifPresentOrElse() 带有两个参数:Consumer 和 Runnable。如果对象包含一个值,则会执行 Consumer 动作;否则,会执行 Runnable 动作。
stream():将Optional实例转换为一个 Stream 对象。如果 Optional 不存在值,则 Stream 为空,如果 Optional 包含一个非 null 值,则 Stream 会包含单个值。
使用注意事项
- Optional 并不能序列化;如果您需要序列化一个包含 Optional 值的对象,Jackson可支持将Optionals当作普通对象来对待。Jackson 会将空对象作为 null,它还会将有值对象当作一个包含该值的字段。这个功能可在jackson-modules-java8项目中找到。
- 将该类型作为方法或者构造函数的参数。这将导致不必要的代码复杂化。
相反,使用方法重载(method overloading)来处理非强制性参数要方便得多。
Optional的主要用途是作为一种返回类型。在获得该类型的一个实例后,如果存在值,您可以提取该值,如果不存在值,则您可以获得一个替换值。
Optional类同 stream 或者其他方法的组合使用,这些方法会返回一个可构建流畅 API 的Optional 值。
isPresent() 与 obj != null 无任何分别,而没有 isPresent() 作铺垫的 get() 调用在 IDEA 中会收到告警:
Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value,get() will throw an exception. (调用 Optional.get() 前不事先用 isPresent() 检查值是否可用. 假如 Optional 不包含一个值,get() 将会抛出一个异常)
把 Optional 类型用作属性或是方法参数在 IDEA 中更是强力不推荐的:
Reports any uses of java.util.Optional,java.util.OptionalDouble,java.util.OptionalInt,java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable,which java.util.Optional is not. (使用任何像 Optional 的类型作为字段或方法参数都是不可取的. Optional 只设计为类库方法的,可明确表示可能无值情况下的返回类型. Optional 类型不可被序列化,用作字段类型会出问题的)
其他
IO流
方法有很多;包括可以使用Apache commons 类库FileUtils。Java8引入walk()方法,它遍历目录后会创建出一个惰性流(文件系统很大的情况下非常有用)。
日期API
Date类增加一个新的方法toInstant():
public Instant toInstant() {
return Instant.ofEpochMilli(getTime());
}
CompletableFuture
Java 8引进CompletableFuture,继承自Future<T>
。当不希望或者不需要一个直接计算结果时,会收到一个Future对象来保存计算完成时分配的实际结果。通过调用complete()方法并且无需异步等待即可显式完成。它还允许在一系列操作中构建管道数据流程。这样,任何类型的可用值都可以在Future中使用默认返回值,即使计算没有完成。这也将成为CompletableFuture提案更新的一部分,包括延迟和超时、更好地支持子类化和一些实用方法。
参考
了解、接受和利用Java中的Optional (类)
Java Stream API 进阶篇