Java 8 知识归纳(一)—— 流 与 Lambda

2020-03-20  本文已影响0人  大棋17

一、Java8 的三个编程概念

二、流简介

       Stream APICollection API的行为差不多,但Collection API主要为了访问和存储数据,而Stream API主要用于描述对数据的计算

       经典的Java程序只能利用单核进行计算,流提供了多核处理数据的能力。但前提是传递给Stream API的方法不会互动(即有可变的共享对象)时,才能多核工作。

三、Lambda

Lambda表达式由 参数列表箭头主体 组成:

微信截图_20200204105243.png

四、函数式接口

函数式接口指只定义一个抽象方法的接口

       注:哪怕有再多默认方法,只要接口中只定义了一个抽象方法,它仍然是函数式接口。

       Lambda允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把其作为函数式接口的实例。

FunctionalInterface注解

       @FunctionalInterface用于表示该接口为函数式接口。如果它不是函数式接口的话,编译器将返回一个提示原因的错误。

       注:@FunctionalInterface不是必需的,但最好为函数式接口都标注@FunctionalInterface.

函数描述符

       函数式接口的抽象方法的基本签名 本质上就是 Lambda表达式的签名。Java8将这种抽象方法叫作函数描述符

       Runnable接口的run方法即不接受任何参数也不返回,其函数描述符为:() -> void。 该函数描述符代表了函数列表为空且返回void的函数。

       ScalaKotlin等语言在其类型系统中提供 显式的类型注释 来描述函数的类型(即函数类型)

函数接口 函数描述符 基本类型特化
Predicate<T> T -> boolean IntPredicate LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

五、方法引用

方法引用可以把现有方法像Lambda一样传递。

方法引用主要分三类:

注:构造函数、数组构造函数以及父类调用的方法引用形式比较特殊:

利用 类名 和 关键字 new 来生成构造方法的方法引用。

六、流

从支持数据处理操作的源生成的元素序列 —— 流

流允许以声明性方式处理数据集合。还可以透明地并行处理,无须写任何多线程代码。

注:

筛选

流的切片

//输出结果为:[1, 2, 3, 3]
//在初始列表中的数据已排序的情况下:
List<Integer> numbers = Arrays.asList(1,2,3,3,4,4,5,6);
numbers.stream()
    //当发现第一个 i < 4 为 false 的元素时,则停止处理
    .takeWhile(i -> i < 4)
    .collect(Collectors.toList());
//输出结果:[4, 4, 5, 6]
//在初始列表中的数据已排序(由高到低)的情况下:
List<Integer> numbers = Arrays.asList(1,2,3,3,4,4,5,6);
numbers.stream()
    //当发现第一个i < 4 为 true 的元素时,则停止处理,并返回所有剩余的元素。
    .dropWhile(i -> i < 4)
    .collect(Collectors.toList());
//输出结果:[1, 3]
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
numbers.stream()
    //筛选只小于4的元素
    .filter(i -> i < 4)
    //只返回前两个值
    .limit(2)
    .collect(Collectors.toList());
//输出结果:[3, 0]
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
numbers.stream()
    //筛选只小于4的元素
    .filter(i -> i < 4)
    //跳过第一个值
    .skip(2)
    .collect(Collectors.toList());

映射

//输出结果:[6, 2, 4, 1]
List<String> languages = Arrays.asList("Kotlin","Go","Java","C");
languages.stream()
    //将 字符串 转为 int 
    .map(String::length)
    .collect(Collectors.toList());
//输出结果:[K, o, t, l, i, n, G, J, a, v, C]
List<String> languages = Arrays.asList("Kotlin","Go","Java","C");
languages.stream()
    .map(str -> str.split(""))
    //Arrays::stream 将 str.split("") 返回的字符数组转换为流,再由 flatMap 统一将这些流合并成一个流.最终:Stream<String[]> 转换为 Stream<String>,
    //flatMap 本质也是对流的元素进行转换(map也是对流的元素进行转换)。将流的元素转换为新的流,再将其整合进一个流中。
    .flatMap(Arrays::stream) // 等价于:flatMap(strArray -> Arrays.stream(strArray))
    .distinct()
    .collect(Collectors.toList());

练习:

1、返回所有对数

给定列表[ 1,2,3 ]和 列表[ 3, 4 ],返回[ (1,3) , (1,4) , (2,3) , (2,4) , (3,3) , (3,4) ]

//输出结果:[ (1,3) , (1,4) , (2,3) , (2,4) , (3,3) , (3,4) ]
List<Integer> numbers1 = Arrays.asList(1,2,3);
List<Integer> numbers2 = Arrays.asList(3,4);
List<int[]> pairs = 
    numbers1.stream()
        //将其扁平化为一个流
        .flatMap(i ->
                numbers2.stream()
                    //将其转换为一个数组,并返回这个流
                    .map(j -> new int[]{i,j})
        ).collect(Collectors.toList());

查找与匹配

//输出结果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().anyMatch(i -> i > 3);
//输出结果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().allMatch(i -> i < 10);
//输出结果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().noneMatch(i -> i > 10);
List<Apple> inventory = Arrays.asList(
    new Apple(80,"green"),
    new Apple(155, "green"),
    new Apple(120, "red"));
Optional<Apple> apple = inventory.stream()
    .filter(a -> a.getColor().equals("green"))
    .findAny();
List<Apple> inventory = Arrays.asList(
    new Apple(80,"green"),
    new Apple(155, "green"),
    new Apple(120, "red"));
Optional<Apple> apple = inventory.stream()
    .filter(a -> a.getColor().equals("green"))
    .findFirst();

注:

      1、anyMatchallMatchnoneMatch 都属于终端操作。

       2、anyMatchallMatchnoneMatchfindFirstfindAny 不用处理整,只要找到一个元素,就可以得到结果了。

       3、findAnyfindFirst 同时存在的原因是 并行findAny在并行流中限制较少。

归约

将流中所有元素反复结合起来,从而得到一个值的查询,可以被归类为归约操作。(用函数式编程语言的术语来说,这称为折叠)

reduce方法:接收的Lambda将列表中的所有元素进行处理并归约成一个新值。

有初始值

接收一个初始值 和 一个BinaryOperator<T>将两个元素结合起来产生一个新值。

T reduce(T identity, BinaryOperator<T> accumulator);

无初始值

一个BinaryOperator<T>将两个元素结合起来产生一个新值。

Optional<T> reduce(BinaryOperator<T> accumulator);
//输出值:36
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用带初始值的reduce方法
int sum = numbers.stream()
    .reduce(0,Integer::sum);//等价于 reduce(0,(a,b) -> a + b)
//或使用无初始值的reduce方法
Optional<Integer> sumOptional = numbers.stream().reduce(Integer::sum);
Optional<Integer> maxOptional = numbers.stream().reduce(Integer::max);
Optional<Integer> minOptional = numbers.stream().reduce(Integer::min);

数值流

       原先的归约求和代码中,Integet::sum暗含装箱和拆箱的成本。Stream API提供了原始类型流特化,专门支持处理数值流的方法。Java8引入原始类型特化接口解决数值流拆箱与装箱的问题:IntStreamDoubleStreamLongStream,分别将流中的元素特化为 intlongdouble

mapToIntmapToDoublemapToLong用于将流转换为特化流:

//输出值:36
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();

当需要把原始流转换成一般流时(如:把 int 装箱回 Integer ),可以使用 boxed

List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用 IntStrean 特化流
IntStream intStream = numbers.stream()
    .mapToInt(Integer::intValue);
Stream<Integer> stream = intStream.boxed();

Optional也相应的提供原始类型特化版本:OptionalIntOptionalLongOptionalDouble

List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用 OptionalInt 特化Optional
OptionalInt maxNumber = numbers.stream()
    .mapToInt(Integer::intValue)
    .max();

数值范围

IntStreamLongStream提供产生生成数值范围的静态方法:rangerangeClosed

range方法生成半闭区间(左闭右开),rangeClosed方法生成闭区间。

IntStream.range(1,100)
    .filter(n -> n % 2 == 0)
    .count();

构建流

静态方法 Stream.of 接受任意数量的参数,显式创建一个流。

//显式创建字符串流
Stream<String> strStream =Stream.of("Java","Kotlin","Go");

静态方法Stream.empty创建一个空流。

Stream<String> strStream =Stream.empty();

静态方法Arrays.stream将数组创建为一个流。

int[] numbers = {2,3,5,6,7};
int sum = Arrays.stream(numbers).sum();

java.nio.file.Files中很多静态方法会返回一个流,以便利用Stream API处理文件等I/O操作。

如:Files.lines返回一个由指定文件中的各行构成的字符串流:

long uniqueWords = 0;
//流会自动关闭,不需要额外try-finally操作
try(Stream<String> lines = 
    Files.lines(Paths.get("data.text"), Charset.defaultCharset())){
    //统计有多少不重复的单词。
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
        .distinct()
        .count();
}catch (IOException e){}

Stream API提供了两个静态方法来从函数生成流:Stream.iterate()String.generate()

不同于从集合创建的流,这两个静态方法创建的流没有固定大小,称为无限流

迭代:

iterate方法接收一个接受一个初始值作为流的第一个元素。再接收一个Lambda依次应用在每一个产生的新值上。

Stream.iterate(0,n -> n + 2)
    .limit(10)
    .forEach(System.out::println);

Java 9iterate方法进行增加,接受多一个谓词作为判断迭代调用何时终止。(谓词作为第二参数传入)

IntStream.iterate(0,n -> n < 100,n -> n + 2)
    .forEach(System.out::println);

当然,也可以使用takeWhile对流执行短路操作(takeWhile函数Java9开始支持):

IntStream.iterate(0,n -> n + 2)
    .takeWhile(n -> n < 100)
    .forEach(System.out::println);

生成:

generate接受一个Supplier<T>类型的Lambda提供新值。

Stream.generate(Math::random)
    .limit(5)
    .forEach(System.out::println);

七、用流收集数据

流支持两种类型的操作:中间操作末端操作

归约和汇总

String nameStr = menu.stream().map(Dish::getName).collect(joining());

分组

CollectionsgroupingBy() 方法会把流中的元素分成不同的组。

微信截图_20200224172850.png

操作分组的元素

如果在 groupingBy() 之前,使用 filter() 对流进行过滤操作,可能会造成键的丢失。

例如:

       存在以下Map: { FISH = [ prawns, salmon], OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

但如果在使用filter() 后,再 groupingBy() 可能对某些键在结果映射中完全消失:

       { OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

为此,Collectors 类提供了 filtering() 静态工厂方法,它接受一个谓词对每一个分组中的元素执行过滤操作。最后不符合谓词条件的键将得到空的列表:

{ FISH = [], OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

Map<Dish.Type,List<Dish>> caloricDishesByType = menu.stream()
    .collect( groupingBy(Dish::getType),
            filtering(dish -> dish.getCalories() > 500,toList()))

       使用重载的 groupingBy() 方法 和 filtering()方法 :先分组再过滤;

       先使用 filter(),再使用 groupingBy() 方法:先过滤再分组。

Collectors 提供 mapping 静态工厂方法,接受一个映射函数和另外一个 Collectors 函数作为参数。映射函数将分组中的元素进行转换,作为参数的 Collectors 函数会收集对每个元素执行该映射函数的结果。

Map<Dish.Type,List<String>> dishNamesByType = menu.stream().collect(
    groupingBy(
        Dish::getType,
        mapping(
            //将元素转换为其名字
            Dish::getName,
            //用于收集该组进行完映射的元素
            Collectors.toList()
        )
    )
)

Collectors 工具类也提供了 flatMapping,跟 flatMap 类似的功能。

多级分组

同时Collectors 工具类也提供了可以嵌套分组的groupingBy(),用于进行多级分组

注:

       可以理解为在进行完第一次分组后,再对每一组元素进行再次分组。

       groupingBy(f)( f 是分类函数 ) 实际上是groupingBy(f,toList())的简便写法。

Map<Dish.Type,Map<CaloricLevel,List<Dish>>> dishesByTypeCaloricLevel = 
    menu.stream().collect(
        groupingBy(
            Dish::getType,
            groupingBy(dish -> {
                if(dish.getCalories() <= 400)
                    return CaloricLevel.DIET;
                else if(dish.getCalories() <= 700)
                    return CaloricLevel.NORMAL;
                else 
                    return CaloricLevel.FAT;
            })
        )
    );
微信截图_20200226150753.png

按子组收集数据

groupingBy()的第二个收集器可以是任何类型。例如可以使用 counting() 收集器作为它的第二个参数,统计分组的数量:

Map<Dish.TYPE,Long> typesCount = menu.stream().collect(
    groupingBy(Dish::getType,counting())
);

得到以下的map: { MEAT = 3 , FISH = 2 , OTHER = 4 }

Map<Dish.Type,Dish> mostCaloricByType = 
    menu.stream().collect(
        groupingBy(Dish::getTpye,
                   collectingAndThen(
                        //maxBy返回的是Optional类型对象
                        maxBy(comparingInt(Dish::getCalories)),
                       //当找到最大值后,会执行get操作。
                        Optional::get
                   )
        )
    );

如果 menu 中没有某一类型的Dish,该类型不会对应一个 Optional.empty() 值,而且根本不会在Map的键中。所以转换函数Optional::get的操作是安全的。

分区

Collectors 工具类提供 partitionedMenu() 静态工厂函数来实现分区,分区是分组的特殊情况。由谓词作为分类函数,这意味着得到的分组 Map 的键类型是 Boolean ,最多分为 truefalse 两组。

//将得到以下结果:
Map<Boolean,List<Dish>> partitionedMenu = 
    menu.stream().collect(
        //分区函数
        partitioningBy(
            //分期的标准
            Dish::isVegetarian
        )
    )

同时partitionedMenu()也和groupingBy()类似,可以进行二级分区。

收集器接口

public interface Collector<T, A, R> {
    //创建一个空的累加器
    Supplier<A> supplier();
    //将元素添加到结果容器
    BiConsumer<A, T> accumulator();
    //合并两个结果(定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器如何合并)
    BinaryOperator<A> combiner();
    //对结果容器应用最终转换
    Function<A, R> finisher();
    //定义收集器的行为
    Set<Characteristics> characteristics();
}

泛型的定义如下:

       T 表示流中要手机的项目的泛型。

       A 表示累加器的类型。(累加器是收集过程中用于累积部分结果的对象)

       R 表示收集操作得到的对象的类型。

ToListCollector为例

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    public ToListCollector() {}
    
    //创建ArrayList对象作为累加器
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }
    
    //利用add函数将流中的元素添加到列表中
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }
    
    //两个累加器(即两个ArrayList对象)进行相加
    public BinaryOperator<List<T>> combiner() {
        return (list, list2) -> {
            list.addAll(list2);
            return list;
        };
    }
    
    //累加器进行最终的转换
    public Function<List<T>, List<T>> finisher() {
        //Function.identity()表示给什么返回什么,也就是不进行转换
        //恒等
        return Function.identity();
    }

    //定义收集器的行为
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}

Characteristics的三个枚举:

进行自定义收集,而不去实现 Collector

对于 IDENTITY_FINISH 的收集操作,Stream重载的 collect 方法接受三个函数——supplieraccumulatorcombiner。该 collect 方法创建的收集器的 Characteristics 永远是Characteristics.IDENTITY_FINISHCharacteristics.CONCURRENT

List<Dish> dishes = menu.stream().collect(
    //创建累加容器
    ArrayList::new,
    //将流元素添加到累加容器中
    List::add,
    //合并累加容器
    List::addAll
);

八、并行数据处理与性能

使用并行流时,考虑以下因素

流的数据源和可分解性:

可分解性
ArrayList
LinkedList
IntStream.range 极佳
Stream.iterate
HashSet
TreeSet

九、Collection API的增强功能

Arrays.asList() 创建一个固定大小的列表,列表的元素可以更新,但不可以增加或删除。

Java 9 引入以下工厂方法:

重载与变参

Java API中,List.of包含多个重载版本:

static <E> List<E> of(E e1);
static <E> List<E> of(E e1, E e2);

而不提供变参版本是因为需要额外的分配一个数组,这个数组被封装于列表中。使用变参版本的方法,就要负担分配数组、初始化以及最后进行垃圾回收的开销。(如果元素数量超过10个,实际调用的还是变参方法。)、

使用 ListSetMap

十、 重构

改善代码可读性

参考资料

Java实战(第2版)

Java8系列

Java 8 知识归纳(一)—— 流 与 Lambda

Java 8 知识归纳(二)—— Optional

Java 8 知识归纳(三)—— 日期API

上一篇下一篇

猜你喜欢

热点阅读