一、Lambda表达式
Lambda表示式也称为闭包,允许把函数作为参数传递进方法中,使用Lambda可以使表达式更简单。
(parameters) -> expression
或者(parameters) -> {statements;}
重要特性:
- parameters中的参数类型可以不声明,编译器可以统一识别参数值
- 定义一个参数时,parameters中的圆括号不写;多个参数时需要用圆括号定义,中间用逗号隔开
- 箭头后面只有一个语句时,不用写大括号,且不用写return语句,编译器会自动返回值
Lambda表达式的简单例子:
//1、不需要参数,返回值为5
() -> 5
//2、接收一个数字类型的参数,返回其2倍的值
x -> 2*x
//3、接收2个参数,并返回他们的差
(x,y) -> x-y
//4、接收2个int型整数,返回他们的和
(int x, int y) -> x+y
//5、接收一个String类型对象,并在控制台打印
(String s) -> System.out.println(s)
//6、接收2个int型整数,并在大括号中加上return语句
(int a,int b) -> {return a+b;}
注意点:
- Lambda表达式只能引用标记了final的外层局部变量,即不能在lambda表达式内部修改定义在外部的局部变量,否则会报错。
- 如果外部变量不定义为final,之后如果被重新赋值(引用类型内部状态修改除外),就会出现内部无法看见外部,外部也无法看见内部的问题;Java不支持引用传递,没有nonlocal这样的机制。
- lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
- 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
二、方法引用
方法引用(method reference)通过使用一对冒号::
,通过方法的名字来指向一个方法。
-
构造器引用:
类名::new
,或者更一般的Class<T>::new
Supplier<Student> s1 = Student::new; //无参构造器 Function<String, Student> s2 = Student::new; //一个参数构造器 BiFunction<String, Integer, Student> s3 = Student::new; //两个参数构造器 三个参数的构造器需要自定义函数式接口: public interface ThreeFunction<T, U, V, R> { R apply(T t, U u, V v); } 使用方法: ThreeFunction<String, String, Integer, Student> s4 = Student::new;
-
静态方法引用:
类名::静态方法名
-
类中普通方法引用:
类名::普通方法名
-
实例对象的方法引用:
实例类名::方法名
三、函数式接口
函数式接口:只有一个抽象方法的接口,可以隐式转换为Lambda表达式。Java8提供了@FunctionalInterface注解显式地说明某个接口是函数式接口,用于编译级错误检查,当我们在函数式接口中定义了连个及以上抽象方法时,会报错。但是默认方法(default)和静态方法(static)不会破环函数式接口的定义。例如,如下接口符合函数式接口的定义:
@FunctionalInterface
public interface FunctionDefaultMethods {
void method();
default defaultMethod();
static staticMethod();
}
默认方法 vs. 抽象方法
区别在于:接口中的抽象方法必须实现,而实现默认方法会被实现类继承或者覆写,如下所示:
@FunctionalInterface
private interface Defaulable {
default String notRequired() {
return "Default implementation";
}
}
// 实现类DefaultableImpl不覆写,则默认继承notRequired方法。
private static class DefaultableImpl implements Defaulable {}
// 实现类OverridableImpl覆写notRequired方法。
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
静态方法允许出现在函数式接口中,是因为它是一个已经实现的方法,符合函数式接口的定义。
@FunctionalInterface
private interface DefaulableFactory {
static Defaulable create(Supplier<Defaulable> supplier) {
return supplier.get();
}
}
Defaulable defaulable = DefaulableFactory.create(DefaultableImpl::new);
System.out.println(defaulable.notRequired());//Default implementation
defaulable = DefaulableFactory.create(OverridableImpl::new);
System.out.println(defaulable.notRequired());//Overridden implementation
由于JVM为默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法可以在不打破现有继承体系的基础上改进接口,例如:java.util.Collection接口添加新默认方法,如stream()、parallelStream()、removeIf(),Iterable接口中实现的forEach()方法等等。尽管默认方法有很多好处,但是在实际开发中应该谨慎使用,因为在复杂的继承体系中,默认方法可能会引起歧义和编译错误。
四、Optional
Java8引入的Optional类是为了解决空指针异常(NullPointerException)的问题。
Optional中可以有值也可能为null,它提供了一些有用的方法来避免显式的null检查。
empty()---新建一个空的Optional
Optional<String> emptyOpt = Optional.empty();
emptyOpt.get();
访问emptyOpt变量的值会导致NoSuchElementException。
of()与ofNullable()---新建非空的Optional
两个方法的不同之处在于:将null值作为参数传递进去时,of()方法会抛出NullPointerException,而ofNullable()方法两者都可以处理。
get()---访问Optional对象的值
String name = "John";
Optional<String> opt = Optional.ofNullable(name);
assertEquals("John", opt.get());
但是,get()方法会在值为null的时候抛出异常,为了避免异常,可以先使用下面的ifPresent()验证是否有值。
ifPresent()---检查Optional对象是否有值
该方法除了执行检查,还接受一个Consumer(消费者)参数。如果对象非空才会执行assertEquals断言。
Optional<User> opt = Optional.ofNullable(user);
assertTrue(opt.isPresent());
assertEquals(user.getEmail(), opt.get().getEmail());
orElse()---传入参数非空则返回参数值,否则返回设置的默认值
User user = null;
User user2 = new User("anna@gmail.com", "1123");
User re = Optional.ofNullable(user).orElse(user2);
assertEquals(user2.getEmail(), re.getEmail());
这里user对象是空的,所以返回了作为默认值的user2。
orElseGet()---传入参数非空则返回参数值,否则执行作为参数传入的Supplier(供应者)函数式接口,并将返回其执行结果
User re = Optional.ofNullable(user).orElseGet(() -> user2);
orElse()和orElseGet()的不同
public void test() {
User user = null;
User re = Optional.ofNullable(user).orElse(createNewUser());
User re2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}
private User createNewUser() {
return new User("extra@gamil.com", "1234");
}
当传入对象为空时,两者的结果时一样的;但是当传入对象为非空时,orElse()方法仍然会创建User对象,但是orElseGet()方法不会创建User对象。
public void test() {
User user = new User("john@gmail.com", "1234");
User re = Optional.ofNullable(user).orElse(createNewUser());
User re2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}
private User createNewUser() {
return new User("extra@gamil.com", "1234");
}
在执行较为密集的调用时,比如调用Web服务或数据查询,这个差异会对行能产生重大影响。
orElseThrow()---对象为空时抛出指定的异常
User re = Optional.ofNullable(user).orElseTrow(() -> new IllegalArgumentException());
这个方法让我们可以决定抛出什么样的异常,而不总是NullPointerException。
map()---对值调用作为参数的函数,然后将返回的值包装在Optional中
flatMap()---对值调用作为参数的函数,然后将返回的值包装在Optional中
filter()---按条件过滤值,接受一个Predicate参数,返回测试结果为true的值,如果测试结果为false,则返回一个空的Optional
Java9为Optional类增加了三个方法:or()、ifPresentOrElse()和stream()。or()和orElse()、orElseGet()类似,or()的返回值是由Supplier参数产生的另一个Optional对象。
ifPresentOrElse() 方法需要两个参数:一个 Consumer 和一个 Runnable。如果对象包含值,会执行 Consumer 的动作,否则运行 Runnable。如果你想在有值的时候执行某个动作,或者只是跟踪是否定义了某个值,那么这个方法非常有用:
Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
() -> logger.info("User not found"));
最后介绍的是新的 stream() 方法,它通过把实例转换为 Stream 对象,让你从广大的 Stream API 中受益。如果没有值,它会得到空的 Stream;有值的情况下,Stream 则会包含单一值。
我们来看一个把 Optional 处理成 Stream 的例子:
@Test
public void whenGetStream_thenOk() {
User user = new User("john@gmail.com", "1234");
List<String> emails = Optional.ofNullable(user)
.stream()
.filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
.map( u -> u.getEmail())
.collect(Collectors.toList());
assertTrue(emails.size() == 1);
assertEquals(emails.get(0), user.getEmail());
}
Optional 主要用作返回类型。在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。
Optional 类有一个非常有用的用例,就是将其与流或其它返回 Optional 的方法结合,以构建流畅的API。
我们来看一个示例,使用 Stream 返回 Optional 对象的 findFirst() 方法:
@Test
public void whenEmptyStream_thenReturnDefaultOptional() {
List<User> users = new ArrayList<>();
User user = users.stream().findFirst().orElse(new User("default", "1234"));
assertEquals(user.getEmail(), "default");
}
Optional 是 Java 语言的有益补充 —— 它旨在减少代码中的 NullPointerExceptions,虽然还不能完全消除这些异常。
它也是精心设计,自然融入 Java 8 函数式支持的功能。
总的来说,这个简单而强大的类有助于创建简单、可读性更强、比对应程序错误更少的程序。
五、Stream流式计算
流Stream将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行筛选、排序、聚合等处理,元素Stream在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
一个Stream
由 数据输入 + 0个或多个中间操作函数 + 1个最终操作函数。它是一个流水线操作,用来对一系列元素做流水线操作,但不会改变原来输入的数据。
中间操作(Intermediate Operations)
filter用来对所给的元素按照某种条件筛选,只留下符合条件的元素;map返回的是一个stream对象,相当于将一个集合的元素通过一个函数进行映射,返回的是映射结果集合组成的stream对象;sorted用来对stream中的元素排序
最终操作(Terminal Operations)
collect方法用于手机中间操作的结果,将stream对象还原成原来的数据结构或者转为其它数据结构返回。
List<Integer> number = Arrays.asList(2,3,4,5);
Set square = number.stream().map(x->x*x).collect(Collectors.toSet());
forEach方法可以迭代Stream
中的所有元素,执行给定的操作(如打印或写数据流)
reduce 方法用来将stream
对象中的元素进行累计操作,最终变成一个值,并将这个值返回。如返回stream
所有元素的累加值或者累积值等。
List<Integer> number = Arrays.asList(2,3,4,5);
// 先用filter将number中的偶数筛选出来,然后把所有的筛选结果加起来作为返回值。reduce的一个参数是累计的初始值,第二个参数指定累计的操作。
int even = number.stream().filter(x->x%2==0).reduce(0,(ans,i)-> ans+i);
这里ans
变量初始值为0
,i
表示number
中的元素,将number
中的所有元素与ans
相加并返回ans
。
查找与匹配
短路操作符:allMatch anyMatch noneMatch findFirst findAny limit skip
anyMatch:流中是否有一个元素能匹配给定的谓词
allMatch:看看流中的元素是否都能匹配给定的谓词
noneMatch:以确保流中没有任何元素与给定的谓词匹配
findAny:将返回当前流中的任意元素,可以与其他流操作结合使用
findFirst:返回第一个元素
findAny VS. findFirst:
- 两者通常与Optional类一起使用(可能返回空)
- 对于顺序流式处理,findFirst和findAny返回的结果一样
- findFirst在并行上限制较多,如果不关心返回的元素,可以使用findAny
limit(n):取前n个元素
skip(n):跳过前n个元素
归约---reduce
Lambda反复调用每个元素,直到流被归约成一个值,这样的查询可以被归类为归约操作(将流归约为一个值)
用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操 作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
eg:求和:int sum = nums.stream().reduce(0, (a, b) -> a+b);
//0作为第一个参数(a)的初始值,nums中的第一个元素作为第二个参数(b)的初始值,更简洁的写法:
int sum = nums.stream().reduce(0, Integer::sum);
无初始值:
reduce可以不传初始值,但是会返回一个Optional对象:
Optional<Integer> sum = nums.stream().reduce((a, b) -> a + b));
为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。
最大值和最小值:
Optional<Integer> max = nums.stream().reduce(Integer::max);
Optional<Integer> min = nums.stream().reduce(Integer::min);
Map-Reduce模型
map与reduce的连接通常称为map-reduce模式,因为其很容易并行化,Google用它进行网络搜索。
eg:用map和reduce统计菜单中一共有多少个菜?
int cnt = menu.stream().map(d -> 1).reduce(0, (a, b) -> a+b);
int cnt = menu.stream().count();
归约方法的优势与并行化
相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉 了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum, 这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升! 但现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。
原始类型流转化
1、映射到数值流
-
mapToInt
-
mapToDouble
-
mapToLong
上述三个原始类型流接口可以将流中的元素转化为int、double和long,从而避免了暗含的装箱成本。eg:下面两段代码,第一段代码有装箱的成本,第二段代码直接对原始类型进行求和。
int calories = menu.stream() .map(Dish::getCalories) //Stream<Integer> .reduce(0, Integer::sum);
int calories = menu.stream() .mapToInt(Dish::getCalories) //IntStream .sum(); //IntStream还支持max、min、average
2、转换回对象流
IntStream上的操作只能产生原始整数,并且IntStream 的 map 操作接受的 Lambda 必须接受 int 并返回 int (一个 IntUnaryOperator)。把原始流转换成为一般流,可以用boxed方法,eg:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //Stream流转换为数值流 Stream<Integer> stream = intStream.boxed();// 数值流转换为Stream流
3、默认值Optional类
在求IntStream的最大值时,如果元素为空返回值默认为0,如何区分没有元素的流和最大值就是0的流?Optional类提供了:OptionalInt、OptionalDouble和OptionalLong三种原始类型特化版本。eg:
OptionalInt maxCalories = menu.stream() .mapToInt(Dish::getCalories) .max(); // 如果流为空的话,可以显式处理OptionalInt去定义一个默认值 int max = maxCalories.orElse(1); // 如果没有最大值,显式提供一个默认最大值
4、数值范围
Java8引入了静态方法range和rangeClosed,可以用于IntStream和LongStream,这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range是不包含结束值的,而rangeClosed则包含结束值。eg:
IntStream evenNums = IntStream.rangeClosed(1, 100) // 表示范围:(1,100] .filter(n -> n%2 == 0); System.out.println(evenNums.count()); // 统计偶数个数 IntStream evenN = IntStream.range(1, 100) // 表示范围:(1,100) .filter(n -> n%3 == 0);
5、创建流的方式
1、由值创建流
可以使用静态方法Stream.of
,通过显式值创建一个流,它可以接受任意数量的参数。eg:
Stream<String> stream = Stream.of("Java 8", "Lambda", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
也可以用empty
得到一个空流
Stream<String> stream = Stream.empty();
2、由数组创建流
静态方法Arrays.stream
表示从数组创建一个流,它接受一个数组作为参数,eg:将一个原始类型int的数组转换成一个IntStream:
int[] nums = {1, 2, 3, 4};
int sum = Arrays.stream(numbers).sum();
3、由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用split方法将行拆分成单词。应该注意的是,你该如何使用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinct和count方法链接起来,数数流中有多少各不相同的单词。
4、由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
Stream.iterate(0, n -> n+2)
.limit(10)
.forEach(System.out::println);
请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。一般来说,在需要依次生成一系列值的时候应该使用iterate,比如一系列日期:1月31日,2月1日,依此类推。
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
Stream的懒加载机制(Lazy)
List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");
final String firstNameWith3Letters = names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name))
.findFirst()
.get();
从经典Java Eager的角度下考虑,首先会遍历集合得到名字长度为3的所有名字(filter),然后再遍历filter之后的集合,将名字转换成大写(map),最后再从大写名字的集合中找到第一个(findFirst)并返回。然而,stream的Lazy机制的执行过程如下:只有当findFirst()方法被调用时。filter和map方法才会被真正触发,而filter
也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map
方法中。当终结操作获得了它需要的元素,整个计算过程结束;如果没有获得答案,那么它会要求中间操作对更多的集合元素进行计算,直到找到答案或者整个集合被处理完毕。
对于Stream
操作,更好的代码阅读顺序是从右到左,或者从下到上。Stream
每一个操作都只会做到恰到好处。
控制台的输出是这样的:
getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
JDK会将所有的中间操作合并成一个,这个过程被称为熔断操作(Fusing Operation)。因此,在最坏的情况下(即集合中没有符合要求的元素),集合也只会被遍历一次,而不会像我们想象的那样执行了多次遍历,也许这就回答了官方文档中为什么说"Processing streams lazily allows for significant efficiencies"了。如果只声明Stream对象的中间操作,没有最终操作,这些中间操作并不会执行。
流操作:有状态和无状态
判断有无状态的标准:是否需要知道先前的数据历史,前后数据是否有依赖关系
map、filter等操作从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作不需要知道上一个元素的状态,只关注当前元素,没有内部状态(假设使用的Lambda或者方法引用没有内部可变状态),称为无状态操作
但诸如reduce、sum、max等操作需要内部状态来累积结果。求最大值、最小值时,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。sort、distinct、limit、skip等操作一开始都和filter和map差不多:都是接受一个 流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操 作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么 呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
[图片上传失败...(image-6dfd0a-1665648613817)]
网友评论