文章

java8实战

java8实战

1:Lambda 表达式

  • 可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
  • Lambda的基本语法是(parameters) -> expression或(parameters) -> { statements; }
  • Lambda示例
    • 布尔表达式:(List<String> list) -> list.isEmpty()
    • 创建对象:() -> new Apple(10)
    • 消费一个对象:(Apple a) -> {System.out.println(a.getWeight());}
    • 从一个对象中选择/抽取:(String s) -> s.length()
    • 组合两个值:(int a, int b) -> a * b
    • 比较两个对象:(Apple a1, Apple a2) ->a1.getWeight().compareTo(a2.getWeight())

函数式接口

  • 一言以蔽之,函数式接口就是只定义一个抽象方法的接口。
  • Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例
  • 接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
  • Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口
  • 请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。
Predicate接收T返回布尔
  • java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FunctionalInterface
public interface Predicate<T>{
	boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
  List<T> results = new ArrayList<>();
  for(T s: list){
    if(p.test(s)){
    	results.add(s);
    }
  }
  return results;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer接收T无返回
  • java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T 的对象,没有返回(void)。
1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface Consumer<T>{
	void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> c){
  for(T i: list){
  	c.accept(i);
  }
}

forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));
Function接收T返回R
  • java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个 泛型T的对象,并返回一个泛型R的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FunctionalInterface
public interface Function<T, R>{
	R apply(T t);
}

public static <T, R> List<R> map(List<T> list,Function<T, R> f) {
  List<R> result = new ArrayList<>();
  for(T s: list){
  	result.add(f.apply(s));
  }
  return result;
}

// [7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas","in","action"),
(String s) -> s.length()
);
总结
函数式接口函数描述符原始类型特化
Predicate<T>T->booleanIntPredicate
LongPredicate
DoublePredicate
Consumer<T>T->voidIntConsumer
LongConsumer
DoubleConsumer
Function<T,R>T->RIntFunction<R>
IntToDoubleFunction
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的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的 参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

  • List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);

  • 类型检查过程可以分解为

    首先,你要找出filter方法的声明。

    第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数

    第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。

    第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean

    最后,filter的任何实际参数都必须匹配这个要求

  • Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。

1
2
3
4
//参数a没有显式类型
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
局部变量
  • Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。
  • Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。
  • Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。)
1
2
3
4
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
//错误:Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的
  • 实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。
  • 如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。
  • 因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了

方法引用

  • 方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法
  • 使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。不需要括号,因为 你没有实际调用这个方法。
  • 方法引用主要有三类
    • 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
    • 指向任意类型实例方法的方法引用( 例如String 的length 方法, 写作String::length)。
    • 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction 用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive- Transaction::getValue)。
  • 对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用: ClassName::new。

复合表达式

  • 可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结 果成为另一个函数的输入。
比较器复合
  • 逆序:如果你想要对苹果按重量递减排序怎么办?用reversed

    inventory.sort(comparing(Apple::getWeight).reversed());

  • 比较器链:如果发现有两个苹果一样重怎么办?

    在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator

    inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

谓词复合
  • 谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词

  • 使用negate方法来返回一个Predicate的非,比如苹果不是红的:

    Predicate<Apple> notRedApple = redApple.negate();

  • 把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

    Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

  • 表达要么是重(150克以上)的红苹果,要么是绿苹果:

    Predicate<Apple> redAndHeavyAppleOrGreen =redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getColor()));

  • and和or方法是按照在表达式链中的位置,从左向右确定优先级的。

    因此,a.or(b).and(c)可以看作(a b) && c。
函数复合
  • Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

  • andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

  • 假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

    1
    2
    3
    4
    
    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.andThen(g);//数学上会写作g(f(x))
    int result = h.apply(1);//4
    
  • compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。它将意味着f(g(x)),而andThen则意味着g(f(x))

1
2
3
4
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);//数学上会写作f(g(x))
int result = h.apply(1);//3

2:引入流

流简介

  • 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。

  • Java 8中的Stream API可以让你写出这样的代码:

    声明性——更简洁,更易读

    可复合——更灵活

    可并行——性能更好

  • Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)

  • 流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序 值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元 素(如ArrayList 与 LinkedList)。但流的目的在于表达计算

  • ——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。

  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中 的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执 行,也可并行执行。

  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大 的流水线。

  • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

流与集合

  • 请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。
  • 使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。
  • Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出 一个函数说要干什么就可以了

流操作

  • 可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。

  • 除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。 这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

  • 终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚 至void。

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

    一个数据源(如集合)来执行一个查询;

    一个中间操作链,形成一条流的流水线;

    一个终端操作,执行流水线,并能生成结果。

3:使用流

排序sorted

可以类比sql的 orderBy 需要注意的是对象一定要是可以比较的,也既是否实现了Comparator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SortedDemo {

    public static void main(String[] args) {
        // 打印 13,15,20,23,50
        Arrays.asList(20, 15, 13, 23, 50).stream().sorted().forEach(System.out::println);
        
        // 如果自定义对象可以传入一个比较器
        Arrays.asList(21, 16, 14, 24, 51).stream().sorted(Integer::compareTo).forEach(System.out::println);
        
        // 不可以比较的可以传入一个Comparator接口
        Arrays.asList("22", "17", "15", "25", "52").stream().sorted((x, y) -> {
            return Integer.parseInt(x) - Integer.parseInt(y);
        }).forEach(System.out::println);
        
        // 不使用lambda表达式可以这样写,其它对象也可以参考下面的写法
        Arrays.asList("22", "17", "15", "25", "52").stream().sorted(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return Integer.parseInt(o1) - Integer.parseInt(o2);
            }
        }).forEach(System.out::println);

    }
}
————————————————
版权声明本文为CSDN博主诗水人间的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/qq_41813208/article/details/120384948

筛选和切片

谓词筛选filter
  • Streams接口支持filter方法。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
  • 筛选出所有素菜,创建一张素食菜单
1
2
3
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
元素各异distinct
  • 流还支持一个叫作distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。底层是Set去重
  • 筛选出列表中所有的偶数,并确保没有重复
1
2
3
4
5
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
截短流limit
  • 流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。
  • limit也可以用在无序流上,比如源是一个Set。这种情况下,limit的结果不会以任何顺序排列。
  • 选出热量超过300卡路里的头三道菜:
1
2
3
4
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());
跳过元素skip
  • 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流
  • limit(n)和skip(n)是互补的
  • 跳过超过300卡路里的头两道菜,并返回剩下的
1
2
3
4
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());

映射

  • 一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。
映射map
  • 流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)
  • 找出每道菜的名称有多长,怎么做?
1
2
3
4
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
流扁平化flatMap
  • 一言以蔽之,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

  • 给定单词列表[“Hello”,”World”],你想要返回列表[“H”,”e”,”l”, “o”,”W”,”r”,”d”]。

1
2
3
4
5
6
7
8
9
10
11
12
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split(""))//将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream)//将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());

/*
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所
有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流
相当于把一个箱子拆开,让里面的物品称为单个个体
*/

查找和匹配

  • 看数据集中的某些元素是否匹配一个给定的属性。StreamAPI通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
至少匹配anyMatch
  • anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”
  • anyMatch方法返回一个boolean,因此是一个终端操作。
  • 菜单里面是否有素食可选择:
1
2
3
if(menu.stream().anyMatch(Dish::isVegetarian)){
	System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
匹配所有allMatch
  • allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词
  • 菜品是否有利健康(即所有菜的热量都低于1000卡路里):
1
2
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
  • 和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。
1
2
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
  • anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉 的Java中&&和||运算符短路在流中的版本。
查找任意findAny
  • findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。
  • 找到一道素食菜肴。
1
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
  • Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。这样就不用返回众所周知容易出问题的null了。

    isPresent()将在Optional包含值的时候返回true, 否则返回false。

    ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。

    T get()会在值存在时返回值,否则抛出一个NoSuchElement异常

    T orElse(T other)会在值存在时返回值,否则返回一个默认值

查找第一个findFirst
  • 有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流)。你可能想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findany。
  • 给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
1
2
3
4
5
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9

归约

  • 此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)
元素求和reduce
  • 常规求和操作
1
2
3
4
int sum = 0;
for (int x : numbers) {
sum += x;
}
  • reduce接受两个参数:

    一个初始值,这里是0;相当于sum

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

1
2
3
4
5
6
7
8
9
List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

/*
首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0 + 4得到4
然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。
再用累积值和下一个元素3调用Lambda,得到12。
用12和流中最后一个元素9调用Lambda,得到最终结果21。
*/
  • 在Java 8中,Integer类现在有了一个静态的sum方法来对两个数求和

    int sum = numbers.stream().reduce(0, Integer::sum);

  • reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:

    Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

    考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。

  • 使用reduce来计算流中的最大值和最小值

    Optional<Integer> max = numbers.stream().reduce(Integer::max);

    Optional<Integer> max = numbers.stream().reduce(Integer::min);

    你当然也可以写成Lambda (x, y) -> x < y ? x : y而不是Integer::min,不过后者比较易读。

数值流

mapToInt,boxed
  • Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和 LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。
  • 将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前 面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream<T>
  • 用mapToInt对menu中的卡路里求和:
1
2
3
int calories = menu.stream()
.mapToInt(Dish::getCalories)//返回一个IntStream
.sum();
  • 如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。
  • 要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法
1
2
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);//将Stream 转换为数值流
Stream<Integer> stream = intStream.boxed();//将数值流转换为Stream
默认值OptionalInt
  • Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类 型特化版本:OptionalInt、OptionalDouble和OptionalLong。
  • 要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
1
2
3
4
5
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
//如果没有最大值的话,显式提供一个默认最大值
int max = maxCalories.orElse(1);
数值范围range
  • 和数字打交道时,有一个常用的东西就是数值范围。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
1
2
3
4
5
//表示范围[1, 100],一个从1到100的偶数流
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);

//从1 到100 有50个偶数
System.out.println(evenNumbers.count());

构建流

由值构建流Stream.of
  • 可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数
  • 使用Stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再 一个个打印出来:
1
2
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
  • 你可以使用empty得到一个空流

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

数组创建流Arrays.stream
  • 使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
1
2
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
文件生成流
  • Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
  • 一个文件中有多少各不相同的词:
1
2
3
4
5
6
7
8
9
long uniqueWords = 0;
//流会自动关闭
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
  uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))//生成单词流
  .distinct()//删除重复项
  .count();//数一数有多少个不同的单词
}
catch(IOException e){
}
函数生成流,无限流
  • Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
Stream.iterate(0, n -> n + 2)
  .limit(10)
  .forEach(System.out::println);
/*
iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的
Lambda(UnaryOperator<t>类型)。这里,我们使用Lambda n -> n + 2,返回的是前一个元
素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加
上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的,
因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是
按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集
合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。
然后可以调用forEach终端操作来消费流,并分别打印每个元素。
*/
  • 生成斐波纳契数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34…
1
2
3
4
5
6
//使用一个数组的两个元素来代表元组。例如,new int[]{0,1}就代表了斐波纳契序列(0, 1)中的第一个元素。

Stream.iterate(new int[]{0, 1},t -> new int[]{t[1],t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
  • generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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(10).forEach(System.out::println);

/*
前面的代码创建了一个IntSupplier的实例。此对象有可变的状态:它在两个实例变量中
记录了前一个斐波纳契项和当前的斐波纳契项。getAsInt在调用时会改变对象的状态,由此在
每次调用时产生新的值。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,
但在每次迭代时会创建新的元组。
*/

4:用流收集数据

归约和汇总

  • import static java.util.stream.Collectors.*;

    这样你就可以写counting()而用不着写Collectors.counting()之类的了。

最大max最小值min计数count
  • Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素。
  • 找出菜单中热量最高的菜
1
2
3
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

Optional<Dish> mostCalorieDish =menu.stream().collect(maxBy(dishCaloriesComparator));
汇总summingInt
  • Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。
  • 求出菜单列表的总热量:
1
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
  • 但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数
  • 可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:
1
2
3
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
//这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值(getter)方法来访问结果。打印menuStatisticobject会得到以下输出:
IntSummaryStatistics{count=9, sum=4300, min=120,average=477.777778, max=800}
连接字符串joining
  • joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
  • 把菜单中所有菜肴的名称连接起来
1
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
  • joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。此外还 要注意,如果Dish类有一个toString方法来返回菜肴的名称,那你无需用提取每一道菜名称的 函数来对原流做映射就能够得到相同的结果:
1
String shortMenu = menu.stream().collect(joining());
  • joining工厂方法有一个重载版本可以接受元素之间的分界符,这样你就可以得到一个逗号分隔的菜肴名称列表:
1
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
归约汇总reducing
  • Collectors.reducing工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。它需要三个参数。
    • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
    • 第二个参数就是函数,将菜肴转换成一个表示其所含热量的int。
    • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
  • 用reducing方法创建的收集器来计算你菜单的总热量
1
2
3
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,Integer::sum));
  • 单参数形式的reducing来找到热量最高的菜
1
2
Optional<Dish> mostCalorieDish = menu.stream()
  .collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

分组groupingBy

  • 假设你要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的都放另一组。用Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成
1
2
3
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

//{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],MEAT=[pork, beef, chicken]}
  • 你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。
  • 想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。
1
2
3
4
5
6
7
public enum CaloricLevel { DIET, NORMAL, FAT }

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(dish -> {
  if (dish.getCalories() <= 400) return CaloricLevel.DIET;
  else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
  else return CaloricLevel.FAT;
} ));
多级分组
  • 要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。我们可以把一个内层groupingBy传递给外层groupingBy
  • 普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f,toList())的简便写法。
1
2
3
4
5
6
7
8
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;
  } )
  )
);
  • 传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy
  • 数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数:
1
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
  • 查找菜单中热量最高的菜肴的收集器改一改,按照菜的类型分类:
1
2
Map<Dish.Type, Optional<Dish>> mostCaloricByType =menu.stream()
.collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));
  • 把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen工厂方法返回的收集器。这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。
  • 查找每个子组中热量最高的Dish
1
2
3
Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType,collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),Optional::get)));
  • 常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。
  • 对于每种类型的Dish,菜单中都有哪些CaloricLevel。
1
2
3
4
5
6
7
8
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },toSet() )));

//{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
//对于返回的Set是什么类型并没有任何保证。但通过使用toCollection,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求HashSet:把toSet()换为toCollection(HashSet::new)
一个对象中的多个字段分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo2 {
    public static void main(String[] args) {
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // data list
        List<User> userList = Arrays.asList(
                User.builder().id(123456).name("Zhang, San").city("ShangHai").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(777777).name("Zhang, San").city("ShangHai").sex("woman").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(888888).name("Li, Si").city("ShangHai").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(999999).name("Zhan, San").city("HangZhou").sex("woman").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(555555).name("Li, Si").city("NaJin").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build()
        );
        
        //根据城市和性别进行分组
        Map<String, List<User>> groupMap = userList.stream()
        	.collect(Collectors.groupingBy(u -> u.getCity() + "|" + u.getSex()));
        groupMap.forEach((k, v) -> {
            System.out.println(k);
            System.out.println(v);
        });
    }
}

优雅改造,自建工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MyBeanUtil {

    public static void main(String[] args) {
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // data list
        List<User> userList = Arrays.asList(
                User.builder().id(123456).name("Zhang, San").city("ShangHai").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(777777).name("Zhang, San").city("ShangHai").sex("woman").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(888888).name("Li, Si").city("ShangHai").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(999999).name("Zhan, San").city("HangZhou").sex("woman").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build(),
                User.builder().id(555555).name("Li, Si").city("NaJin").sex("man").birthDay(LocalDateTime.parse("2022-07-01 12:00:00", df)).build()
        );
        // 进行分组,根据名字和城市分组
        Map<String, List<User>> groupMap = groupingBy(userList, User::getSex, User::getCity);
        //打印分组结果
        groupMap.forEach((k, v) -> {
            System.out.println(k);
            System.out.println(v);
        });
    }
    /**
     * 将数据分组,根据方法引用(bean的get方法)
     *
     * @param list      为分组的数据
     * @param functions get方法数组
     */
    @SafeVarargs
    public static <T, R> Map<String, List<T>> groupingBy(List<T> list, Function<T, R>... functions) {
        return list.stream().collect(Collectors.groupingBy(t -> groupingBy(t, functions)));
    }

    /**
     * 分组工具根据函数式接口使用分组,将数据根据分组结果进行拆分
     */
    @SafeVarargs
    public static <T, R> String groupingBy(T t, Function<T, R>... functions) {
        if (functions == null || functions.length == 0) {
            throw new NullPointerException("functions数组不可以为空");
        } else if (functions.length == 1) {
            return functions[0].apply(t).toString();
        } else {
            return Arrays.stream(functions).map(fun -> fun.apply(t).toString()).reduce((str1, str2) -> str1 + "|" + str2).get();
        }
    }
}

为了能够完全集成在jdk类库中达到效果userList.stream().collect(Collectors.groupingBy(User::getCity,User::getSex));

于是将java.util.stream.Collectors源码完整copy出来,然后加入下面3个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    public static <T, K> Collector<T, ?, HashMap<K, List<T>>>
    groupingBy(Function<? super T, ? extends K>... classifier) {
        return groupingBy("|", classifier);
    }
    
    public static <T, K> Collector<T, ?, HashMap<K, List<T>>>
    groupingBy(String split, Function<? super T, ? extends K>... classifier) {
        return groupingBy(split, classifier, HashMap::new, toList());
    }
    
    public static <T, K, D, A, M extends Map<? super K, D>>
    Collector<T, ?, M> groupingBy(String split,
                                  Function<? super T, ? extends K>[] classifierArr,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
        Supplier<A> downstreamSupplier = downstream.supplier();
        BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
        BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
            String key = Arrays.stream(classifierArr).map(classifier -> Objects.requireNonNull(classifier.apply(t))).map(String::valueOf).reduce((s1, s2) -> s1 + split + s2).get();
            A container = m.computeIfAbsent((K) key, k -> downstreamSupplier.get());
            downstreamAccumulator.accept(container, t);
        };
        BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
        @SuppressWarnings("unchecked")
        Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;

        if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
            return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
        } else {
            @SuppressWarnings("unchecked")
            Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
            Function<Map<K, A>, M> finisher = intermediate -> {
                intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
                @SuppressWarnings("unchecked")
                M castResult = (M) intermediate;
                return castResult;
            };
            return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
        }
    }

分区partitioningBy

  • 分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。
  • 想要把菜单按照素食和非素食分开:
1
2
3
4
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));

//{false=[pork, beef, chicken, prawns, salmon],true=[french fries, rice, season fruit, pizza]}
  • 用同样的分区谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果:
1
2
List<Dish> vegetarianDishes =
menu.stream().filter(Dish::isVegetarian).collect(toList());
  • 分区的好处在于保留了分区函数返回true或false的两套流元素列表。partitioningBy 工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
1
2
3
4
5
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,groupingBy(Dish::getType)));

//{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},true={OTHER=[french fries, rice, season fruit, pizza]}}
  • 找到素食和非素食中热量最高的菜:
1
2
3
4
5
6
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(maxBy(comparingInt(Dish::getCalories)),Optional::get)));

//{false=pork, true=pizza}

收集器接口Collector

  • Collector接口的定义
1
2
3
4
5
6
7
8
9
10
11
12
public interface Collector<T, A, R> {
  Supplier<A> supplier();
  BiConsumer<A, T> accumulator();
  Function<A, R> finisher();
  BinaryOperator<A> combiner();
  Set<Characteristics> characteristics();
}
/*
T是流中要收集的项目的泛型。
A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
R是收集操作得到的对象(通常但并不一定是集合)的类型。
*/
  • 你可以实现一个ToListCollector<T>类,将Stream<T>中的所有元素收集到一个List<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>{
  /*建立新的结果容器:supplier方法
  supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实	     例,供数据收集过程使用。
  */
  public Supplier<List<T>> supplier() {
    return ArrayList::new;
  }
  /*
  将元素添加到结果容器:accumulator方法
  accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n1 个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的
元素的效果。
  */
  public BiConsumer<List<T>, T> accumulator() {
  	return List::add;
  }
  /*
  对结果容器应用最终转换:finisher方法
  在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
  */
  public Function<List<T>, List<T>> finisher() {
  	return Function.identity();
  }
  
  /*
  合并两个结果容器:combiner方法
  combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。
  */
  public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
    list1.addAll(list2);
    return list1; }
  }
  
  /*
  characteristics方法
  characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。
  */
  public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(
    IDENTITY_FINISH, CONCURRENT));
  }
}
  • 这个实现与Collectors.toList方法并不完全相同,但区别仅仅是一些小的优化。

    List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>());

    List<Dish> dishes = menuStream.collect(toList());

    构造之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。

toList
1
2
3
4
5
6
7
8
9
public class CollectDemo {

    public static void main(String[] args) {
        // 转换为字符串数组
        final List<String> collect = Arrays.asList(1, 2, 3, 4).stream()
        .map(String::valueOf).collect(Collectors.toList());
    }
}

toMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo {


    public static void main(String[] args) throws Exception {

        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().name(String.valueOf(i)).sex("男").age(11).build());
        }
        
        Map<String, Object> map = users.stream()
                .collect(Collectors.toMap(k -> k.name + k.sex, v -> v.age));

        map.forEach((k,v)->{
            System.out.println(k+" "+v);
        });
        
    }
}

解决key有冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Demo {

    public static void main(String[] args) {

        // 准备测试数据
        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().id(String.valueOf(i)).sex("男").build());
        }
        // 得到转换后的Map
        Map<String, String> map = users.stream().parallel()
                .collect(Collectors.toMap(k -> k.sex, v -> v.id, (x, y) -> {
                    System.out.println("第一个值的value:"+x);
                    System.out.println("第二个值的value:"+y);
                    System.out.println("===========保留:"+y);
                    return y;
                }));

        // 遍历map
        map.forEach((k, v) -> {
            System.out.println("最终结果:" + k + " " + v);
        });

    }
}

返回自定义map实现类

toMap(Function, Function, BinaryOperator, Supplier) 返回自定义的Map实现类,例如HashMap、ConcurrentHashMap等

最后一个参数一般都是ConcurrentHashMap::new或者HashMap::new这种形式

并发收集parallel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CollectDemo {

    public static void main(String[] args) {

        ArrayList<Object> collect;
        collect = Arrays.asList(
                User.builder().age(1).build(),
                User.builder().age(2).build(),
                User.builder().age(1).build(),
                User.builder().age(2).build(),
                User.builder().age(5).build()
        ).stream().collect(ArrayList::new, ArrayList::add, (x, y) -> {
        });
        // 单线程进行收集
        System.out.println(collect);

        // 正确的并发收集
        collect = Arrays.asList(
                User.builder().age(1).build(),
                User.builder().age(2).build(),
                User.builder().age(1).build(),
                User.builder().age(2).build(),
                User.builder().age(5).build()
        ).stream().parallel().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        System.out.println(collect);

    }
}

5:并行数据处理

并行流

  • 并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
  • 你可以把流转换成并行流,从而让求和并行运行——对顺序流调用parallel方法:
1
2
3
4
5
6
public static long parallelSum(long n) {
  return Stream.iterate(1L, i -> i + 1)
  .limit(n)
  .parallel()//将流转换为并行流
  .reduce(0L, Long::sum);
}
  • 对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行。类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。
  • 自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
  • 有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用limit可能会比单个有序流(比如数据源是一个List)更高效。
  • 还要考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。
  • 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素的好处还抵不上并行化造成的额外开销。
  • 要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂方法创建的原始类型流也可以快速分解。
  • 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。例如,一个SIZED流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处理,但筛选操作可能丢弃的元素个数却无法预测,导致流本身的大小未知。
  • 还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通过并行流得到的性能提升。
可分解性
ArrayList极佳
LinkedList
IntStream.range极佳
Stream.iterate
HashSet
TreeSet

分支合并框架

  • 分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。
  • 它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
使用RecursiveTask
  • 要把任务提交到这个池,必须创建RecursiveTask<R>的一个子类,其中R是并行化任务(以及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当然它可能会更新其他非局部机构)。

  • 要定义RecursiveTask,只需实现它唯一的抽象方法compute:

    protected abstract R compute();

    这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成 单个子任务结果的逻辑。

  • 试着用这个框架为一个数字范围(这里用一个long[]数组表示)求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {
	//要求和的数组
    private final long[] numbers;
    //子任务处理的数组的起始和终止位置
	private final int start;
	private final int end;
    //不再将任务分解为子任务的数组大小
	public static final long THRESHOLD = 10000;
  
    //公共构造函数用于创建主任务
	public ForkJoinSumCalculator(long[] numbers) {
		this(numbers, 0, numbers.length);
	}
  
    //私有构造函数用于以递归方式为主任务创建子任务
	private ForkJoinSumCalculator(long[] numbers, int start, int end) {
		this.numbers = numbers;
		this.start = start;
		this.end = end;
	}
  
    //覆盖RecursiveTask抽象方法
	@Override
	protected Long compute() {
        //该任务负责求和的部分的大小
		int length = end - start;
		if (length <= THRESHOLD) {
            //如果大小小于或等于阈值,顺序计算结果
			return computeSequentially();
		}
        //创建一个子任务来为数组的前一半求和
		ForkJoinSumCalculator leftTask =
		new ForkJoinSumCalculator(numbers, start, start + length/2);
        //利用另一个ForkJoinPool线程异步执行新创建的子任务
		leftTask.fork();
        //创建一个任务为数组的后一半求和
		ForkJoinSumCalculator rightTask =
		new ForkJoinSumCalculator(numbers, start + length/2, end);
        //同步执行第二个子任务,有可能允许进一步递归划分
		Long rightResult = rightTask.compute();
        //读取第一个子任务的结果,如果尚未完成就等待
		Long leftResult = leftTask.join();
        //该任务的结果是两个子任务结果的组合
		return leftResult + rightResult;
	}
  
    //在子任务不再可分时计算结果的简单算法
	private long computeSequentially() {
		long sum = 0;
		for (int i = start; i < end; i++) {
			sum += numbers[i];
		}
		return sum;
	}
}
  • 现在编写一个方法来并行对前n个自然数求和就很简单了。你只需把想要的数字数组传给 ForkJoinSumCalculator的构造函数:
1
2
3
4
5
public static long forkJoinSum(long n) {
	long[] numbers = LongStream.rangeClosed(1, n).toArray();
	ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
	return new ForkJoinPool().invoke(task);
}
  • 使用分支/合并框架的最佳做法
    • 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子 任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂, 因为每个子任务都必须等待另一个子任务完成才能启动。
    • 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直 接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
    • 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用 它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为 其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
    • 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计 算快。一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。一个惯用方 法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出 同时进行。
    • 你必须选择一个标准,来决定任务是要进一步拆分还是已小到可以顺序求值。
迭代器Spliterator
  • Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitableiterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。
  • Java 8已经为集合框架中包含的所有数据结构提供了一个默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Spliterator<T> {
  boolean tryAdvance(Consumer<? super T> action);
  Spliterator<T> trySplit();
  long estimateSize();
  int characteristics();
}
/*
T是Spliterator遍历的元素的类型。

tryAdvance方法的行为类似于普通的Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true

trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理

estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值也有助于让拆分均匀一点
*/
  • 拆分过程
    • 将Stream拆分成多个部分的算法是一个递归过程
    • 第一步是对第一个Spliterator调用trySplit,生成第二个Spliterator。
    • 第二步对这两个Spliterator调用trysplit,这样总共就有了四个Spliterator
    • 第三步这个框架不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割
    • 最后,这个递归拆分过程到第四步就终止了,这时所有的Spliterator在调用trySplit时都返回了null。
  • Spliterator接口声明的最后一个抽象方法是characteristics,它将返回一个int,代表Spliterator本身特性集的编码。

6:默认方法

  • Java 8引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。这种机制可以使你平滑地进行接口的优化和演进。
  • List接口中的sort方法是Java 8中全新的方法
1
2
3
default void sort(Comparator<? super E> c){
	Collections.sort(this, c);
}
  • 请注意返回类型之前的新default修饰符。通过它,我们能够知道一个方法是否为默认方法。这里sort方法调用了Collections.sort方法进行排序操作。由于有了这个新的方法,我们现在可以直接通过调用sort,对列表中的元素进行排序。
  • Comparator.naturalOrder方法。这是Comparator接口的一个全新的静态方法,它返回一个 Comparator对象,并按自然序列对其中的元素进行排序(即标准的字母数字方式排序)。
  • Collection中的stream方法的定义如下:
1
2
3
default Stream<E> stream() {
	return StreamSupport.stream(spliterator(), false);
}
  • 默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的

API演进

  • 版本2的类库接口比版本1的类库新添加了一个方法,而使用类库的用户却没有实现最新的新增方法,编译程序就会报错。
  • 这就是默认方法试图解决的问题。它让类库的设计者放心地改进应用程序接口,无需担忧对遗留代码的影响,这是因为实现更新接口的类现在会自动继承一个默认的方法实现。
  • 现在,接口包含的方法签名在它的实现类中也可以不提供实现。实际上,缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供。
  • 默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体。

行为多继承

  • 由于Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。
  • 如果一个类同时实现了两个接口,这两个接口恰巧又提供了同样的默认方法签名,这时会发生什么情况?
  • 如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
    • (1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
    • (2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
    • (3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
  • Java 8中引入了一种新的语法X.super.m(…),其中X是你希望调用的m方法所在的父接口。举例来说,如果你希望C使用来自于B的默认方法,它的调用方式看起来就如下所示:
1
2
3
4
5
public class C implements B, A {
  void hello(){
  	B.super.hello();
  }
}

7:Optional空判断

  • 通过静态工厂方法Optional.empty,创建一个空的Optional对象:
1
Optional<Car> optCar = Optional.empty();
  • 使用静态工厂方法Optional.of,依据一个非空值创建一个Optional对象:
1
Optional<Car> optCar = Optional.of(car);
  • 使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional对象:
1
Optional<Car> optCar = Optional.ofNullable(car);
  • Optional类提供了多种方法读取Optional实例中的变量值。
    • get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。
    • orElse(T other),它允许你在Optional对象不包含值时提供一个默认值。
    • orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
    • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。
    • ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。
    • empty:返回一个空的Optional 实例
    • filter:如果值存在并且满足提供的谓词,就返回包含该值的Optional 对象;否则返回一个空的 Optional 对象
    • flatMap:如果值存在,就对该值执行提供的mapping 函数调用,返回一个Optional 类型的值,否则就返 回一个空的Optional 对象
    • isPresent:如果值存在就返回true,否则返回false
    • map:如果值存在,就对该值执行提供的mapping 函数调用
    • of:将指定值用Optional 封装之后返回,如果该值为null,则抛出一个NullPointerException 异常
    • ofNullable:将指定值用Optional 封装之后返回,如果该值为null,则返回一个空的Optional 对象
  • 以不解包的方式组合两个Optional对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
	return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

/*
你对第一个Optional对象调用flatMap方法,如果它是个空值,传递给它
的Lambda表达式不会执行,这次调用会直接返回一个空的Optional对象。反之,如果person
对象存在,这次调用就会将其作为函数Function的输入,并按照与flatMap方法的约定返回
一个Optional<Insurance>对象。这个函数的函数体会对第二个Optional对象执行map操
作,如果第二个对象不包含car,函数Function就返回一个空的Optional对象,整个
nullSafeFindCheapestInsuranc方法的返回值也是一个空的Optional对象。最后,如果
person和car对象都存在,作为参数传递给map方法的Lambda表达式能够使用这两个值安全
地调用原始的findCheapestInsurance方法,完成期望的操作。
*/
  • 使用filter剔除特定的值
  • 你可能需要检查保险公司的名称是否为“Cambridge-Insurance”。为了以一种安全的方式进行这些操作,你首先需要确定引用指向的Insurance对象是否为null,之后再调用它的getName方法
1
2
Optional<Insurance> optInsurance = ...; 
optInsurance.filter(insurance ->  "CambridgeInsurance".equals(insurance.getName()))  .ifPresent(x -> System.out.println("ok"));

8:异步编程

  • 主要目标是在同一个CPU上执行几个松耦合的任务,充分利用CPU的核,让其足够忙碌,从而最大化程序的吞吐量

Future接口

  • 它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成。
  • 要使用Future,通常你只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService,就万事大吉了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建Executor-Service,通过它你可以向线程池提交任务
ExecutorService executor = Executors.newCachedThreadPool();
//向Executor-Service提交一个Callable对象
Future<Double> future = executor.submit(new Callable<Double>() {
  public Double call() {
  	return doSomeLongComputation();
}});
//异步操作进行的同时,你可以做其他的事情
doSomethingElse();
try {
  	//获取异步操作的结果,如果最终被阻塞,无法得到结果,那么在最多等待1秒钟之后退出
	Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
  • 我们知道Future接口提供了方法来检测异步计算是否已经结束(使用isDone方法),等待异步操作结束,以及获取计算的结果

9:新的日期和时间

  • 开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
  • 你可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等
1
2
3
4
5
6
7
8
9
10
LocalDate date = LocalDate.of(2014, 3, 18);  //2014-03-18
int year = date.getYear();  //2014
Month month = date.getMonth();  //MARCH
int day = date.getDayOfMonth();  //18
DayOfWeek dow = date.getDayOfWeek();  //TUESDAY
int len = date.lengthOfMonth();  //31 (days in March)
boolean leap = date.isLeapYear();  false (not a leap year)

使用工厂方法从系统时钟中获取当前的日期:
LocalDate today = LocalDate.now();
  • 一天中的时间,比如13:45:20,可以使用LocalTime类表示。你可以使用of重载的两个工厂方法创建LocalTime的实例。第一个重载函数接收小时和分钟,第二个重载函数同时还接收秒
1
2
3
4
5
6
7
LocalTime time = LocalTime.of(13, 45, 20);  //13:45:20
int hour = time.getHour();  //13
int minute = time.getMinute();  //45
int second = time.getSecond();  //20

LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");
  • LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造
1
2
3
4
5
6
7
8
9
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

LocalDate date1 = dt1.toLocalDate(); //2014-03-18
LocalTime time1 = dt1.toLocalTime();  //13:45:20
  • 通过它们各自的atTime或者atDate方法,向LocalDate传递一个时间对象,或者向LocalTime传递一个日期对象的方式,你可以创建一个LocalDateTime对象。你也可以使用toLocalDate或者toLocalTime方法,从LocalDateTime中提取LocalDate或者LocalTime组件:
  • Duration类和Period类共享了很多相似的方法
方法名是否静态方法描述
between创建两个时间点之间的interval
from由一个临时时间点创建interval
of由它的组成部分创建interval 的实例
parse由字符串创建interval 的实例
addTo创建该interval 的副本,并将其叠加到某个指定的temporal 对象
get读取该interval 的状态
isNegative检查该interval 是否为负值,不包含零
isZero检查该interval 的时长是否为零
minus通过减去一定的时间创建该interval 的副本
multipliedBy将interval 的值乘以某个标量创建该interval 的副本
negated以忽略某个时长的方式创建该interval 的副本
plus以增加某个指定的时长的方式创建该interval 的副本
subtractFrom从指定的temporal 对象中减去该interval
  • 操纵日期
1
2
3
4
5
6
7
8
9
LocalDate date1 = LocalDate.of(2014, 3, 18); //2014-03-18
LocalDate date2 = date1.withYear(2011); //2011-03-18
LocalDate date3 = date2.withDayOfMonth(25); //2011-03-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); //2011-09-25

LocalDate date1 = LocalDate.of(2014, 3, 18); //2014-03-18
LocalDate date2 = date1.plusWeeks(1);  //2014-03-25
LocalDate date3 = date2.minusYears(3); //2011-03-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); //2011-09-25
  • 表示时间点的日期时间类的通用方法
方法名是否静态方法方法描述
from依据传入的Temporal 对象创建对象实例
now依据系统时钟创建Temporal 对象
of由Temporal 对象的某个部分创建该对象的实例
parse由字符串创建Temporal 对象的实例
atOffset将Temporal 对象和某个时区偏移相结合
atZone将Temporal 对象和某个时区相结合
format使用某个指定的格式器将Temporal 对象转换为字符串(Instant 类不提供该方法)
get读取Temporal 对象的某一部分的值
minus创建Temporal 对象的一个副本,通过将当前Temporal 对象的值减去一定的时长创建该副本
plus创建Temporal 对象的一个副本,通过将当前Temporal 对象的值加上一定的时长创建该副本
with以该Temporal 对象为模板,对某些状态进行修改创建该对象的副本
  • 将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。
1
2
3
4
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2014, 3, 18); //2014-03-18
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); //2014-03-23
LocalDate date3 = date2.with(lastDayOfMonth()); //2014-03-31
  • TemporalAdjuster中包含的工厂方法列表
方法名描述
dayOfWeekInMonth创建一个新的日期,它的值为同一个月中每一周的第几天
firstDayOfMonth创建一个新的日期,它的值为当月的第一天
firstDayOfNextMonth创建一个新的日期,它的值为下月的第一天
firstDayOfNextYear创建一个新的日期,它的值为明年的第一天
firstDayOfYear创建一个新的日期,它的值为当年的第一天
firstInMonth创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值
lastDayOfMonth创建一个新的日期,它的值为当月的最后一天
lastDayOfNextMonth创建一个新的日期,它的值为下月的最后一天
lastDayOfNextYear创建一个新的日期,它的值为明年的最后一天
lastDayOfYear创建一个新的日期,它的值为今年的最后一天
lastInMonth创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值
next/previous创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期
nextOrSame/previousOrSame创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象
  • 格式化日期
1
2
3
4
5
6
7
8
9
10
11
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); //20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); //2014-03-18

LocalDate date1 = LocalDate.parse("20140318",DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18",DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

10:类库更新

集合

  • 集合类和接口中新增的方法
    • Map:getOrDefault,forEach,compute,computeIfAbsent,computeIfPresent,merge,putIfAbsent,remove(key,value),replace,replaceAll
    • Iterable:forEach,spliterator
    • Iterator:forEachRemaining
    • Collection:removeIf,stream,parallelStream
    • List:replaceAll,sort
    • BitSet:stream
本文由作者按照 CC BY 4.0 进行授权