Java8学习笔记

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8学习笔记相关的知识,希望对你有一定的参考价值。


Java 8引入函数式编程,好处:

  1. 代码简洁,意图明确,使用stream接口而不是for循环。
  2. 多核友好,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()方法得到的;但两者有以下不同:

  1. 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  2. 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  3. 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  4. 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:

  1. 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
  2. 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以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 函数接口(作为其收到的一个参数),并返回执行结果;
区别

  1. 当对象为空时,二者在表现上并无差别,都是代之以返回默认值。
  2. 当对象为空时,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 会包含单个值。

使用注意事项

  1. Optional 并不能序列化;如果您需要序列化一个包含 Optional 值的对象,​​Jackson​​​可支持将Optionals当作普通对象来对待。Jackson 会将空对象作为 null,它还会将有值对象当作一个包含该值的字段。这个功能可在​​jackson-modules-java8​​项目中找到。
  2. 将该类型作为方法或者构造函数的参数。这将导致不必要的代码复杂化。
    相反,使用方法重载(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 进阶篇


以上是关于Java8学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

Java8学习笔记

Java8学习笔记

Java学习笔记之二十八深入了解Java8新特性

Java8学习笔记 - 方法引用:Lambda的语法糖

Java8学习笔记 - 方法引用:Lambda的语法糖

Java8学习笔记--Stream API详解[转]