前言,这篇文章2013年底就写了,吃惊于scala的语法的简洁,同为JVM语言,JAVA 对于数据处理上在语法方面是否有提升空间,将if else for替换为lambda语法,是过程式编程向函数式组合的一种转变,一个是深入基层事无巨细,做大量判断和边界处理,一个是依托成熟的工具,将业务拆分为流水线,一层层处理,节省精力。
最后有2到OD考试题,难度就是leetcode简单偏中等。有基本算法思想都能解决,但是考试的时候时间很紧张,用lamda和stream的方式去完成更好一些

1 lambda

1.1 定义

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。
可以这么理解,lambda是Functional的匿名类,Functional类可以是JAVA标准包的,也可以是自定义的,例如:

//标准包Comparator
@FunctionalInterface  
public interface Comparator<T> {
	int compare(T o1, T o2);
}

// 它就是一个Comparator实例
(Apple a1, Apple a2) -> {
	// lambda 假设有个Apple类,有getWeight方法
	return a1.getWeight > a2.getWeight ? 1 : 0
}

1.2 Java 8中的常用函数式接口

函数接口函数描述java.util.functions
Predicate<T>T->booleanIntPredicate
LongPredicate
DoublePredicate
Consumer<T>T->voidIntConsumer
LongConsumer
DoubleConsumer
Function<T,R>T->RIntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
Supplier<T>()->TBooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier
UnaryOperator<T>T->TIntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator<T>(T,T)->TIntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
BiPredicate<L,R>(L,R)->boolean
BiConsumer<T,U>(T,U)->voidObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
BiFunction<T,U,R>(T,U)->RToIntBiFunction<T,U>
ToLongBiFunction<T,U>
ToDoubleBiFunction<T,U>
使用案例Lambda的例子对应接口
布尔表达式
List<String> list) -> list.isEmpty()
Predicate<List<String>>
创建对象
() -> new Apple(10)
Supplier<Apple>
消费对象
(Apple a) -> System.out.println(a.getWeight())
Consumer<Apple>
从一个对象中选择/抽取
(String s) -> s.length()
Function<String, Integer>
组合两个值
(int a, int b) -> a * b
IntBinaryOperator
比较两个对象
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
Comparator<Apple>

1.3 方法引用(lambda的更简写)

方法引用,其形式为目标引用 :: 方法名称,可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

1.3.1 类型:

方法引用主要有三类:

  1. 指向静态方法的方法引用,例如Integer的parseInt方法,写作Integer::parseInt。
  2. 指向任意类型实例方法的方法引用,例如String的length方法,写作String::length,就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如Lambda表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase
  3. 指向现有对象的实例方法的方法引用,假设你有一个对象变量obj它支持实例方法getValue,那么你就可以写obj::getValue。例如()->obj.getValue()可以写作obj::getValue
Lambda等效方法
(Apple a) -> a.getWeight()Apple::getWeight
() -> Thread.currentThread().dumpStack()Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)String::substring
(String s) -> System.out.println(s)System.out::println

1.3.2 图示

方法引用类型规则可如下图表示:
在这里插入图片描述

1.4 构造函数引用

作用就是构造多一个写法

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似,举例如下:

  1. 构造参数签名() -> Apple:
// 指向默认的Apple()构造函数
Supplier<Apple> c1 = Apple::new; 
// 调用Supplier的get方法将产生一个新的Apple
Apple a1 = c1.get(); 

// -> 等价写法
Supplier<Apple> c1 = () -> new Apple(); 
Apple a1 = c1.get();
  1. 构造参数签名(Integer weight) -> Apple:
// 指向Apple(Integer weight)的构造函数
Function<Integer, Apple> c2 = Apple::new;
// 调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
Apple a2 = c2.apply(110); 

// -> 等价写法
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
  1. 具有两个参数的构造函数Apple(String color, Integer weight):
BiFunction<Integer, String, Apple> c3 = Apple::new
Apple c3 = c3.apply(110, "brand1");

// -> 等价写法
BiFunction<Integer, String, Apple> c3 =
(weight, brand) -> new Apple(color, brand); 
Apple c3 = c3.apply(110, "brand1"); 

1.5 复合Lambda表达式

在实践中可以把多个简单的Lambda复合成复杂的表达式。

1.5.1 比较器复合

解算法题中,在整个算法中,经常会有排序的步骤,如果能有很方便的方法进行排序,那解题过程会省下很多时间。

ArrayList<Apple> inventory = new ArrayList<>();  
  
inventory.sort(  
        Comparator.comparing(Apple::getWeight)  // 首先按照重量排序
                .reversed()  // 逆序
                .thenComparing(Apple::getBrand)  // 再按照品牌排序
);

1.5.2 谓词复合

谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂谓词。比如要满足(a || b) && c,假定要选取一个重量在500以上,并且颜色是红色或者品牌是麥芒的苹果。可以这么写:

Predicate<Apple> over500 = a -> a.weight > 500;  
Predicate<Apple> isMaimang = a -> a.brand.equals("Maimang");  
Predicate<Apple> isRed = a -> a.color.equals("red");  

// a -> (a.weight > 500) && ( a.brand.equals("Maimang") || a.color.equals("red"))  
over500.and(isMaimang.or(isRed));

1.5.3 函数复合

把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

f.andThen(g) = g(f(x))
f.compose(g) = f(g(x))

2 流

2.1 定义

Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里),它可以看成是遍历数据集的高级迭代器。这里的遍历不像Collection接口那样需要用户去做外部迭代(比如用for-each),流的遍历可以认为是内部实现的内部迭代,无需用户额外编写。

流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了,你可以从原始数据源那里再获得一个新的流来重新遍历一遍,试图遍历已经消费过的流会产生异常。

它的特征如下:

  1. 源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。
  2. 数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  3. 元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。

重要特点:

  1. 流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  2. 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
    ![[Pasted image 20231015162508.png]]

总而言之,流的使用一般包括三件事:

  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
    ![[Pasted image 20231015163012.png]]
  3. 一个终端操作,执行流水线,并能生成结果
    ![[Pasted image 20231015163023.png]]

2.2 构建流

  1. 由值创建流:Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
  2. 由数组创建流:Arrays.stream(new int[]{1,2,3,4,5}).sum();
  3. 由函数生成流:

iterate方法:接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda。

Stream.iterate(0, n -> n + 2)
	.limit(10)
	.forEach(System.out::println);
	
// 创建斐波纳契序列
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0]+t[1]})  
        .limit(20)  
        .reduce((t1, t2) -> {  
            int[] dest = new int[t1.length + 1];  
            System.arraycopy(t1, 0, dest, 0, t1.length);  
            dest[t1.length] = t2[1];  
            return dest;  
        }).ifPresent(t -> System.out.println(Arrays.toString(t)));

generate方法:generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。

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

// 创建斐波纳契序列
IntSupplier fib = new IntSupplier(){  
    private int previous = 0;  
    private int current = 1;  
    public int getAsInt(){  
        int oldPrevious = this.previous;  
        int nextValue = this.previous + this.current;  
        this.previous = this.current;  
        this.current = nextValue;  
        return oldPrevious;  
    }  
};  
IntStream.generate(fib)  
        .limit(20)  
        .forEach(a -> System.out.print(a + ", "));

其他:

// stream()方法
List<String> list_a = Arrays.asList("a", "b");
Stream<String> stream_a = list_a.stream();

2.3 使用流(重点)

2.3.1.1 中间操作和终端操作总结

中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,例如返回流中的最大元素。

2.3.2 筛选、切片和匹配

注意,Stream中,没有Stream<int>这种写法,对int的Stream操作有IntStream的类。

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .collect(Collectors.toList());
Stream<Integer> s1 = List.of(0, 2, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .distinct()  
        .collect(Collectors.toList());
Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .limit(2)  
        .collect(Collectors.toList());
Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .skip(2)  
        .collect(Collectors.toList());
tream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .map(a -> a + 1)  
        .collect(Collectors.toList());

flatMap暂时还是不很理解,存疑

2.3.3 查找、匹配和归约

2.3.3.1 查找

Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了一个终端操作,看看数据集中的某些元素是否匹配一个给定的属性,它们返回一个boolean或者一个Optional对象。

  1. anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。
  2. allMatch方法可以回答“流中的元素是否都能匹配给定的谓词”。
  3. noneMatch方法可以回答“流中没有任何元素与给定的谓词匹配”。
  4. findAny方法返回当前流中的任意元素。
  5. findFirst和findAny类似,只不过是返回第一个元素。

同时有findFirst和findAny的原因是findFirst并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

2.3.3.1.1 Optional介绍

Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。

  • isPresent()将在Optional包含值的时候返回true, 否则返回false。
  • ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码,一个Consumer函数式接口,可以用Lambda表达式。
  • T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
  • T orElse(T other)会在值存在时返回值,否则返回一个默认值。
Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .map(a -> a + 1)  
        .findFirst()  
        .ifPresent(t -> System.out.println(t + " present"));
2.3.3.2 归约

归约即把流中的内容组合起来。

求和

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .map(a -> a + 1)  
        .reduce(0, (a, b) -> a + b); // 或者reduce(0, Integer::sum);

求最大值

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .map(a -> a + 1)  
        .reduce(Integer::max);

2.3.4 使用数值范围等数值流

2.3.4.1 特化流

有的归约操作可能暗含拆箱装箱工作,下面代码有一个暗含的装箱成本,每个Integer都必须拆箱成一个原始类型,再进行求和。

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .map(a -> a + 1)  
        .reduce(0, Integer::sum);

Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因在于装箱造成的复杂性,即类似int和Integer之间的效率差异。

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法返回一个特化流,而不是Stream<T>

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .mapToInt(a -> a)  
        .max()  
        .orElse(1);

Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本OptionalInt、OptionalDouble和OptionalLong。

max()返回的是一个OptionalInt,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了。

box可以将特化流转为非特化流。

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
s1.filter(n -> n % 2 == 0)  
        .mapToInt(a -> a)  
        .boxed();
2.3.4.2 数值范围

Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值,但range是不包含结束值的,而rangeClosed则包含结束值。

Stream<Integer> s1 = IntStream.range(0, 6).boxed();

2.3.5 用流收集数据

对于某些自定义的类集合,可以封装成流调用collect方法素触发一个归约操作,例如上述例子collect(Collectors.toList()),collect方法的参数是Collector接口的一个实现,也就是给Stream中元素做汇总的方法。

Collectors类提供了创建预定义收集器工厂方法。它们主要提供了三大功能:

  1. 将流元素归约和汇总为一个值。
  2. 元素分组。
  3. 元素分区。
2.3.5.1 用Collectors类创建和使用收集器
2.3.5.2 归约和汇总

计数:

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();  
long count = s1.collect(Collectors.counting());

查找最大值和最小值:
Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素。

Function<Integer, Apple> appleC = Apple::new;
Stream<Apple> appleStream = List.of(  
        appleC.apply(1),   
        appleC.apply(2),   
        appleC.apply(3)  
).stream();  
  
Optional<Apple> max = appleStream.collect(  
        Collectors.maxBy(  
                Comparator.comparing(Apple::getWeight)  
        )  
);  
System.out.println(max.orElseGet(() -> appleC.apply(0)));

汇总:

Function<Integer, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1),  
        appleC.apply(2),  
        appleC.apply(3)  
).stream();  
  
Integer totalWeight = appleStream.collect(  
        Collectors.summingInt(  
                Apple::getWeight  
        )
);  
System.out.println(totalWeight); // 6

统计:
summarizing操作可以就数出元素的个数,并得到总和、平均值、最大值和最小值。

Function<Integer, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1),  
        appleC.apply(2),  
        appleC.apply(3)  
).stream();  
  
IntSummaryStatistics summary = appleStream.collect(  
        Collectors.summarizingInt(  
                Apple::getWeight  
        )  
);  
System.out.println(summary); // IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}

连接字符串:
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串

BiFunction<Integer, String, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1, "brand1"),  
        appleC.apply(2, "brand2"),  
        appleC.apply(3, "brand3")  
).stream();  
  
String allBrands = appleStream  
        .map(Apple::getBrand)  
        .collect(Collectors.joining(", "));  
        
System.out.println(allBrands); // brand1, brand2, brand3
2.3.5.3 归约一般化

之前讨论的所有收集器,都可以用Collectors.reducing工厂方法一般化,上述只是高频方法,如果有更加特殊的方法,需要调用工厂方法自由编写。

它有三个参数:

  1. 第一个参数是归约操作的起始值,也是流中没有元素时的返回值。
  2. 第二个参数是转换函数。
  3. 第三个参数是一个BinaryOperator,将转换后的值累积成一个同类型的值。

reducing工厂方法也可以接受单参数,它被看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点,所以单参数的reducing方法返回一个Optional<T>对象。

BiFunction<Integer, String, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1, "brand1"),  
        appleC.apply(2, "brand2"),  
        appleC.apply(3, "brand3")  
).stream();  
  
String allBrands = appleStream  
        .collect(Collectors.reducing("", Apple::getBrand, (str1, str2) -> str1 + "," + str2));  
System.out.println(allBrands);

Stream接口的collect和reduce方法有何不同,因为两种方法通常会获得相同的结果,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约,collect方法的设计就是要改变容器,从而累积要输出的结果。

reduce归约过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏容器本身。想要线程安全,就需要每次分配一个新的容器,因此collect方法适合表达可变容器上的归约的原因,因此它适合并行操作。

上述的计数,最大最小值,汇总,统计,字符串连接都可以用一般形式的reducing来表示。

long count = s1.collect(Collectors.reducing(0L, e -> 1L, Long::sum));

Optional<Apple> max = appleStream.collect(  
        Collectors.reducing((a1, a2) -> a1.weight > a2.weight ? a1 : a2)  
);

Integer totalWeight = appleStream  
        .collect(Collectors.reducing(0, Apple::getWeight, Integer::sum));

2.3.5.4 数据分组和分区
2.3.5.4.1 分组

一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组,Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成这项任务。

BiFunction<Integer, String, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1, "brand1"),  
        appleC.apply(2, "brand1"),  
        appleC.apply(3, "brand3")  
).stream();  
  
Map<String, List<Apple>> group = appleStream.collect(Collectors.groupingBy(Apple::getBrand));  
  
System.out.println(group);
/**
{
	brand3=[Apple{weight=3, brand='brand3'}], 
	brand1=[Apple{weight=1, brand='brand1'}, Apple{weight=2, brand='brand1'}]
}
*/

groupingBy方法传递了一个分类函数用来把流中的元素分成不同的组。分组操作的结果是一个Map,把分组函数返回的值作为映射
的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。

2.3.5.4.2 多级分组

多级分组由双参数版本的Collectors.groupingBy工厂方法创建收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数,要进行二级分组,需要把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。

2.3.5.4.2.1 一般分组
BiFunction<Integer, String, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1, "brand1"),  
        appleC.apply(3, "brand1"),  
        appleC.apply(1, "brand3"),  
        appleC.apply(3, "brand4")  
).stream();  
  
Map<String, Map<String, List<Apple>>> group = appleStream.collect(  
        Collectors.groupingBy(Apple::getBrand, // 一级分类  
                Collectors.groupingBy(apple -> apple.getWeight() > 2 ? "more than 2" : "less 2")  
        )  
);
/**
{
	brand4={
		more than 2=[Apple{weight=3, brand='brand4'}]
	}, 
	brand3={
		less 2=[Apple{weight=1, brand='brand3'}]
	}, 
	brand1={
		more than 2=[Apple{weight=3, brand='brand1'}], 
		less 2=[Apple{weight=1, brand='brand1'}]
	}
}
*/*

把groupingBy看作“桶”比较容易明白,第一个groupingBy给每个键建立了一个桶。然后再用下游的收集器去收集每个桶中的元素,以此得到 n 级分组。
在这里插入图片描述

2.3.5.4.2.2 按子组收集数据

传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy,可以通过比较器返回一个Optional对象,或者是一个Integer

与maxBy的结合:

Map<String, Optional<Apple>> group = appleStream.collect(  
        Collectors.groupingBy(Apple::getBrand, Collectors.maxBy(Comparator.comparingInt(Apple::getWeight)))  
);
/**
{
	brand4=Optional[Apple{weight=3, brand='brand4', color='null'}], 
	brand3=Optional[Apple{weight=1, brand='brand3', color='null'}], 
	brand1=Optional[Apple{weight=3, brand='brand1', color='null'}]
}
*/

与counting的结合:

Map<String, Long> group = appleStream.collect(  
        Collectors.groupingBy(Apple::getBrand, Collectors.counting())  
);
// {brand4=1, brand3=1, brand1=2}

与summingInt结合:

Map<String, Integer> group = appleStream.collect(  
        Collectors.groupingBy(  
                Apple::getBrand, Collectors.summingInt(Apple::getWeight)  
        )  
);
//{brand4=3, brand3=1, brand1=4}

与mapping结合:

Map<String, List<String>> group = appleStream.collect(  
        Collectors.groupingBy(  
                Apple::getBrand,  
                Collectors.mapping(  
                        apple -> apple.getWeight() > 2 ? "more than 2" : "less 2",  
                        Collectors.toList()  
                )  
        )  
);
// {brand4=[more than 2], brand3=[less 2], brand1=[less 2, more than 2]}
2.3.5.4.2.3 收集器的结果转换为另一种类型

使用Collectors.collectingAndThen工厂方法返回的收集器,这个工厂方法接受两个参数:要转换的收集器以及转换函数。collect操作的最后一步就是将返回值用转换函数做一个映射。

与maxBy结合:

Map<String, Apple> group = appleStream.collect(  
        Collectors.groupingBy(Apple::getBrand,  
                Collectors.collectingAndThen(  
                        Collectors.maxBy(Comparator.comparingInt(Apple::getWeight)),  
                        Optional::get  
                )  
        )  
);
/**
{
	brand4=Apple{weight=3, brand='brand4', color='null'}, 
	brand3=Apple{weight=1, brand='brand3', color='null'}, 
	brand1=Apple{weight=3, brand='brand1', color='null'}}
*/
2.3.5.4.3 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。

Stream<Integer> s1 = List.of(0, 1, 2, 3, 4, 5).stream();
Map<Boolean, List<Integer>> partion1 = s1.collect(  
        Collectors.partitioningBy(i -> i > 2)  
);
// {false=[0, 1, 2], true=[3, 4, 5]}

partitioningBy工厂方法有一个重载版本,可以传递第二个收集器产生一个二级Map:

BiFunction<Integer, String, Apple> appleC = Apple::new;  
        Stream<Apple> appleStream = List.of(  
                appleC.apply(1, "brand1"),  
                appleC.apply(1, "brand1"),  
                appleC.apply(1, "brand3"),  
                appleC.apply(3, "brand4")  
        ).stream();  
  
        Map<Boolean, Map<String, List<Apple>>> partionMap = appleStream.collect(  
                Collectors.partitioningBy(  
                        a -> a.weight > 2,  
                        Collectors.groupingBy(Apple::getBrand)  
                )  
        );  
/**
{
	false={
		brand3=[
			Apple{weight=1, brand='brand3', color='null'}
		], 
		brand1=[
			Apple{weight=1, brand='brand1', color='null'}, 
			Apple{weight=1, brand='brand1', color='null'}
		]
	}, 
	true={
		brand4=
		  Apple{weight=3, brand='brand4', color='null'}
		]
	}
}
**/
2.3.5.5 开发自己的自定义收集器

Collector接口:

public interface Collector<T, A, R> {
	Supplier<A> supplier();
	BiConsumer<A, T> accumulator();
	Function<A, R> finisher();
	BinaryOperator<A> combiner()Set<Characteristics> characteristics();
}
  1. 泛型定义:

    • T是流中要收集的项目的泛型。
    • A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
    • R是收集操作得到的对象(通常但并不一定是集合)的类型。
  2. 建立新的结果容器:supplier方法:

    • supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
  3. 将元素添加到结果容器:accumulator方法:

    • accumulator方法会返回执行归约操作的函数。当遍历到流中第 n 个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第 n 个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。
  4. 对结果容器应用最终转换:finisher方法:

    • 在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果,如果无需转换只需返回Function.identity函数
  5. 合并两个结果容器:combiner方法:

    • combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。
  6. characteristics方法:

    • characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为(是关于流是否可以并行归约),以及可以使用哪些优化的提示。
    • UNORDERED,表示归约结果不受流中项目的遍历和累积顺序的影响。
    • CONCURRENT,表示accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归。
    • IDENTITY_FINISH,这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。
      在这里插入图片描述

自定义toList收集器:

class ToListCollector<T> implements Collector<T, List<T>, List<T>> {  
    @Override  
    public Supplier<List<T>> supplier() {  
        return ArrayList::new; // 创建集合操作的起始点  
  
    }  
    @Override  
    public BiConsumer<List<T>, T> accumulator() {  
        return List::add; // 累积遍历过的项目,原位修改累加器  
    }  
    @Override  
    public Function<List<T>, List<T>> finisher() {  
        return Function.identity(); // 恒等函数  
    }  
    @Override  
    public BinaryOperator<List<T>> combiner() {  
        return (list1, list2) -> {  
            System.out.println("this is the parall"); // 并行操作才会执行  
            list1.addAll(list2); //修改第一个累加器, 将其与第二个累加器的内容合并  
            return list1; // 返回修改后的第一个累加器  
        };  
    }  
    @Override  
    public Set<Characteristics> characteristics() {  
        return Collections.unmodifiableSet(EnumSet.of(  
                IDENTITY_FINISH, CONCURRENT)  // 为收集器添加IDENTITY_FINISH和CONCURRENT标志  
        );  
    }  
}

调用自定义收集器:

BiFunction<Integer, String, Apple> appleC = Apple::new;  
Stream<Apple> appleStream = List.of(  
        appleC.apply(1, "brand1"),  
        appleC.apply(1, "brand1"),  
        appleC.apply(1, "brand3"),  
        appleC.apply(3, "brand4")  
).stream();  
  
List<Apple> result = appleStream.filter(a -> a.getWeight() < 3).collect(new ToListCollector<Apple>());

2.4 code snappit

2.4.1 有2个数组,长度相等,第一个数组作为key,第二个作为value

String[] keys   = {"a", "b", "c"};
Integer[] values = {1, 2, 3};

Map<String, Integer> map =
        IntStream.range(0, keys.length)          // 0 1 2
                 .boxed()
                 .collect(Collectors.toMap(
                         i -> keys[i],            // keyMapper
                         i -> values[i]));        // valueMapper

System.out.println(map);   // {a=1, b=2, c=3}

3 考试案例

熟练运用Lambda和Stream能提高解题速度

3.1 华为OD试题1

【题目描述】
小华是个很有对数字很敏感的小朋友,他觉得数字的不同排列方式有特殊美感。某天,小华突发奇想,如果数字多行排列,第一行1个数,第二行2个数,第三行3个,即第n行有n个数字,并且奇数行正排列,偶数行逆序排列,数字依次累加。这样排列的数字一定很有意思。聪明的你能编写代码帮助小华完成这个想法吗?
规则总结如下:
1.每个数字占据4个位置,不足四位用*补位
2.数字之间相邻4个空格
3.数字的打印顺序按照正序逆序交替打印,奇数行正序,偶数行逆序
4.最后一行数字顶格,第n-1行相对第n行缩进四个空格

【输入输出描述】
输入: 第一行输入位N,表示打印多少行;1<=N<=30,例如输入4
输出:
1***
3**** 2****
4**** 5**** 6****
10** 9*** 8**** 7****

3.2 华为OD试题2

【题目描述】
给定一个仅包含0和1的N*N的二维矩阵,请计算二维矩阵的最大值。
计算规则如下:
1.每行元素按下标顺序组成一个二进制数(下标越大越排在低位),二进制数的值就是该行的值。矩阵各行值之和为矩阵的值。
2.允许通过向左或向右整体循环移动每行元素来改变各元素在行中的位置。
比如:
[0,1,0,0,1]向右整体循环移动1位变为[1,0,1,0,0],二进制数为10100,值为20。
[1,1,0,1,1]向右整体循环移动2位变为[1,1,1,1,0],二进制数为11110,值为30。

【输入输出描述】
输入: 第一行输入一个正整数,记录N的大小。接下来的N行输入二维矩阵信息,例如输入:
3
0,1,1
1,0,1
0,0,1
输出: 矩阵的最大值是16,因为它左右移动后最大值情况为
1, 1, 0 --循环左移一位, 值为6
1, 1, 0 --循环右移一位, 值为6
1, 0, 0 --循环右移一位, 值为4

3.3 答案

3.3.1 答案1

3.3.1.1 思路

在这里插入图片描述

  1. 主要就是确定一行的初始值,可以找到通用公式,但是暴力递归最简单
  2. 基数行1,3,5,7 …数组数字反转
  3. 行数从0开始为了和数组对齐,因为算法中数据结构为数组
  4. 第n行的个数就是从每行的初始值,步长为1,加n次,总共有n+1个数字
3.3.1.2 代码
package kiramario.com.algorithm;  
  
import java.lang.reflect.Array;  
import java.util.Arrays;  
import java.util.Collections;  
import java.util.function.Function;  
import java.util.stream.IntStream;  
import java.util.stream.Stream;  
  
public class algorithm_1 {  
    // 数组生成  
    // 比较重要,并且递归没法用lambda,所以单独一个方法体  
    private static int[] F(Integer n) {  
        if (n == 0) {  
            return new int[]{1};  
        }  
  
        return IntStream.rangeClosed(0, n).map(i -> F(n - 1)[n - 1] + 1 + i).toArray();  
    }  
  
    public static void main(String[] args) {  
  
        // 1. 打印数字 打印1***这种  
        Function<Integer, String> numString =  
                (Integer num) -> String.valueOf(num).replaceAll("(.{0,4})", "$1****").substring(0,4);  
  
        // 2. 缩进, 如果总共3行,则第一行缩进(3-1)*4 个空格  
        Function<Integer, String> indent =  
                (Integer n) -> String.join("", Collections.nCopies(n - 1, "    "));  
  
        // 3. 数组反转  
        Function<int[], int[]> reverseArray = a -> IntStream.range(0, a.length)  
                .map(i -> a[a.length - 1 - i])  
                .toArray();  
          
        // 一般是从控制台读取  
        int n = 10;  
        for(int i = 0; i < n; i++) {  
            int[] nums = i % 2 == 0 ? F(i) : reverseArray.apply(F(i));  
            String numStr =  
                    Arrays.stream(nums)  
                            .mapToObj(x -> numString.apply(Integer.valueOf(x)))  
                            .reduce(indent.apply(n - i), (acc, a) -> acc + a + "    ");  
  
            System.out.println(numStr);  
        }  
    }  
}

3.3.2 答案2

3.3.2.1 思路
  1. 用暴力遍历,所以不管左右移动,一直右移n-1次再加上初始就是所有循环移动的排列
  2. 矩阵取每行所有循环右移的最大值,就是总最大值
3.3.2.2 代码
import java.util.ArrayList;  
import java.util.Arrays;  
import java.util.List;  
import java.util.function.BiFunction;  
import java.util.function.Function;  
import java.util.function.UnaryOperator;  
import java.util.stream.Collectors;  
import java.util.stream.IntStream;  
import java.util.stream.Stream;  
  
public class algorithm_2 {  
  
    /**  
     * 主算法,暴力便利vec的所有右移组合,选取最大的返回  
     * 给定一个向量,返回其最大值和最大值的bits组合  
     * 返回值: [ [maxV], [vec]], 例如: [ [6], [1, 1, 0]]  
     */    
     private static int[][] maxVec(int[] vec) {  
        // 计算二进制值 [1, 1, 0] -> 6        
        Function<int[], Integer> binVal = bits -> Arrays.stream(bits).reduce(0, (acc, b) -> (acc << 1) | b);  
  
        // 循环右移1位, 右移2位就是调用2次  
        UnaryOperator<int[]> rightMove = bits -> IntStream.range(0, bits.length)  
                .map(i -> bits[(i + bits.length - 1) % bits.length])  
                .toArray();  
  
        BiFunction<Integer, int[], int[]> repeatRightMove = (Integer n, int[] bits) -> Stream.iterate(bits, rightMove)  
                .skip(n)  
                .findFirst().get();  
  
        // 长度为n的数组总共有n种情况,可以右移n-1次  
        // 这个是vec的其他情况, 有n-1种组合  
        List<int[]> otherComb = IntStream.range(1, vec.length)  
                        .mapToObj(i -> repeatRightMove.apply(i, vec)).collect(Collectors.toList());  
  
        // 用reduce进行筛选,每次保留值最大的一个数据  
        // 例如:初始值[ [6], [1, 1, 0]], 变化一次 [ [3], [0, 1, 1]] 保留[ [6], [1, 1, 0]]然后进行下一次比较  
        int[] maxBits = otherComb.stream().reduce(vec, (acc, rest) -> {  
            if (binVal.apply(acc) > binVal.apply(rest)) {  
                return acc;  
            }  
            return rest;  
        });  
  
        // 数据转化,方便后面算法,其实也可以构造类,但是这个算法不复杂,数组结构够用了  
        return new int[][]{ {binVal.apply(maxBits)}, maxBits };  
    }  
  
  
    public static void main(String[] args) {  
        int[][] matrix = new int[][] {  
                new int[]{0,1,1},  
                new int[]{1,0,1},  
                new int[]{0,0,1}  
        };  
  
        int maxV = 0;  
        List<int[]> maxBitsComb = new ArrayList<>();  
  
        for(int i = 0; i < matrix.length; i++) {  
            int[][] res = maxVec(matrix[i]);  
            maxV = maxV + res[0][0];  
            maxBitsComb.add(res[1]);  
        }  
  
        System.out.println(maxV);  
        maxBitsComb.forEach(a -> System.out.println((Arrays.toString(a)).replaceAll("\\[|\\]", "")));  
    }  
}

更多推荐