【廖雪峰官方网站/Java教程】函数式编程

news/2024/5/20 4:35:43/文章来源:https://blog.csdn.net/Allenlzcoder/article/details/104453432

本博客是函数式编程这一节的学习笔记,网址:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943847278976
这一节课内容分为3个主题:Lambda基础、方法引用和试用Stream。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。
Java平台从Java 8开始,支持函数式编程。

1.Lambda基础

关于Lambda的详细用法介绍强烈推荐此博客:https://blog.csdn.net/bitcarmanlee/article/details/70195403。题外话,此博客作者是一大牛,java分类中的文章值得细看。

1.1.Lambda表达式

在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:

  • Comparator
  • Runnable
  • Callable
    以Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例,以匿名类方式编写如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {public int compare(String s1, String s2) {return s1.compareTo(s2);}
});

上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:

// Lambda
import java.util.Arrays;
public class Main {public static void main(String[] args) {String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };Arrays.sort(array, (s1, s2) -> {return s1.compareTo(s2);});System.out.println(String.join(", ", array));}
}

观察Lambda表达式的写法,它只需要写出方法定义:

(s1, s2) -> {return s1.compareTo(s2);
}

其中,参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { … }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁。
如果只有一行return xxx的代码,完全可以用更简单的写法:

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错。
有关Comparator用法,贴一个链接:
[1]https://blog.csdn.net/u012250875/article/details/55126531
下面是java中一些使用到Comparator接口的地方:

Arrays.sort(T[], Comparator<? super T> c);
Collections.sort(List<T> list, Comparator<? super T> c);

在利用Comparator接口时,需要重写

int compare(T o1, T o2);

这个函数,关于此函数的比较顺序,jdk官方默认是升序,是基于:
int compare(T o1, T o2)
Returns: a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.

  • o1 < o2: 返回负整数,标识o1和o2的顺序不用交换
  • o1 = o2: 返回零,o1和o2的位置不用动
  • o1 > o2: 返回正整数,o1和o2的位置需要交换

若要实现降序排列,则只需要

  • o1 > o2: 返回负整数,标识o1和o2的顺序不用交换
  • o1 = o2: 返回零,o1和o2的位置不用动
  • o1 < o2: 返回正整数,o1和o2的位置需要交换

有关compareTo的用法,贴两个链接:
[1]https://www.runoob.com/java/number-compareto.html
[2]https://www.cnblogs.com/efforts-will-be-lucky/p/7052910.html
关于匿名内部类解析:
[1]https://blog.csdn.net/chengqiuming/article/details/91352913

1.2.FunctionalInterface

我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:

@FunctionalInterface
public interface Callable<V> {V call() throws Exception;
}

再来看Comparator接口:

@FunctionalInterface
public interface Comparator<T> {int compare(T o1, T o2);boolean equals(Object obj);default Comparator<T> reversed() {return Collections.reverseOrder(this);}default Comparator<T> thenComparing(Comparator<? super T> other) {...}...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。

1.3.小结

  • 单方法接口被称为FunctionalInterface。
  • 接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。
  • Lambda表达式的参数和返回值均可由编译器自动推断。

2.方法引用

2.1.静态方法引用/实例方法引用

使用Lambda表达式,我们就可以不必编写FunctionalInterface接口的实现类,从而简化代码:

Arrays.sort(array, (s1, s2) -> {return s1.compareTo(s2);
});

实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:

import java.util.Arrays;public class Main {public static void main(String[] args) {String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };Arrays.sort(array, Main::cmp);System.out.println(String.join(", ", array));}static int cmp(String s1, String s2) {return s1.compareTo(s2);}
}

上述代码在Arrays.sort()中直接传入了静态方法cmp的引用,用Main::cmp表示。
因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。
因为Comparator< String >接口定义的方法是int compare(String, String),和静态方法int cmp(String, String)相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入:

Arrays.sort(array, Main::cmp);

注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
我们再看看如何引用实例方法。如果我们把代码改写如下:

import java.util.Arrays;public class Main {public static void main(String[] args) {String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };Arrays.sort(array, String::compareTo);System.out.println(String.join(", ", array));}
}

不但可以编译通过,而且运行结果也是一样的,这说明String.compareTo()方法也符合Lambda定义。
观察String.compareTo()的方法定义:

public final class String {public int compareTo(String o) {...}
}

这个方法的签名只有一个参数,为什么和int Comparator< String >.compare(String, String)能匹配呢?
因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:

public static int compareTo(this, String o);

所以,String.compareTo()方法也可作为方法引用传入。
关于静态方法和实例方法的说明参考:
[1]https://www.cnblogs.com/gemuxiaoshe/p/10641670.html

2.2.构造方法引用

除了可以引用静态方法和实例方法,我们还可以引用构造方法。
我们来看一个例子:如果要把一个List< String >转换为List< Person >,应该怎么办?

class Person {String name;public Person(String name) {this.name = name;}
}List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = ???

传统的做法是先定义一个ArrayList< Person >,然后用for循环填充这个List:

List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {persons.add(new Person(name));
}

要更简单地实现String到Person的转换,我们可以引用Person的构造方法:

// 引用构造方法
import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {List<String> names = List.of("Bob", "Alice", "Tim");List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());System.out.println(persons);}
}class Person {String name;public Person(String name) {this.name = name;}public String toString() {return "Person:" + this.name;}
}

后面我们会讲到Stream的map()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:

@FunctionalInterface
public interface Function<T, R> {R apply(T t);
}

把泛型对应上就是方法签名Person apply(String),即传入参数String,返回类型Person。而Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是类名::new,因此,此处传入Person::new。

2.3.小结

FunctionalInterface允许传入:

  1. 接口的实现类(传统写法,代码较繁琐);
  2. Lambda表达式(只需列出参数名,由编译器推断类型);
  3. 符合方法签名的静态方法;
  4. 符合方法签名的实例方法(实例类型被看做第一个参数类型);
  5. 符合方法签名的构造方法(实例类型被看做返回类型)。

FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。

3.使用Stream

3.1.背景介绍

Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。
划重点:这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。两者对比如下:
在这里插入图片描述
有同学会问:一个顺序输出的Java对象序列,不就是一个List容器吗?
再次划重点:这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:
在这里插入图片描述
我们总结一下Stream的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。
Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。
最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。

Stream<BigInteger> naturals = createNaturalStream(); // 不计算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
Stream<BigInteger> s3 = s2.limit(100); // 不计算
s3.forEach(System.out::println); // 计算

惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。
例如,创建一个全体自然数的Stream,不会进行计算,把它转换为上述s2这个Stream,也不会进行计算。再把s2这个无限Stream转换为s3这个有限的Stream,也不会进行计算。只有最后,调用forEach确实需要Stream输出的元素时,才进行计算。我们通常把Stream的操作写成链式操作,代码更简洁:

createNaturalStream().map(BigInteger::multiply).limit(100).forEach(System.out::println);

因此,Stream API的基本用法就是:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:

int result = createNaturalStream() // 创建Stream.filter(n -> n % 2 == 0) // 任意个转换.map(n -> n * n) // 任意个转换.limit(100) // 任意个转换.sum(); // 最终计算结果

小结
Stream API的特点是:

  1. Stream API提供了一套新的流式处理的抽象序列;
  2. Stream API支持函数式编程和链式操作;
  3. Stream可以表示无限序列,并且大多数情况下是惰性求值的。

3.2.创建Stream

要使用Stream,就必须现创建它。创建Stream有很多种方法,我们来一一介绍。

3.2.1.Stream.of()

创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:

import java.util.stream.Stream;public class Main {public static void main(String[] args) {Stream<String> stream = Stream.of("A", "B", "C", "D");// forEach()方法相当于内部循环调用,// 可传入符合Consumer接口的void accept(T t)的方法引用:stream.forEach(System.out::println);}
}

虽然这种方式基本上没啥实质性用途,但测试的时候很方便。

3.2.2.基于数组或Collection

第二种创建Stream的方法是基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });Stream<String> stream2 = List.of("X", "Y", "Z").stream();stream1.forEach(System.out::println);stream2.forEach(System.out::println);}
}
  1. 把数组变成Stream使用Arrays.strem()方法;
  2. 对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream;
  3. 上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。

3.2.3.基于Supplier

具体请参考网址:https://www.liaoxuefeng.com/wiki/1252599548343744/1322655160467490

3.2.4.其他方法

创建Stream的第三种方法是通过一些API提供的接口,直接获得Stream。
例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {...
}

此方法对于按行遍历文本文件十分有用。
另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:

Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);

3.2.5.基本类型

因为Java的范型不支持基本类型,所以我们无法用Stream< int >这样的类型,会发生编译错误。为了保存int,只能使用String< Integer >,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:

// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);

3.2.6.小结

创建Stream的方法有 :

  1. 通过指定元素、指定数组、指定Collection创建Stream;
  2. 通过Supplier创建Stream,可以是无限序列;
  3. 通过其他类的相关方法创建。

基本类型的Stream有IntStream、LongStream和DoubleStream。

3.3.使用map

Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。
所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:
在这里插入图片描述
可见,map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

如果我们查看Stream的源码,会发现map()方法接收的对象是Function接口对象,它定义了一个apply()方法,负责把一个T类型转换成R类型:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

其中,Function的定义是:

@FunctionalInterface
public interface Function<T, R> {// 将T类型转换为R:R apply(T t);
}

利用map(),不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ").stream().map(String::trim) // 去空格.map(String::toLowerCase) // 变小写.forEach(System.out::println); // 打印}
}

小结:
map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream;
可以将一种元素类型转换成另一种元素类型。

3.4.使用filter

Stream.filter()是Stream的另一个常用转换方法。
所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。
例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:
在这里插入图片描述
用IntStream写出上述逻辑,代码如下:

import java.util.stream.IntStream;public class Main {public static void main(String[] args) {IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).filter(n -> n % 2 != 0).forEach(System.out::println);}
}

从结果可知,经过filter()后生成的Stream元素可能变少。
filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

@FunctionalInterface
public interface Predicate<T> {// 判断元素t是否符合条件:boolean test(T t);
}

filter()除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:

import java.time.*;
import java.util.function.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {Stream.generate(new LocalDateSupplier()).limit(31).filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY).forEach(System.out::println);}
}class LocalDateSupplier implements Supplier<LocalDate> {LocalDate start = LocalDate.of(2020, 1, 1);int n = -1;public LocalDate get() {n++;return start.plusDays(n);}
}

小结:
使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream。

3.5.使用reduce

map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
我们来看一个简单的聚合方法:

import java.util.stream.*;public class Main {public static void main(String[] args) {int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);System.out.println(sum); // 45}
}

reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:

@FunctionalInterface
public interface BinaryOperator<T> {// Bi操作:两个输入,一个输出T apply(T t, T u);
}

上述代码看上去不好理解,但我们用for循环改写一下,就容易理解了:

Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {sum = (sum, n) -> sum + n;
}

可见,reduce()操作首先初始化结果为指定值(这里是0),紧接着,reduce()对每个元素依次调用(acc, n) -> acc + n,其中,acc是上次计算的结果:

// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9

因此,实际上这个reduce()操作是一个求和。
如果去掉初始值,我们会得到一个Optional< Integer >:

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent) {System.out.println(opt.get());
}

这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。
利用reduce(),我们可以把求和改成求积,代码也十分简单:

import java.util.stream.*;public class Main {public static void main(String[] args) {int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);System.out.println(s); // 362880}
}

注意:计算求积时,初始值必须设置为1。
除了可以对数值进行累积计算外,灵活运用reduce()也可以对Java对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过map()和reduce()操作聚合成一个Map< String, String >:

import java.util.*;public class Main {public static void main(String[] args) {// 按行读取配置文件:List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");Map<String, String> map = props.stream()// 把k=v转换为Map[k]=v:.map(kv -> {String[] ss = kv.split("\\=", 2);return Map.of(ss[0], ss[1]);})// 把所有Map聚合到一个Map:.reduce(new HashMap<String, String>(), (m, kv) -> {m.putAll(kv);return m;});// 打印结果:map.forEach((k, v) -> {System.out.println(k + " = " + v);});}
}

关于Map.putAll()方法,该方法用来追加另一个Map对象到当前Map集合对象,它会把另一个Map集合对象中的所有内容添加到当前Map集合对象。

putAll(Map<? extends K,? extends V> m) 

小结:

  1. reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并;
  2. reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

3.6.输出集合

3.6.1.转换操作/聚合操作

我们介绍了Stream的几个常见操作:map()、filter()、reduce()。这些操作对Stream来说可以分为两类,一类是转换操作,即把一个Stream转换为另一个Stream,例如map()和filter(),另一类是聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()。
区分这两种操作是非常重要的,因为对于Stream来说,对其进行转换操作并不会触发任何计算!我们可以做个实验:

import java.util.function.Supplier; 
import java.util.stream.Stream;public class Main {public static void main(String[] args)     {Stream<Long> s1 = Stream.generate(new NatualSupplier());Stream<Long> s2 = s1.map(n -> n * n);Stream<Long> s3 = s2.map(n -> n - 1);System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842}
}class NatualSupplier implements Supplier<Long> {long n = 0;public Long get() {n++;return n;}
}

因为s1是一个Long类型的序列,它的元素高达922亿个,但执行上述代码,既不会有任何内存增长,也不会有任何计算,因为转换操作只是保存了转换规则,无论我们对一个Stream转换多少次,都不会有任何实际计算发生。

而聚合操作则不一样,聚合操作会立刻促使Stream输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream进行聚合操作,会触发一系列连锁反应:

Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);

我们对s4进行reduce()聚合计算,会不断请求s4输出它的每一个元素。因为s4的上游是s3,它又会向s3请求元素,导致s3向s2请求元素,s2向s1请求元素,最终,s1从Supplier实例中请求到真正的元素,并经过一系列转换,最终被reduce()聚合出结果。
可见,聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。

3.6.2.输出为List/Set

reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。
下面的代码演示了如何将一组String先过滤到空字符串,然后把非空字符串保存到List中:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());System.out.println(list);}
}

把Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。
类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

3.6.3.输出为数组

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

3.6.4.输出为Map

如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");Map<String, String> map = stream.collect(Collectors.toMap(// 把元素s映射为key:s -> s.substring(0, s.indexOf(':')),// 把元素s映射为value:s -> s.substring(s.indexOf(':') + 1)));System.out.println(map);}
}

3.6.5.分组输出

Stream还有一个强大的分组功能,可以按组输出。我们看下面的例子:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");Map<String, List<String>> groups = list.stream().collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));System.out.println(groups);}
}

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List,上述代码运行结果如下:

{A=[Apple, Avocado, Apricots],B=[Banana, Blackberry],C=[Coconut, Cherry]
}

可见,结果一共有3组,按"A",“B”,"C"分组,每一组都是一个List。
假设有这样一个Student类,包含学生姓名、班级和成绩:

class Student {int gradeId; // 年级int classId; // 班级String name; // 名字int score; // 分数
}

如果我们有一个Stream,利用分组输出,可以非常简单地按年级或班级把Student归类。

3.6.6.小结

  1. Stream可以输出为集合:
  2. Stream通过collect()方法可以方便地输出为List、Set、Map,还可以分组输出。

3.7.其他操作

我们把Stream提供的操作分为两类:转换操作和聚合操作。除了前面介绍的常用操作外,Stream还提供了一系列非常有用的方法。

3.7.1.排序

对Stream的元素进行排序十分简单,只需调用sorted()方法:

import java.util.*;
import java.util.stream.*;public class Main {public static void main(String[] args) {List<String> list = List.of("Orange", "apple", "Banana").stream().sorted().collect(Collectors.toList());System.out.println(list);}
}

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可:

List<String> list = List.of("Orange", "apple", "Banana").stream().sorted(String::compareToIgnoreCase).collect(Collectors.toList());

关于sort()用法的一个例子

package lambda;import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;public class lambdaSortTest {public static void main(String[] args) {List<Dog> dogList = new ArrayList<>();Dog dog1 = new Dog(13, "black");Dog dog2 = new Dog(13, "red");Dog dog3 = new Dog(8, "yellow");dogList.add(dog1);dogList.add(dog2);dogList.add(dog3);dogList.stream().sorted(Comparator.comparing(Dog::getPrice) // 先按照价格升序排列.thenComparing(Dog::getColor)) // 再按照颜色升序排列.forEach(System.out::println);}
}class Dog {private int price;private String color;public Dog(int price, String color) {this.price = price;this.color = color;}public int getPrice() {return price;}public String getColor() {return color;}@Overridepublic String toString() {return "Dog, price = "+ price + "; color = "+ color + ".";}
}

输出如下:

Dog, price = 8; color = yellow.
Dog, price = 13; color = black.
Dog, price = 13; color = red.

注意sorted()只是一个转换操作,它会返回一个新的Stream。

3.7.2.去重

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():

List.of("A", "B", "A", "C", "B", "D").stream().distinct().collect(Collectors.toList()); // [A, B, C, D]

3.7.3.截取

截取操作常用于把一个无限的Stream转换成有限的Stream,skip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:

List.of("A", "B", "C", "D", "E", "F").stream().skip(2) // 跳过A, B.limit(3) // 截取C, D, E.collect(Collectors.toList()); // [C, D, E]

截取操作也是一个转换操作,将返回新的Stream。

3.7.4.合并

将两个Stream合并为一个Stream可以使用Stream的静态方法concat():

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]

3.7.5.flatMap

如果Stream的元素是集合:

Stream<List<Integer>> s = Stream.of(Arrays.asList(1, 2, 3),Arrays.asList(4, 5, 6),Arrays.asList(7, 8, 9));

而我们希望把上述Stream转换为Stream< Integer >,就可以使用flatMap():

Stream<Integer> i = s.flatMap(list -> list.stream());

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream:
在这里插入图片描述

3.7.6.并行

通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。
把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:

Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream.sorted() // 可以进行并行排序.toArray(String[]::new);

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。

3.7.7.其他聚合方法

除了reduce()和collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

3.7.8.其他方法

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件; boolean
  • anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {System.out.println("Hello, " + str);
});

3.7.9.小结

Stream提供的常用操作有:

  • 转换操作:map(),filter(),sorted(),distinct(),skip(),limit();
  • 合并操作:concat(),flatMap();
  • 并行处理:parallel();
  • 聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();
  • 其他操作:allMatch(), anyMatch(), forEach()。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_712923.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【廖雪峰官方网站/Java教程】反射

注&#xff1a;本文参考自&#xff1a;https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512 什么是反射&#xff1f; 反射就是Reflection&#xff0c;Java的反射是指程序在运行期可以拿到一个对象的所有信息。 正常情况下&#xff0c;如果我们要调用一个对象的…

【廖雪峰官方网站/Java教程】泛型

泛型是一种“代码模板”&#xff0c;可以用一套代码套用各种类型。 1.什么是泛型 1.1.泛型入门概念介绍 为了在ArrayList中兼容不同类型等元素&#xff0c;我们必须把ArrayList变成一种模板&#xff1a;ArrayList<T>&#xff0c;代码如下&#xff1a; public class …

【廖雪峰官方网站/Java教程】多线程(1)

多线程是Java最基本的一种并发模型&#xff0c;本章我们将详细介绍Java多线程编程。 1.多线程基础 1.1.进程 在计算机中&#xff0c;我们把一个任务称为一个进程&#xff0c;浏览器就是一个进程&#xff0c;视频播放器是另一个进程&#xff0c;类似的&#xff0c;音乐播放器…

【廖雪峰官方网站/Java教程】多线程(2)

1.使用wait和notify 1.1.多线程协调 在Java程序中&#xff0c;synchronized解决了多线程竞争的问题。例如&#xff0c;对于一个任务管理器&#xff0c;多个线程同时往队列中添加任务&#xff0c;可以用synchronized加锁&#xff1a; class TaskQueue {Queue<String> q…

【廖雪峰官方网站/Java教程】多线程(3)

1.使用线程池 1.1.ExecutorService介绍 Java语言虽然内置了多线程支持&#xff0c;启动一个新线程非常方便&#xff0c;但是&#xff0c;创建线程需要操作系统资源&#xff08;线程资源&#xff0c;栈空间等&#xff09;&#xff0c;频繁创建和销毁大量线程需要消耗大量时间。…

【廖雪峰官方网站/Java教程】Maven基础

Maven是一个Java项目管理和构建工具&#xff0c;它可以定义项目结构、项目依赖&#xff0c;并使用统一的方式进行自动化构建&#xff0c;是Java项目不可缺少的工具。 1.Maven介绍 1.1.Maven功能及项目结构 1.1.1.Maven主要功能 Maven就是是专门为Java项目打造的管理和构建工…

【廖雪峰官方网站/Java教程】设计模式(一)

0.概述.设计模式的基本概念及原则 设计模式&#xff0c;即Design Patterns&#xff0c;是指在软件设计中&#xff0c;被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码&#xff0c;提高代码的可扩展性和可维护性。 为什么要使用设计模式&#xff1f;根本原因…

[转]上海新东方vs新东方,SEO实战

引用前言&#xff1a;半夜无聊上网&#xff0c;看到了这篇文章&#xff0c;觉得还不错&#xff0c;看了收获不小&#xff0c;所以就转过来了。来源 非常郑重地声明一下&#xff08;文章发表约20小时后补充&#xff09; 我不得不承认&#xff0c;这篇文章有点“软”&#xff0c;…

前端web:响应式网站开发的现状你了解吗?

当企业对网络营销有了更深的认识时&#xff0c;不管是大企业还是小企业&#xff0c;都已经建立了自己的个性化响应网站&#xff0c;都希望利用互联网这一新形态的市场&#xff0c;以及网络营销的新型营销模式。如今建立响应式网站也是一种趋势&#xff0c;利用互联网的优势&…

学用MVC4做网站五:5.4删除文章

前几天把添加、修改功能都做了&#xff0c;今天开始写删除功能。删除文章既要删除文章本身同时也要在公共模型中删除对应项。 首先写从数据库中删除文章的函数。打开ArticleRepository修改Delete的函数。有上次的教训这次明白了传递的id应该是公共模型id。 /// <summary>…

三分钟免费搞定网站在线客服,利用PowerTalkBox控件制作而成,为大家提供比较好的示例...

下载地址:http://download.csdn.net/source/1876659 必须安装.net2.0才可以支持网站服务端 内带完整的安装流程,支持飞信功能,使您不在电脑前时也可以用手机交流. 可以利用以下的js代码实现浮动窗口的功能. <script languagejavascript>var cao_x,cao_y; function cao888…

amazon s3_在Amazon S3上托管静态网站

amazon s3Static website hosting on Amazon S3 is one of the very popular use cases of Amazon S3. It allows you to host an entire static website and on very low cost. Amazon S3 is a highly available and scalable hosting solution.Amazon S3上的静态网站托管是Am…

一个可以实时查相关电子产品价格的网站

香港价格网&#xff0c;里面的价格和香港的百老汇、丰泽等的价格几乎同步&#xff0c;相差不大&#xff0c;有很大的参考价值&#xff0c;对于准备去香港买电子产品的网友来说&#xff0c;是个非常好的网站&#xff0c;特别分享&#xff1a; http://www.price.com.hk/转载于:ht…

简单高效!25个漂亮的简约风格网站设计作品

在过去几年里&#xff0c;网站设计领域发生了巨大变化。除了 RWD&#xff08;响应式网页设计&#xff09;和 Web 字体的革命&#xff0c;现代设计的发展趋势迅速流行扁平化的配色方案&#xff0c;网页排版变得更加重要&#xff0c;重点已放在内容第一。最后&#xff0c;页面加载…

增城seo搜索引擎优化_搜索引擎seo优化主要从哪里入手?

首先我们应该了解什么是搜索引擎优化以及网站搜索引擎seo优化的价值&#xff0c;从基础开始逐步深入&#xff0c;下面拓王朝所要讲的都是一些理论知识&#xff0c;很好理解&#xff0c;有不同见解欢迎评论。SEO优化SEO搜索引擎优化&#xff0c;是指通过采用易于搜索引擎索引和排…

[转]使用ThinkPHP框架快速开发网站(多图)

本文转自&#xff1a;http://blog.csdn.net/ruby97/article/details/7574851 这一周一直忙于做实验室的网站&#xff0c;基本功能算是完成了。比较有收获的是大概了解了ThinkPHP框架。写一些东西留作纪念吧。如果对于同样是Web方面新手的你有一丝丝帮助&#xff0c;那就更好了挖…

《大型网站技术架构》读书笔记[3] - 架构核心五要素

架构设计中要考虑的核心五要素&#xff1b; 性能、可用性、扩展性、伸缩性、安全性 性能 性能的测试指标 响应时间 应用执行一个操作需要的时间&#xff0c;包括从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标&#xff0c;直观地反映了系统的“…

java抓取网页数据_Golang丨Java丨Python爬虫实战—Boss直聘网站数据抓取

我们分别通过Golang、Python、Java三门语言&#xff0c;分别实现对Boss直聘网站的招聘数据进行爬取。首先打开Boss直聘网站&#xff1a;然后我们在职位类型中输入Go或者Golang关键字&#xff1a;然后我们可以看到一个列表&#xff0c;和Go语言相关的各种招聘职位&#xff0c;还…

我的网站被黑了,关键词被劫持,总结一下是怎么解决的。

1、发现被黑&#xff0c;网站被黑的症状 两年前自己用wordpress搭了一个网站&#xff0c;平时没事写写文章玩玩。但是前些日子&#xff0c;突然发现网站的流量突然变小&#xff0c;site了一下百度收录&#xff0c;发现出了大问题&#xff0c;网站被黑了。大多数百度抓取收录的页…

一个大图切成几个小图加载速度更快_谷歌SEO页面速度的重要性

什么是页面速度&#xff1f;页面速度是指网页加载所需的时间。一个页面的加载速度是由几个不同的因素决定的&#xff0c;包括网站的服务器、页面文件大小和图片压缩。也就是说&#xff0c;"页面速度 "并不像 "网页速度 "那么重要。"页面速度 "并…