Java Stream函数式编程接口最初是在Java 8中引入的,并且与lambda一起成为Java开发的里程碑式的功能特性,它极大的方便了开放人员处理集合类数据的效率。
Java Stream就是一个数据流经的管道,并且在管道中对数据进行操作,然后流入下一个管道。管道的功能包括:Filter(过滤)、Map(映射)、sort(排序)等,集合数据通过Java Stream管道处理之后,转化为另一组集合或数据输出。
首先,java.util.Collection接口中加入了default的stream方法,也就是说Collection接口下的所有实现都可以通过stream方法来获取Stream流。
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
Map<String, Object> map = new HashMap<>();
map.keySet().stream();
map.values().stream();
map.entrySet().stream();
List<String> nameStrs = Arrays.asList("Monkey", "Lion", "Giraffe","Lemur");
List<String> list = nameStrs.stream()
.filter(s -> s.startsWith("L"))
.map(String::toUpperCase)
.sorted()
.collect(toList());
Set<String> set = new HashSet<>(list);
Stream<String> streamFromSet = set.stream();
String[] array = {"Monkey", "Lion", "Giraffe", "Lemur"};
Stream<String> nameStrs2 = Stream.of(array);
Stream<String> nameStrs3 = Stream.of("Monkey", "Lion", "Giraffe", "Lemur");
通过Files.lines方法将文本文件转换为管道流,下图中的Paths.get()方法作用就是获取文件,是Java NIO的API。也就是说:我们可以很方便的使用Java Stream加载文本文件,然后逐行的对文件内容进行处理。
Stream<String> lines = Files.lines(Paths.get("file.txt"));
对Stream流中的数据进行处理,比如:过滤、数据转换等等。
@Data
@AllArgsConstructor
public class Employee {
private Integer id;
private Integer age; //年龄
private String gender; //性别
private String firstName;
private String lastName;
}
public class StreamFilterPredicate {
public static void main(String[] args){
Employee e1 = new Employee(1,23,"M","Rick","Beethovan");
Employee e2 = new Employee(2,13,"F","Martina","Hengis");
Employee e3 = new Employee(3,43,"M","Ricky","Martin");
Employee e4 = new Employee(4,26,"M","Jon","Lowman");
Employee e5 = new Employee(5,19,"F","Cristine","Maria");
Employee e6 = new Employee(6,15,"M","David","Feezor");
Employee e7 = new Employee(7,68,"F","Melissa","Roy");
Employee e8 = new Employee(8,79,"M","Alex","Gussin");
Employee e9 = new Employee(9,15,"F","Neetu","Singh");
Employee e10 = new Employee(10,45,"M","Naveen","Jain");
List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10);
List<Employee> filtered = employees.stream()
.filter(e -> e.getAge() > 70 && e.getGender().equals("M"))
.collect(Collectors.toList());
System.out.println(filtered);
}
}
lambda表达式表达的是一个匿名接口函数的实现。那具体到Stream.filter()中,它表达的是什么呢?可以看出它表达的是一个Predicate接口,在英语中这个单词的意思是:谓词。
// @return the new stream
Stream<T> filter(Predicate<? super T> predicate);
通常情况下,filter函数中lambda表达式为一次性使用的谓词逻辑。如果我们的谓词逻辑需要被多处、多场景、多代码中使用,通常将它抽取出来单独定义到它所限定的主语实体中。
比如:将下面的谓词逻辑定义在Employee实体class中。
public static Predicate<Employee> ageGreaterThan70 = x -> x.getAge() >70;
public static Predicate<Employee> genderM = x -> x.getGender().equals("M");
List<Employee> filtered = employees.stream()
.filter(Employee.ageGreaterThan70.and(Employee.genderM))
.collect(Collectors.toList());
List<Employee> filtered = employees.stream()
.filter(Employee.ageGreaterThan70.or(Employee.genderM))
.collect(Collectors.toList());
List<Employee> filtered = employees.stream()
.filter(Employee.ageGreaterThan70.or(Employee.genderM).negate())
.collect(Collectors.toList());
map
函数作用就是针对管道流中的每一个数据元素进行某种操作,然后返回一个新流。
map函数处理数据。如下:
// @return the new stream
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
List<String> alpha = Arrays.asList("Monkey", "Lion", "Giraffe", "Lemur");
// 使用Stream管道流
List<String> collect = alpha.stream().map(String::toUpperCase).collect(Collectors.toList());
//上面使用了方法引用,和下面的lambda表达式语法效果是一样的
//List<String> collect = alpha.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());
map()函数转换数据的类型。如下:
alpha.stream()
.map(String::length)
.forEach(System.out::println);
Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.mapToInt(String::length)
.forEach(System.out::println);
mapToInt():接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream
mapToLong():接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream
maoToDouble():接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream
map函数处理对象数据格式转化。如下:
public static void main(String[] args){
Employee e1 = new Employee(1,23,"M","Rick","Beethovan");
Employee e2 = new Employee(2,13,"F","Martina","Hengis");
Employee e3 = new Employee(3,43,"M","Ricky","Martin");
Employee e4 = new Employee(4,26,"M","Jon","Lowman");
Employee e5 = new Employee(5,19,"F","Cristine","Maria");
Employee e6 = new Employee(6,15,"M","David","Feezor");
Employee e7 = new Employee(7,68,"F","Melissa","Roy");
Employee e8 = new Employee(8,79,"M","Alex","Gussin");
Employee e9 = new Employee(9,15,"F","Neetu","Singh");
Employee e10 = new Employee(10,45,"M","Naveen","Jain");
List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10);
// 使用map方式
List<Employee> maped = employees.stream()
.map(e -> {
e.setAge(e.getAge() + 1);
e.setGender(e.getGender().equals("M") ? "male" : "female");
return e;
}).collect(Collectors.toList());
// 使用peek方式,可以省掉return
List<Employee> maped = employees.stream()
.peek(e -> {
e.setAge(e.getAge() + 1);
e.setGender(e.getGender().equals("M") ? "male" : "female");
}).collect(Collectors.toList());
System.out.println(maped);
}
由于map的参数e就是返回值,所以可以用peek函数。peek函数是一种特殊的map函数,当函数没有返回值或者参数就是返回值的时候可以使用peek函数。
map可以对管道流中的数据进行转换操作,但是如果管道中还有管道该如何处理?即:如何处理二维数组及二维集合类。实现一个简单的需求:将“hello”,“world”两个字符串组成的集合,元素的每一个字母打印出来。如果不用Stream我们怎么写?写2层for循环,第一层遍历字符串,并且将字符串拆分成char数组,第二层for循环遍历char数组。
List<String> words = Arrays.asList("hello", "word");
words.stream()
.map(w -> Arrays.stream(w.split(""))) //[[h,e,l,l,o],[w,o,r,l,d]]
.forEach(System.out::println);
输出结果:
java.util.stream.ReferencePipeline$Head@3551a94
java.util.stream.ReferencePipeline$Head@531be3c5
说明:Arrays.stream() 返回的是一个新的stream
用map方法是做不到的,这个需求用map方法无法实现。map只能针对一维数组进行操作,数组里面还有数组,管道里面还有管道,它是处理不了每一个元素的。
flatMap可以理解为将若干个子管道中的数据全都平面展开到父管道中进行处理。
words.stream()
.flatMap(w -> Arrays.stream(w.split(""))) // [h,e,l,l,o,w,o,r,l,d]
.forEach(System.out::print);
输出结果:helloword
List<String> limitN = Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.limit(2)
.collect(Collectors.toList());
List<String> skipN = Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.skip(2)
.collect(Collectors.toList());
使用distinct
方法对管道中的元素去重,涉及到去重就一定涉及到元素之间的比较,distinct方法时调用Object的equals方法进行对象的比较的,如果你有自己的比较规则,可以重写equals方法。
Stream流中的distinct方法对于基本数据类型可以直接去重,但是对于自定义类型,我们需要重写hashCode和equals方法。
List<String> uniqueAnimals = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion")
.distinct()
.collect(Collectors.toList());
说明:产生一个新流,其按指定顺序排序。
默认的情况下,sorted是按照字母的自然顺序进行排序。如下代码的排序结果是:[Giraffe, Lemur, Lion, Monkey]
,字数按顺序G在L前面,L在M前面。第一位无法区分顺序,就比较第二位字母。
List<String> alphabeticOrder = Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.sorted()
.collect(Collectors.toList());
//自然排序
List<Integer> list = Arrays.asList(1, 3,24, 5, 6, 8, 23, 45, 72, 16);
Stream<Integer> stream = list.stream();
stream.sorted().forEach(System.out::println);
//对象排序:对象排序可以先实现comparable接口,或者直接指定
List<Student> studentList = getStudents();
//实现comparable接口
studentList.stream().sorted().forEach(System.out::println);
//直接指定
studentList.stream().sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).forEach(System.out::println);
List<String> list = Arrays.asList("c", "e", "a", "d", "b");
list.stream().sorted((s1, s2) -> s1.compareTo(s2)).forEach(System.out::println);
@Test
public void test4() {
List<Integer> list = Lists.newArrayList();
// 通过List接口直接获取并行流
Stream<Integer> integerStream = list.parallelStream();
// 将已有的串行流转换为并行流
Stream<Integer> integerStream1 = Stream.of(1, 2, 3).parallel();
}
parallel()
函数表示对管道中的元素进行并行处理,而不是串行处理。但是这样就有可能导致管道流中后面的元素先处理,前面的元素后处理,也就是元素的顺序无法保证。
总结:
1. Stream并行流计算 >> 普通for循环 ~= Stream串行流计算 (之所以用两个大于号,你细品)
2. 数据容量越大,Stream流的执行效率越高
3. Stream并行流计算通常能够比较好的利用CPU的多核优势。CPU核心越多,Stream并行流计算效率越高
/**
* 并行流 安全(同步锁)
*/
@Test
public void test5() {
List<Integer> listNew = Lists.newArrayList();
Object obj = new Object();
IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
synchronized (obj) {
listNew.add(i);
}
});
System.out.println(listNew.size());
}
/**
* 并行流 安全(Collections.synchronizedList(listNew))
*/
@Test
public void test6() {
List<Integer> listNew = Lists.newArrayList();
List<Integer> safeList = Collections.synchronizedList(listNew);
IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
safeList.add(i);
});
System.out.println(safeList.size());
}
/**
* 并行流 安全(Vector)
*/
@Test
public void test7() {
List<Integer> listNew = new Vector<>();
IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
listNew.add(i);
});
System.out.println(listNew.size());
}
/**
* 并行流 安全
*/
@Test
public void test8() {
List list = IntStream.rangeClosed(1, 1000).parallel().boxed().collect(Collectors.toList());
System.out.println(list.size());
}
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、 Integer,甚至是 void 。
List<Student> list = getStudents();
//判断所有学生年龄是否都大于12岁
boolean allMatch = list.stream().allMatch(item -> item.getAge() > 12);
//判断是否存在学生的年龄大于12岁
boolean anyMatch = list.stream().anyMatch(item -> item.getAge() > 20);
//判断是否有学生叫"小李"
boolean noneMatch = list.stream().noneMatch(item -> item.getName().equals("小李"));
//查找第一个年龄大于20的学生
//findFirst用于查找第一个符合“匹配规则”的元素,返回值为Optional
Optional<Student> first = list.stream().filter(e -> e.getAge() > 20).findFirst();
//查找学生数据
long count = list.stream().count();
long count1 = list.stream().filter(item -> item.getScore() > 90).count();
//查找当前流中的元素
//findAny用于查找任意一个符合“匹配规则”的元素,返回值为Optional
Optional<Student> any = list.stream().findAny();
//查找学生最高的分数:Student实现了comparable接口的话,可以直接比较
Stream<Double> doubleStream = list.stream().map(item -> item.getScore());
doubleStream.max(Double::compareTo);
Stream API为我们提供了Stream.reduce
用来实现集合元素的归约。reduce函数有三个参数:
Identity
标识:一个元素,它是归约操作的初始值,如果流为空,则为默认结果。Accumulator
累加器:具有两个参数的函数:归约运算的部分结果和流的下一个元素。阶段累加结果作为累加器的第一个参数;集合遍历元素作为累加器的第二个参数。
reduce初始值为0,累加器可以是lambda表达式,也可以是方法引用。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers.stream().reduce(0, (subtotal, element) -> subtotal + element);
System.out.println(result); //21
int result = numbers.stream().reduce(0, Integer::sum);
System.out.println(result); //21
案例:
@Test
public void test() {
// 第一次的时候会将默认值给x;之后每次会将上一次操作结果赋值给x,y就是每次从数据中获取元素
Integer sum = Stream.of(4, 5, 3, 9).reduce(0, (x, y) -> {
System.out.println("x: " + x + " --- y: " + y);
return x + y;
});
System.out.println(sum);
// 取出数据元素中的最大值
Integer max = Stream.of(4, 5, 3, 9).reduce(0, (x, y) -> {
return x > y ? x : y;
});
System.out.println(max);
}
x: 0 --- y: 4
x: 4 --- y: 5
x: 9 --- y: 3
x: 12 --- y: 9
21
9
只要累加器参数类型能够匹配,可以对任何类型的集合进行归约计算。
List<String> letters = Arrays.asList("a", "b", "c", "d", "e");
String result = letters.stream().reduce("", (partialString, element) -> partialString + element);
System.out.println(result); //abcde
String result = letters.stream().reduce("", String::concat);
System.out.println(result); //ancde
计算所有的员工的年龄总和。
Employee e1 = new Employee(1,23,"M","Rick","Beethovan");
Employee e2 = new Employee(2,13,"F","Martina","Hengis");
Employee e3 = new Employee(3,43,"M","Ricky","Martin");
Employee e4 = new Employee(4,26,"M","Jon","Lowman");
Employee e5 = new Employee(5,19,"F","Cristine","Maria");
Employee e6 = new Employee(6,15,"M","David","Feezor");
Employee e7 = new Employee(7,68,"F","Melissa","Roy");
Employee e8 = new Employee(8,79,"M","Alex","Gussin");
Employee e9 = new Employee(9,15,"F","Neetu","Singh");
Employee e10 = new Employee(10,45,"M","Naveen","Jain");
List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10);
Integer total = employees.stream().map(Employee::getAge).reduce(0, Integer::sum);
System.out.println(total); //346
对于大数据量的集合元素归约计算,更能体现出Stream并行流计算的威力。
在进行并行流计算的时候,可能会将集合元素分成多个组计算。为了更快的将分组计算结果累加,可以使用合并器。
Integer total2 = employees
.parallelStream()
.map(Employee::getAge)
.reduce(0, Integer::sum, Integer::sum); //注意这里reduce方法有三个参数
System.out.println(total); //346
如果我们只是希望将Stream管道流的处理结果打印出来,而不是进行类型转换,我们就可以使用forEach()方法或forEachOrdered()方法。
Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion")
.parallel()
.forEach(System.out::println); // 遍历流中的数据
Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion")
.parallel()
.forEachOrdered(System.out::println);
将管道流处理结果在转换成集合类。
通过Collectors.toSet()
方法收集Stream的处理结果,将所有元素收集到Set集合中。
Set<String> collectToSet = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion") .collect(Collectors.toSet());
//最终collectToSet 中的元素是:[Monkey, Lion, Giraffe, Lemur],注意Set会去重。
可以将元素收集到List
使用toList()
收集器中。
List<String> collectToList = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion").collect(Collectors.toList());
// 最终collectToList中的元素是: [Monkey, Lion, Giraffe, Lemur, Lion]
通过toArray(String[]::new)方法收集Stream的处理结果,将所有元素收集到字符串数组中。
String[] toArray = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion").toArray(String[]::new);
//最终toArray字符串数组中的元素是: [Monkey, Lion, Giraffe, Lemur, Lion]
使用Collectors.toMap()
方法将数据元素收集到Map里面。但是出现一个问题:那就是管道中的元素是作为key,还是作为value。我们用到了一个Function.identity()
方法,该方法很简单就是返回一个“ t -> t ”(输入就是输出的lambda表达式)。另外使用管道流处理函数distinct()
来确保Map键值的唯一性。
Map<String, Integer> toMap = Stream.of(
"Monkey", "Lion", "Giraffe", "Lemur", "Lion"
)
.distinct()
.collect(Collectors.toMap(
Function.identity(), //元素输入就是输出,作为key
s -> (int) s.chars().distinct().count()// 输入元素的不同的字母个数,作为value
));
// 最终toMap的结果是: {Monkey=6, Lion=4, Lemur=5, Giraffe=6}
Collectors.groupingBy用来实现元素的分组收集。下面的代码演示如何根据首字母将不同的数据元素收集到不同的List,并封装为Map。
Map<Character, List<String>> groupingByList = Stream.of(
"Monkey", "Lion", "Giraffe", "Lemur", "Lion"
)
.collect(Collectors.groupingBy(
s -> s.charAt(0) , //根据元素首字母分组,相同的在一组
// counting() // 加上这一行代码可以实现分组统计
));
// 最终groupingByList内的元素: {G=[Giraffe], L=[Lion, Lemur, Lion], M=[Monkey]}
// 如果加上counting() ,结果是: {G=1, L=3, M=1}
这是该过程的说明:groupingBy第一个参数作为分组条件,第二个参数是子收集器。
多级分组:
@Test
public void test() {
Stream.of(
new Employee(1, 23, "M", "Rick", "Beethovan"),
new Employee(2, 25, "W", "Rick", "Beethovan"),
new Employee(3, 13, "M", "Rick", "Beethovan"),
new Employee(4, 26, "W", "Rick", "Beethovan"),
new Employee(5, 43, "M", "Rick", "Beethovan")
).collect(Collectors.groupingBy(Employee::getGender, Collectors.groupingBy(p -> p.getAge() > 18 ? "成年" : "未成年"))).forEach((k, v) -> {
System.out.println(k);
v.forEach((k1, v1) -> {
System.out.println("\t" + k1 + "=" + v1);
});
});
}
W
成年=[com.java.master.Stream.Employee@2ac1fdc4, com.java.master.Stream.Employee@5f150435]
M
未成年=[com.java.master.Stream.Employee@1c53fd30]
成年=[com.java.master.Stream.Employee@50cbc42f, com.java.master.Stream.Employee@75412c2f]
向所需Collection类型提供构造函数的方式。
LinkedList<String> collectToCollection = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion").collect(Collectors.toCollection(LinkedList::new));
//最终collectToCollection中的元素是: [Monkey, Lion, Giraffe, Lemur, Lion]
注意:代码中使用了LinkedList::new
,实际是调用LinkedList的构造函数,将元素收集到LinkedList
。当然你还可以使用诸如LinkedHashSet::new
和PriorityQueue::new
将数据元素收集为其他的集合类型,这样就比较通用了。
Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表和一个false列表。
@Test
public void test2() {
Stream.of(
new Employee(1, 23, "M", "Rick", "Beethovan"),
new Employee(2, 25, "W", "Rick", "Beethovan"),
new Employee(3, 13, "M", "Rick", "Beethovan"),
new Employee(4, 26, "W", "Rick", "Beethovan"),
new Employee(5, 43, "M", "Rick", "Beethovan")
).collect(Collectors.partitioningBy(p -> p.getAge() > 18)).forEach((k, v) -> {
System.out.println(k + "\t" + v);
});
}
false [com.java.master.Stream.Employee@c39f790]
true [com.java.master.Stream.Employee@71e7a66b, com.java.master.Stream.Employee@2ac1fdc4, com.java.master.Stream.Employee@5f150435, com.java.master.Stream.Employee@1c53fd30]
/**
* joining
*/
@Test
public void test3() {
String re = Stream.of(
new Employee(1, 23, "M", "Rick", "Beethovan"),
new Employee(2, 25, "W", "Rick", "Beethovan"),
new Employee(3, 13, "M", "Rick", "Beethovan"),
new Employee(4, 26, "W", "Rick", "Beethovan"),
new Employee(5, 43, "M", "Rick", "Beethovan")
).map( m -> m.getFirstName()).collect(Collectors.joining(" -- "));
System.out.println(re);
}
// 将两个流进行合并
List<String> list = Arrays.asList("a", "b");
List<String> list2 = Arrays.asList("c", "d");
Stream<String> concatStream = Stream.concat(list.stream(), list2.stream());
concatStream.forEach(System.out::println);
输出:
a
b
c
d
// 管道中元素数据累加结果sum: 6
int sum = IntStream.of(1, 2, 3).sum();
//管道中元素数据平均值average: OptionalDouble[2.0]
OptionalDouble average = IntStream.of(1, 2, 3).average();
//管道中元素数据最大值max: 3
int max = IntStream.of(1, 2, 3).max().orElse(0);
// 全面的统计结果statistics: IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}
IntSummaryStatistics statistics = IntStream.of(1, 2, 3).summaryStatistics();
// 获取最大值
Optional optional = Stream.of(4, 5, 3, 9).collect(Collectors.maxBy((x, y) -> x - y));
System.out.println(optional);
// 获取最小值
Optional optional = Stream.of(4, 5, 3, 9).collect(Collectors.minBy((x, y) -> x - y));
System.out.println(optional);
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。