jdk8新特性---------Lambda、Stream操作

为什么要用jdk8的新特性?
1、代码简洁、掌握后编写容易2、性能高于传统操作
一、函数式接口
函数式接口:就是一个接口中只能包含一个抽象方法。没有/多于一个则不是函数式接口。
Lambda只适用于接口式接口,Lambda一般用作参数(匿名内部类)/返回值
二、Lambda
三、方法引用
”::“ 双冒号的方法调用方法:这被称为“方法引用”,是一种新的语法。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用
public class DemoPrintRef {public static void getMax(int[] arr) { int sum = 0; for (int n : arr) {sum += n; } System.out.println(sum); } /* public static void main(String[] args) { //不用;;引用方法时 printMax((int[] arr) -> { int sum = 0; for (int n : arr) { sum += n; } System.out.println(sum); }); } */public static void main(String[] args) { printMax(Demo11MethodRefIntro::getMax); //用::引用上面的方法 } private static void printMax(Consumer consumer) { int[] arr = {10, 20, 30, 40, 50}; consumer.accept(arr); } }

常见引用方式
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
1. instanceName::methodName 对象::方法名
2. ClassName::staticMethodName 类名::静态方法
3. ClassName::methodName 类名::普通方法
4. ClassName::new 类名::new 调用的构造器
5. TypeName[]::new String[]::new 调用数组的构造器
小结
首先了解Lambda表达式的冗余情况,体验了方法引用,了解常见的方法引用方式

四、Stream流
1、整体介绍
stream流主要是用来处理一些集合的,在传统对集合的处理一般是使用for、迭代器、增强for,而jdk8后提出了Lambda表达式,该表达式存在延时执行的功能,在一些场景下能使得性能变高(上面Lambda有举例讲到)(stream流中大量的方法结合Lambda表达式),另外jdk8也为stream提供了顺序流和并行流(fork/join),并行流可以利用多核cpu进行并行计算,大大提高计算效率(但需要较大的cpu资源)
2、Stream流式思想概述
注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
jdk8新特性---------Lambda、Stream操作
文章图片
jdk8新特性---------Lambda、Stream操作
文章图片
那么既然把stream比喻做加工厂的生产流水线,那么我们完成加工一个产品需要什么步骤?
(1)原材料:需要获取到Stream流的源
(2)中间加工步骤:对stream流的操作加工过程
(3)加工完成:即对于stream流加工的最后一步(该步骤做完则会对流进行关闭)
那么下面对于这三个步骤进行详细分析:
3、原材料
该步骤是将一些集合、数组转换为stream流,主要有两种方式:
(1)对于集合(属于Collection接口:set、list等)(注意map不属于Collection):一般使用Collection的默认方法stream()获取流。举例:
List list = new ArrayList<>(); //获取流Stream stream1 = list.stream(); Set set = new HashSet<>(); //获取流Stream stream2 = set.stream(); Vector vector = new Vector<>(); // ...Stream stream3 = vector.stream();

对于map:java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:
Map map = new HashMap<>(); // 1、可以分别获取key、value的流 Stream keyStream = map.keySet().stream(); Stream valueStream = map.values().stream(); // 2、也可以key+value一起,Stream流中的元素是以map的形式存在 Stream entryStream = map.entrySet().stream();

(2) : Stream中的静态方法of获取流,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:
// Stream中的静态方法: static Stream of(T... values) Stream stream6 = Stream.of("aa", "bb", "cc"); String[] arr = {"aa", "bb", "cc"}; Stream stream7 = Stream.of(arr); Integer[] arr2 = {11, 22, 33}; Stream stream8 = Stream.of(arr2); // 注意:基本数据类型的数组不行 int[] arr3 = {11, 22, 33}; Stream stream9 = Stream.of(arr3);

注意:基本类型要转换为包装类,Stream包其实有为基本数据类型提供特殊的stream流(后面讲),其中: of 方法的参数其实是一个可变参数,所以支持数组。
总结:
1. 通过Collection接口中的默认方法Stream stream()
2. 通过Stream接口中的静态of方法
4、中间加工步骤
在对于流的加工处理中,存在两种操作,一种叫中间操作,一种叫终结操作。而中间加工步骤则是进行一个/多个中间方法的调用。所以该步骤实质就是调用一/多个中间方法:(注意:终结操作不执行,则中间操作不执行。)
主要的中间操作方法:(一般返回值为Stream的则为中间操作方法)
  1. distinct:去重
  2. filter:过滤
  3. map:映射(int映射成char)
  4. limit: 截取前几个元素
  5. sorted:按照自然排序方式进行排序
  6. skip:返回丢弃了前n个元素的流
  7. concat:对多个流进行合并
  8. mapToInt()、mapToLong() 以及mapToDouble:将流的元素包装类型转换为基本类型
1、distinct
Stream.of(22, 33, 22, 11, 33) .distinct() .forEach(System.out::println); //结果:22、33、11

自定义类型是根据对象的hashCode和equals来去除重复元素的
Stream.of( new Person("刘德华", 58), new Person("张学友", 56), new Person("张学友", 56), new Person("黎明", 52)) .distinct() .forEach(System.out::println);

2、filter(返回筛选完的流)
List one = new ArrayList<>(); Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); one.stream().filter(s -> s.length() == 2).forEach(System.out::println); //过滤出来长度为2的名字结果:老子 庄子 孙子 对应的stream流

3、map:
map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。
下面的代码中将字符元素映射成它的哈希码(ASCII值)。
List l = Stream.of('a','b','c') .map( c -> c.hashCode()) .collect(Collectors.toList()); System.out.println(l); //[97, 98, 99]

4、limit:limit 方法可以对流进行截取,只取用前n个。
对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。
List one = new ArrayList<>(); Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); one.stream().limit(3).forEach(System.out::println); //结果:"迪丽热巴", "宋远桥", "苏星河"

5、sorted:如果需要将数据排序,可以使用 sorted 方法
Stream sorted();
Stream sorted(Comparator comparator);
sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator comparator)可以指定排序的方式。
对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。
// sorted(): 根据元素的自然顺序排序 // sorted(Comparator comparator): 根据比较器指定的规则排序 Stream.of(33, 22, 11, 55) .sorted() .sorted((o1, o2) -> o2 - o1)//自定义 .forEach(System.out::println);

6、skip:返回丢弃了前n个元素的流
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

List one = new ArrayList<>(); Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); one.stream().skip(2).forEach(System.out::println)

7、concat:多个流合并成一个流
Stream.concat(streamA, streamB);
8、mapToInt()、mapToLong() 以及mapToDouble:将流的元素包装类型转换为基本类型
// Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱 Stream stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5}); // 把大于3的和打印出来 // Integer result = stream //.filter(i -> i.intValue() > 3) //.reduce(0, Integer::sum); // System.out.println(result); // 先将流中的Integer数据转成int,后续都是操作int类型 IntStream intStream = stream.mapToInt(Integer::intValue); int reduce = intStream .filter(i -> i > 3) .reduce(0, Integer::sum); System.out.println(reduce); // 将IntStream转化为Stream IntStream intStream1 = IntStream.rangeClosed(1, 10); Stream boxed = intStream1.boxed(); boxed.forEach(s -> System.out.println(s.getClass() + ", " + s));

注意:中间方法调用完后生成返回新的流,原先的流则不保存。所以流一般也说是只操作一次。可以调用流的方法再创建出一个流对象,再对新创出来的流对象进行操作。
5、加工完成
这一步是流操作的最后一步,该步走完就会关闭流。该步骤主要是对流的终结操作
终结方法:
  1. Match:来检查流中的元素是否满足条件
  2. count:计算元素个数
  3. collect:将流的元素存为某种类型的容器(String、list、set等)
  4. find:返回某个元素
  5. forEach、forEachOrdered:遍历集合
  6. max、min:返回最大、最小值
  7. reduce:将所有数据归纳得到一个数据(常用)
  8. toArray():流中的元素放入到一个数组中
写的太累了,这里就解释几个就好
Match:有三个实现方法,可以得到匹配的值
boolean b = Stream.of(5, 3, 6, 1) // .allMatch(e -> e > 0); // allMatch: 元素是否全部满足条件 // .anyMatch(e -> e > 5); // anyMatch: 元素是否任意有一个满足条件 .noneMatch(e -> e < 0); // noneMatch: 元素是否全部不满足条件 System.out.println("b = " + b); 结果返回布尔值

collect:将流的元素存为某种类型的容器(可以配合collections工具类的toList、groupingBy、toSet等方法)
List output = wordList.stream(). map(String::toUpperCase).collect(Collectors.toList()); //先将流的字母改为大写,再存到list集合中

find:findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
findFirst()返回第一个元素,如果流为空,返回空的Optional。
forEach:遍历
List one = new ArrayList<>(); Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); /*one.stream().forEach((String s) -> {System.out.println(s); }); */ // 简写 // one.stream().forEach(s -> System.out.println(s)); one.stream().forEach(System.out::println);

reduce:
Integer total2 = Stream.of(1,2,3,4,5).reduce(0, (x, y) -> x +y);
其中:先将0赋给x;然后将流元素赋给y;计算后的结果赋给下一次的x。
最后得到的是x=1+2+3+4+5=15
由于x+y的表达式可以变化为其他类型,所以该方法有很多应用
6、上面讲了一些方法,下面讲下stream流的两种形式:串行(单线程)、并行(多线程)
1、获取并行流:两种方式:
直接获取并行流: parallelStream()
将串行流转成并行流: parallel()
parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。
2、效率对比
3、parallelStream的安全问题
该流的操作是存在安全问题的,可以通过加锁、使用安全的容器、使用toArrary()/collect()方法
4、parallelStream背后的技术:fork/join
fork/join框架:1.7提出,使用分治的思想,大任务拆成多个小任务异步执行,最后join执行。
Fork/Join原理-工作窃取算法
Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的 cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念 Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖 的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来 执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就 去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争, 比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我 们使用了ForkJoinPool的ParallelStream。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置 系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N (N为线程数量),来调整ForkJoinPool的线 程数量,可以尝试调整成不同的参数来观察每次的输出结果。

小结
1. parallelStream是线程不安全的
2. parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,假如本身电脑CPU的负载很大,那还到处用 并行流,那并不能起到作用
3. I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集 型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多
4. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证 其中的顺序

【jdk8新特性---------Lambda、Stream操作】7、Optional类
8、新的日期和时间 API
9、重复注解与类型注解
累了,其他以后再写
可以参考:https://colobu.com/2016/03/02/Java-Stream/#collect
https://juejin.im/post/6844903830254010381
https://www.exception.site/java8/java8-new-features

    推荐阅读