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());
System.out.println(list);
最终的输出结果是:[LEMUR, LION]
使用Stream.of()方法,将数组转换为管道流。
String[] array = {"Monkey", "Lion", "Giraffe", "Lemur"};
Stream<String> nameStrs2 = Stream.of(array);
Stream<String> nameStrs3 = Stream.of("Monkey", "Lion", "Giraffe", "Lemur");
通过调用集合类的stream()方法,将集合类对象转换为管道流。
List<String> list = Arrays.asList("Monkey", "Lion", "Giraffe", "Lemur");
Stream<String> streamFromList = list.stream();
Set<String> set = new HashSet<>(list);
Stream<String> streamFromSet = set.stream();
通过Files.lines方法将文本文件转换为管道流,下图中的Paths.get()方法作用就是获取文件,是Java NIO的API!
也就是说:我们可以很方便的使用Java Stream加载文本文件,然后逐行的对文件内容进行处理。
Stream<String> lines = Files.lines(Paths.get("file.txt"));
环境搭建
@Data
@AllArgsConstructor
@Builder
public class Employee {
private Integer id;
private Integer age; //年龄
private String gender; //性别
private String firstName;
private String lastName;
}
写一个测试类,这个测试类的内容也很简单,新建十个Employee 对象
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);
需要注意的是上面的filter传入了lambda表达式(之前的章节我们已经讲过了),表达过滤年龄大于70并且男性的Employee员工。输出如下:
[Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin)]
下面要说我们的重点了,通过之前的章节的讲解,我们已经知道lambda表达式表达的是一个匿名接口函数的实现。那具体到Stream.filter()中,它表达的是什么呢?看下图:可以看出它表达的是一个Predicate接口,在英语中这个单词的意思是:谓词
什么是谓词逻辑?
WHERE 和 AND 限定了主语employee是什么,那么WHERE和AND语句所代表的逻辑就是谓词逻辑
SELECT *
FROM employee
WHERE age > 70
AND gender = 'M'
通常情况下,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());
输出如下:
[Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin)]
List<Employee> filtered = employees.stream()
.filter(Employee.ageGreaterThan70.or(Employee.genderM))
.collect(Collectors.toList());
输出如下:实际上就是年龄大于70的和所有的男性(由于79的那位也是男性,所以就是所有的男性)
[Employee(id=1, age=23, gender=M, firstName=Rick, lastName=Beethovan), Employee(id=3, age=43, gender=M, firstName=Ricky, lastName=Martin), Employee(id=4, age=26, gender=M, firstName=Jon, lastName=Lowman), Employee(id=6, age=15, gender=M, firstName=David, lastName=Feezor), Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin), Employee(id=10, age=45, gender=M, firstName=Naveen, lastName=Jain)]
List<Employee> filtered = employees.stream()
.filter(Employee.ageGreaterThan70.or(Employee.genderM).negate())
.collect(Collectors.toList());
输出如下:把上一小节代码的结果取反,实际上就是所有的女性
[Employee(id=2, age=13, gender=F, firstName=Martina, lastName=Hengis), Employee(id=5, age=19, gender=F, firstName=Cristine, lastName=Maria), Employee(id=7, age=68, gender=F, firstName=Melissa, lastName=Roy), Employee(id=9, age=15, gender=F, firstName=Neetu, lastName=Singh)]
最简单的需求:将集合中的每一个字符串,全部转换成大写!
List<String> alpha = Arrays.asList("Monkey", "Lion", "Giraffe", "Lemur");
//不使用Stream管道流
List<String> alphaUpper = new ArrayList<>();
for (String s : alpha) {
alphaUpper.add(s.toUpperCase());
}
System.out.println(alphaUpper); //[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());
System.out.println(collect); //[MONKEY, LION, GIRAFFE, LEMUR]
所以map函数的作用就是针对管道流中的每一个数据元素进行转换操作。
map()函数不仅可以处理数据,还可以转换数据的类型。如下:
List<Integer> lengths = alpha.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths); //[6, 4, 7, 5]
Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.mapToInt(String::length)
.forEach(System.out::println);
输出如下:
6
4
7
5
除了mapToInt。还有maoToLong,mapToDouble等等用法
还是使用上一节中的Employee类,创建10个对象。需求如下:
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> maped = employees.stream()
.map(e -> {
e.setAge(e.getAge() + 1);
e.setGender(e.getGender().equals("M")?"male":"female");
return e;
}).collect(Collectors.toList());*/
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
words.stream()
.flatMap(w -> Arrays.stream(w.split(""))) // [h,e,l,l,o,w,o,r,l,d]
.forEach(System.out::println);
输出打印结果:
h
e
l
l
o
w
o
r
d
Stream管道流的基本操作。我们来回顾一下:
看下面的脑图,可以有更清晰的理解:
其实在程序员编程中,经常会接触到“有状态”,“无状态”,绝大部分的人都比较蒙。而且在不同的场景下,“状态”这个词的含义似乎有所不同。但是“万变不离其宗”,理解“状态”这个词在编程领域的含义,笔者教给大家几个关键点:
是不是更蒙了?举个例子,你就明白了
回到我们的Stream管道流
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方法。
List<String> uniqueAnimals = Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion")
.distinct()
.collect(Collectors.toList());
上面代码去重之后的结果是: [“Monkey”, “Lion”, “Giraffe”, “Lemur”]
默认的情况下,sorted是按照字母的自然顺序进行排序。如下代码的排序结果是:[Giraffe, Lemur, Lion, Monkey],字数按顺序G在L前面,L在M前面。第一位无法区分顺序,就比较第二位字母。
List<String> alphabeticOrder = Stream.of("Monkey", "Lion", "Giraffe", "Lemur")
.sorted()
.collect(Collectors.toList());
通常情况下,有状态和无状态操作不需要我们去关心。除非?:你使用了并行操作。
还是用班级按身高排队为例:班级有一个人负责排序,这个排序结果最后就会是正确的。那如果有2个、3个人负责按大小个排队呢?最后可能就乱套了。一个人只能保证自己排序的人的顺序,他无法保证其他人的排队顺序。
Stream.of("Monkey", "Lion", "Giraffe", "Lemur", "Lion")
.parallel()
.forEach(System.out::println);
parallel()函数表示对管道中的元素进行并行处理,而不是串行处理。但是这样就有可能导致管道流中后面的元素先处理,前面的元素后处理,也就是元素的顺序无法保证。
如果数据量比较小的情况下,不太能观察到,数据量大的话,就能观察到数据顺序是无法保证的。
Monkey
Lion
Lemur
Giraffe
Lion
通常情况下,parallel()能够很好的利用CPU的多核处理器,达到更好的执行效率和性能,建议使用。但是有些特殊的情况下,parallel并不适合:深入了解请看这篇文章: Parallel stream processing in Java 8 – performance of sequential vs. parallel stream processing
该文章中几个观点,说明并行操作的适用场景:
虽然在不同的场景、不同的数据结构、不同的硬件环境下。Stream流与for循环性能测试结果差异较大,甚至发生逆转。但是总体上而言:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>junitperf</artifactId>
<version>2.0.0</version>
</dependency>
测试用例一:
import com.github.houbb.junitperf.core.annotation.JunitPerfConfig;
import com.github.houbb.junitperf.core.report.impl.HtmlReporter;
import org.junit.jupiter.api.BeforeAll;
import java.util.Arrays;
import java.util.Random;
public class StreamIntTest {
public static int[] arr;
@BeforeAll
public static void init() {
arr = new int[500000000]; //5亿个随机Int
randomInt(arr);
}
@JunitPerfConfig( warmUp = 1000, reporter = {HtmlReporter.class})
public void testIntFor() {
minIntFor(arr);
}
@JunitPerfConfig( warmUp = 1000, reporter = {HtmlReporter.class})
public void testIntParallelStream() {
minIntParallelStream(arr);
}
@JunitPerfConfig( warmUp = 1000, reporter = {HtmlReporter.class})
public void testIntStream() {
minIntStream(arr);
}
private int minIntStream(int[] arr) {
return Arrays.stream(arr).min().getAsInt();
}
private int minIntParallelStream(int[] arr) {
return Arrays.stream(arr).parallel().min().getAsInt();
}
private int minIntFor(int[] arr) {
int min = Integer.MAX_VALUE;
for (int anArr : arr) {
if (anArr < min) {
min = anArr;
}
}
return min;
}
private static void randomInt(int[] arr) {
Random r = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = r.nextInt();
}
}
}
测试用例二:
import com.github.houbb.junitperf.core.annotation.JunitPerfConfig;
import com.github.houbb.junitperf.core.report.impl.HtmlReporter;
import org.junit.jupiter.api.BeforeAll;
import java.util.ArrayList;
import java.util.Random;
public class StreamStringTest {
public static ArrayList<String> list;
@BeforeAll
public static void init() {
list = randomStringList(1000000);
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void testMinStringForLoop(){
String minStr = null;
boolean first = true;
for(String str : list){
if(first){
first = false;
minStr = str;
}
if(minStr.compareTo(str)>0){
minStr = str;
}
}
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void textMinStringStream(){
list.stream().min(String::compareTo).get();
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void testMinStringParallelStream(){
list.stream().parallel().min(String::compareTo).get();
}
private static ArrayList<String> randomStringList(int listLength){
ArrayList<String> list = new ArrayList<>(listLength);
Random rand = new Random();
int strLength = 10;
StringBuilder buf = new StringBuilder(strLength);
for(int i=0; i<listLength; i++){
buf.delete(0, buf.length());
for(int j=0; j<strLength; j++){
buf.append((char)('a'+ rand.nextInt(26)));
}
list.add(buf.toString());
}
return list;
}
}
测试用例三:
import com.github.houbb.junitperf.core.annotation.JunitPerfConfig;
import com.github.houbb.junitperf.core.report.impl.HtmlReporter;
import org.junit.jupiter.api.BeforeAll;
import java.util.*;
import java.util.stream.Collectors;
public class StreamObjectTest {
public static List<Order> orders;
@BeforeAll
public static void init() {
orders = Order.genOrders(10);
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void testSumOrderForLoop(){
Map<String, Double> map = new HashMap<>();
for(Order od : orders){
String userName = od.getUserName();
Double v;
if((v=map.get(userName)) != null){
map.put(userName, v+od.getPrice());
}else{
map.put(userName, od.getPrice());
}
}
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void testSumOrderStream(){
orders.stream().collect(
Collectors.groupingBy(Order::getUserName,
Collectors.summingDouble(Order::getPrice)));
}
@JunitPerfConfig(duration = 10000, warmUp = 1000, reporter = {HtmlReporter.class})
public void testSumOrderParallelStream(){
orders.parallelStream().collect(
Collectors.groupingBy(Order::getUserName,
Collectors.summingDouble(Order::getPrice)));
}
}
class Order{
private String userName;
private double price;
private long timestamp;
public Order(String userName, double price, long timestamp) {
this.userName = userName;
this.price = price;
this.timestamp = timestamp;
}
public String getUserName() {
return userName;
}
public double getPrice() {
return price;
}
public long getTimestamp() {
return timestamp;
}
public static List<Order> genOrders(int listLength){
ArrayList<Order> list = new ArrayList<>(listLength);
Random rand = new Random();
int users = listLength/200;// 200 orders per user
users = users==0 ? listLength : users;
ArrayList<String> userNames = new ArrayList<>(users);
for(int i=0; i<users; i++){
userNames.add(UUID.randomUUID().toString());
}
for(int i=0; i<listLength; i++){
double price = rand.nextInt(1000);
String userName = userNames.get(rand.nextInt(users));
list.add(new Order(userName, price, System.nanoTime()));
}
return list;
}
@Override
public String toString(){
return userName + "::" + price;
}
}
在开始之前,我先卖个关子提一个问题:我们现在有一个Employee员工类。
@Data
@AllArgsConstructor
public class Employee {
private Integer id;
private Integer age; //年龄
private String gender; //性别
private String firstName;
private String lastName;
}
你知道怎么对一个Employee对象组成的List集合,先按照性别字段倒序排序,再按照年龄的倒序进行排序么?
如果您不知道4行代码以内的解决方案(其实是1行代码就可以实现,但笔者格式化为4行),我觉得您有必要一步步的看下去。
cities是一个字符串数组。注意london的首字母是小写的。
List<String> cities = Arrays.asList(
"Milan",
"london",
"San Francisco",
"Tokyo",
"New Delhi"
);
System.out.println(cities);
//[Milan, london, San Francisco, Tokyo, New Delhi]
cities.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(cities);
//[london, Milan, New Delhi, San Francisco, Tokyo]
cities.sort(Comparator.naturalOrder());
System.out.println(cities);
//[Milan, New Delhi, San Francisco, Tokyo, london]
同样我们可以把排序器Comparator用在Stream管道流中。
cities.stream().sorted(Comparator.naturalOrder()).forEach(System.out::println);
//Milan
//New Delhi
//San Francisco
//Tokyo
//london
在java 7我们是使用Collections.sort()接受一个数组参数,对数组进行排序。在java 8之后可以直接调用集合类的sort()方法进行排序。sort()方法的参数是一个比较器Comparator接口的实现类,Comparator接口的我们下一节再给大家介绍一下。
List<Integer> numbers = Arrays.asList(6, 2, 1, 4, 9);
System.out.println(numbers); //[6, 2, 1, 4, 9]
numbers.sort(Comparator.naturalOrder()); //自然排序
System.out.println(numbers); //[1, 2, 4, 6, 9]
numbers.sort(Comparator.reverseOrder()); //倒序排序
System.out.println(numbers); //[9, 6, 4, 2, 1]
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);
//默认是升序
employees.sort(Comparator.comparing(Employee::getAge));
//第二个参数可以传递一个比较器
//employees.sort(Comparator.comparing(Employee::getAge,(o1,o2)->{return o2-o1;}));
employees.forEach(System.out::println);
Employee(id=2, age=13, gender=F, firstName=Martina, lastName=Hengis)
Employee(id=6, age=15, gender=M, firstName=David, lastName=Feezor)
Employee(id=9, age=15, gender=F, firstName=Neetu, lastName=Singh)
Employee(id=5, age=19, gender=F, firstName=Cristine, lastName=Maria)
Employee(id=1, age=23, gender=M, firstName=Rick, lastName=Beethovan)
Employee(id=4, age=26, gender=M, firstName=Jon, lastName=Lowman)
Employee(id=3, age=43, gender=M, firstName=Ricky, lastName=Martin)
Employee(id=10, age=45, gender=M, firstName=Naveen, lastName=Jain)
Employee(id=7, age=68, gender=F, firstName=Melissa, lastName=Roy)
Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin)
如果我们希望List按照年龄age的倒序排序,就使用reversed()方法。如:
employees.sort(Comparator.comparing(Employee::getAge).reversed());
下面这段代码先是按性别的倒序排序,再按照年龄的倒序排序。
employees.sort(
Comparator.comparing(Employee::getGender)
.thenComparing(Employee::getAge)
.reversed()
);
employees.forEach(System.out::println);
//都是正序 ,不加reversed
//都是倒序,最后面加一个reserved
//先是倒序(加reserved),然后正序
//先是正序(加reserved),然后倒序(加reserved)
细心的朋友可能注意到:我们只用了一个reversed()倒序方法,这个和SQL的表述方式不太一样。
排序结果如下:
Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin)
Employee(id=10, age=45, gender=M, firstName=Naveen, lastName=Jain)
Employee(id=3, age=43, gender=M, firstName=Ricky, lastName=Martin)
Employee(id=4, age=26, gender=M, firstName=Jon, lastName=Lowman)
Employee(id=1, age=23, gender=M, firstName=Rick, lastName=Beethovan)
Employee(id=6, age=15, gender=M, firstName=David, lastName=Feezor)
Employee(id=7, age=68, gender=F, firstName=Melissa, lastName=Roy)
Employee(id=5, age=19, gender=F, firstName=Cristine, lastName=Maria)
Employee(id=9, age=15, gender=F, firstName=Neetu, lastName=Singh)
Employee(id=2, age=13, gender=F, firstName=Martina, lastName=Hengis)
所谓的函数式接口,实际上就是接口里面只能有一个抽象方法的接口。我们上一节用到的Comparator接口就是一个典型的函数式接口,它只有一个抽象方法compare。
只有一个抽象方法?那上图中的equals方法不是也没有函数体么?不急,和我一起往下看!
甚至可以说:函数式接口是专门为lambda表达式准备的,lambda表达式是只实现接口中唯一的抽象方法的匿名实现类。
顺便讲一下default关键字,在java8之前
这就导致一个问题:当一个接口有很多的实现类的时候,修改这个接口就变成了一个非常麻烦的事,需要修改这个接口的所有实现类。
这个问题困扰了java工程师许久,不过在java8中这个问题得到了解决,没错就是default方法
java.lang.Runnable,
java.util.Comparator,
java.util.concurrent.Callable
java.util.function包下的接口,如Consumer、Predicate、Supplier等
我们自定义一个排序器,实现compare函数(函数式接口Comparator唯一的抽象方法)。返回0表示元素相等,-1表示前一个元素小于后一个元素,1表示前一个元素大于后一个元素。这个规则和java 8之前没什么区别。
下面代码用自定义接口实现类的的方式实现:按照年龄的倒序排序!
employees.sort(new Comparator<Employee>() {
@Override
public int compare(Employee em1, Employee em2) {
if(em1.getAge() == em2.getAge()){
return 0;
}
return em1.getAge() - em2.getAge() > 0 ? -1:1;
}
});
employees.forEach(System.out::println);
最终的打印结果如下,按照年龄的自定义规则进行排序。
Employee(id=8, age=79, gender=M, firstName=Alex, lastName=Gussin)
Employee(id=7, age=68, gender=F, firstName=Melissa, lastName=Roy)
Employee(id=10, age=45, gender=M, firstName=Naveen, lastName=Jain)
Employee(id=3, age=43, gender=M, firstName=Ricky, lastName=Martin)
Employee(id=4, age=26, gender=M, firstName=Jon, lastName=Lowman)
Employee(id=1, age=23, gender=M, firstName=Rick, lastName=Beethovan)
Employee(id=5, age=19, gender=F, firstName=Cristine, lastName=Maria)
Employee(id=9, age=15, gender=F, firstName=Neetu, lastName=Singh)
Employee(id=6, age=15, gender=M, firstName=David, lastName=Feezor)
Employee(id=2, age=13, gender=F, firstName=Martina, lastName=Hengis)
这段代码如果以lambda表达式简写。箭头左侧是参数,右侧是函数体,参数类型和返回值根据上下文自动判断。如下:
employees.sort((em1,em2) -> {
if(em1.getAge() == em2.getAge()){
return 0;
}
return em1.getAge() - em2.getAge() > 0 ? -1:1;
});
employees.forEach(System.out::println);
在我们对数组或者集合类进行操作的时候,经常会遇到这样的需求,比如:
这些需求如果用for循环去写的话,还是比较麻烦的,需要使用到for循环和break!本节就介绍一个如何用Stream API来实现“查找与匹配”。
如果我们不用Stream API实现,查找员工列表中是否包含年龄大于70的员工?代码如下:
boolean isExistAgeThan70 = false;
for(Employee employee:employees){
if(employee.getAge() > 70){
isExistAgeThan70 = true;
break;
}
}
如果我们使用Stream API就是下面的一行代码,其中使用到了我们之前学过的"谓词逻辑"。
boolean isExistAgeThan70 = employees.stream().anyMatch(Employee.ageGreaterThan70);
将谓词逻辑换成lambda表达式也可以,代码如下:
boolean isExistAgeThan72 = employees.stream().anyMatch(e -> e.getAge() > 72);
所以,我们介绍了第一个匹配规则函数:anyMatch,判断Stream流中是否包含某一个“匹配规则”的元素。这个匹配规则可以是lambda表达式或者谓词。
boolean isExistAgeThan10 = employees.stream().allMatch(e -> e.getAge() > 10);
boolean isExistAgeLess18 = employees.stream().noneMatch(e -> e.getAge() < 18);
从列表中按照顺序查找第一个年龄大于40的员工。
Optional<Employee> employeeOptional
= employees.stream().filter(e -> e.getAge() > 40).findFirst();
System.out.println(employeeOptional.get());
打印结果
Employee(id=3, age=43, gender=M, firstName=Ricky, lastName=Martin)
Optional类代表一个值存在或者不存在。在java8中引入,这样就不用返回null了。
Stream API为我们提供了Stream.reduce
用来实现集合元素的归约。reduce函数有三个参数:
注意观察上面的图,我们先来理解累加器:
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
//如果不指定起始值,但是这样可能数据为空,因此会被封装为一个Optional对象
Optional<Integer> res = numbers.stream().reduce((subtotal, element) -> subtotal + element);
System.out.println(res.get());
int result1 = numbers
.stream()
.reduce(0, Integer::sum);
System.out.println(result); //21
不仅可以归约Integer类型,只要累加器参数类型能够匹配,可以对任何类型的集合进行归约计算。
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 result1 = 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
除了使用map函数实现类型转换后的集合归约,我们还可以用Combiner合并器来实现,这里第一次使用到了Combiner合并器。
因为Stream流中的元素是Employee,累加器的返回值是Integer,所以二者的类型不匹配。这种情况下可以使用Combiner合并器对累加器的结果进行二次归约,相当于做了类型转换。
Integer total3 = employees.stream()
.reduce(0,(totalAge,emp) -> totalAge + emp.getAge(),Integer::sum); //注意这里reduce方法有三个参数
System.out.println(total3); //346
对于大数据量的集合元素归约计算,更能体现出Stream并行流计算的威力。
在进行并行流计算的时候,可能会将集合元素分成多个组计算。为了更快的将分组计算结果累加,可以使用合并器。
Integer total2 = employees
.parallelStream()
.map(Employee::getAge)
.reduce(0,Integer::sum,Integer::sum); //注意这里reduce方法有三个参数
System.out.println(total); //346
在开始学习之前,仍然有必要回顾一下我们之前给大家讲过的一个例子:
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());
System.out.println(list);
下面要给大家介绍第三阶段:对管道流处理结果都可以做哪些操作呢?下面开始吧!
如果我们只是希望将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);
Monkey
Lion
Giraffe
Lemur
Lion
java Stream 最常见的用法就是:一将集合类转换成管道流,二对管道流数据处理,三将管道流处理结果在转换成集合类。那么collect()方法就为我们提供了这样的功能:将管道流处理结果在转换成集合类。
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]
上面为大家介绍的元素收集方式,都是专用的。比如使用Collectors.toSet()收集为Set类型集合;使用Collectors.toList()收集为List类型集合。那么,有没有一种比较通用的数据元素收集方式,将数据收集为任意的Collection接口子类型。
所以,这里就像大家介绍一种通用的元素收集方式,你可以将数据元素收集到任意的Collection类型:即向所需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的构造函数,将元素收集到Linked List。当然你还可以使用诸如LinkedHashSet::new和PriorityQueue::new将数据元素收集为其他的集合类型,这样就比较通用了。
通过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() // 加上这一行代码可以实现分组统计,这是Collectors里面的静态方法
));
// 最终groupingByList内的元素: {G=[Giraffe], L=[Lion, Lemur, Lion], M=[Monkey]}
//如果加上counting() ,结果是: {G=1, L=3, M=1}
这是该过程的说明:groupingBy第一个参数作为分组条件,第二个参数是子收集器。
boolean containsTwo = IntStream.of(1, 2, 3).anyMatch(i -> i == 2);
// 判断管道中是否包含2,结果是: true
long nrOfAnimals = Stream.of(
"Monkey", "Lion", "Giraffe", "Lemur"
).count();
// 管道中元素数据总计结果nrOfAnimals: 4
int sum = IntStream.of(1, 2, 3).sum();
// 管道中元素数据累加结果sum: 6
OptionalDouble average = IntStream.of(1, 2, 3).average();
//管道中元素数据平均值average: OptionalDouble[2.0]
int max = IntStream.of(1, 2, 3).max().orElse(0);
//管道中元素数据最大值max: 3
IntSummaryStatistics statistics = IntStream.of(1, 2, 3).summaryStatistics();
// 全面的统计结果statistics: IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}
使用Java 8 Streams,我们可以按键和按值对映射进行排序。下面是它的工作原理:
sorted()方法以Comparator作为参数,从而可以按任何类型的值对Map进行排序
在学习Map排序之前,有必要讲一下HashMap的merge()函数,该函数应用场景就是当Key重复的时候,如何处理Map的元素值。
这个函数有三个参数:
String k = "key";
HashMap<String, Integer> map = new HashMap<String, Integer>() {{
put(k, 1);
}};
map.merge(k, 2, (oldVal, newVal) -> oldVal + newVal);
看上面一段代码,我们首先创建了一个HashMap,并往里面放入了一个键值为k:1的元素。当我们调用merge函数,往map里面放入k:2键值对的时候,k键发生重复,就执行后面的lambda表达式。表达式的含义是:返回旧值oldVal加上新值newVal(1+2),现在map里面只有一项元素那就是k:3。
其实lambda表达式很简单:表示匿名函数,箭头左侧是参数,箭头右侧是函数体。函数的参数类型和返回值,由代码上下文来确定。
下面一个例子使用Java 8 Stream按Map的键进行排序:
// 创建一个Map,并填入数据
Map<String, Integer> codes = new HashMap<>();
codes.put("United States", 1);
codes.put("Germany", 49);
codes.put("France", 33);
codes.put("China", 86);
codes.put("Pakistan", 92);
// 按照Map的键进行排序
Map<String, Integer> sortedMap = codes.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.collect(
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldVal, newVal) -> oldVal,
LinkedHashMap::new
)
);
// 将排序后的Map打印
sortedMap.entrySet().forEach(System.out::println);
看上文中第二段代码:
上面的程序将在控制台上打印以下内容,键(国家/地区名称)以自然字母顺序排序:
China=86
France=33
Germany=49
Pakistan=92
United States=1
请注意使用LinkedHashMap来存储排序的结果以保持顺序。默认情况下,Collectors.toMap()返回HashMap。HashMap不能保证元素的顺序。
当然,您也可以使用Stream API按其值对Map进行排序:
Map<String, Integer> sortedMap2 = codes.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldVal, newVal) -> oldVal,
LinkedHashMap::new));
sortedMap2.entrySet().forEach(System.out::println);
这是显示Map按值排序的输出:
United States=1
France=33
Germany=49
China=86
Pakistan=92
大家可能都知道TreeMap内的元素是有顺序的,所以利用TreeMap排序也是可取的一种方法。您需要做的就是创建一个TreeMap对象,并将数据从HashMap put到TreeMap中,非常简单:
// 将 `HashMap` 转为 `TreeMap`
Map<String, Integer> sorted = new TreeMap<>(codes);
这是输出:
China=86
France=33
Germany=49
Pakistan=92
United States=1
如上所示,键(国家/地区名称)以自然字母顺序排序。
首先自定义对象必须要实现Comparable接口,因此排序实际调用的就是compareTo方法
@NoArgsConstructor
@Builder
@Data
@AllArgsConstructor
public class Employee implements Comparable<Employee> {
private String name;
private Integer age;
@Override
public int compareTo(Employee o) {
return age-o.getAge();
}
}
Employee e1 = Employee.builder().age(18).name("大忽悠").build();
Employee e2 = Employee.builder().age(20).name("大忽悠").build();
Employee e3 = Employee.builder().age(19).name("大忽悠").build();
Map<String,Employee> map=new HashMap<>();
map.put("1",e1);
map.put("2",e2);
map.put("3",e3);
Optional<Map.Entry<String, Employee>> first = map.entrySet().stream().sorted(Map.Entry.comparingByValue())
.findFirst();
System.out.println(first.get().getKey()+" "+first.get().getValue());
//输出: 1 Employee(name=大忽悠, age=18)
最后:上文代码
String k = "key";
HashMap<String, Integer> map = new HashMap<String, Integer>() {{
put(k, 1);
}};
map.merge(k, 2, (oldVal, newVal) -> oldVal + newVal);
// 创建一个Map,并填入数据
Map<String, Integer> codes = new HashMap<>();
codes.put("United States", 1);
codes.put("Germany", 49);
codes.put("France", 33);
codes.put("China", 86);
codes.put("Pakistan", 92);
// 按照Map的键进行排序
Map<String, Integer> sortedMap = codes.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.collect(
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldVal, newVal) -> oldVal,
LinkedHashMap::new
)
);
// 将排序后的Map打印
sortedMap.entrySet().forEach(System.out::println);
// sort the map by values
Map<String, Integer> sorted = codes.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldVal, newVal) -> oldVal,
LinkedHashMap::new));
sorted.entrySet().forEach(System.out::println);
本文中为大家介绍使用java8 Stream API逐行读取文件,以及根据某些条件过滤文件内容
在此示例中,我将按行读取文件内容并在控制台打印输出。
Path filePath = Paths.get("c:/temp", "data.txt");
//try-with-resources语法,不用手动的编码关闭流
try (Stream<String> lines = Files.lines( filePath ))
{
lines.forEach(System.out::println);
}
catch (IOException e)
{
e.printStackTrace();//只是测试用例,生产环境下不要这样做异常处理
}
上面的程序输出将在控制台中逐行打印文件的内容。
Never
store
password
except
in mind.
在此示例中,我们将文件内容读取为Stream。然后,我们将过滤其中包含单词"password"的所有行。
Path filePath = Paths.get("c:/temp", "data.txt");
try (Stream<String> lines = Files.lines(filePath)){
List<String> filteredLines = lines
.filter(s -> s.contains("password"))
.collect(Collectors.toList());
filteredLines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();//只是测试用例,生产环境下不要这样做异常处理
}
程序输出。
password
我们将读取给定文件的内容,并检查是否有任何一行包含"password"然后将其打印出来。
Java 7之前的版本,我们可以使用FileReader方式进行逐行读取文件。
private static void readLinesUsingFileReader() throws IOException
{
File file = new File("c:/temp/data.txt");
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
while((line = br.readLine()) != null)
{
if(line.contains("password")){
System.out.println(line);
}
}
br.close();
fr.close();
}
Java8 forEach是一个工具方法用于遍历集合,比如: (list, set or map) 和stream流(java8 提供的另外一个特性),然后对集合中的每一个元素执行特定的操作。
下面的代码片段显示了Iterable接口forEach方法的默认实现。我们可以通过这个方法去遍历除了Map之外的所有集合类。
上面的方法对Iterable的每个元素执行操作,直到所有元素都已处理或该操作引发异常。“ action”用来表示一个接受单个输入参数且不返回结果的操作。它是“Consumer”接口的一个实例
我们可以通过实现Consumer接口的accept方法,实现自己对集合元素需要做的自定义操作。比如:下面的代码是实现集合中字符串转大写并打印出来的操作。
Map.forEach()方法对map中的每一个entry执行特定的操作,直到所有map的entry被处理完成或者抛出异常。
使用Map.forEach() 方法
与List等集合类遍历类似,我们可以自定义一个biconsumer action去处理key-value键值对.
Program output.
Key is : A
Value is : 1
Key is : B
Value is : 2
Key is : C
Value is : 3
下面的代码使用forEach遍历 List中的所有偶数。
输出
2
4
A=1
B=2
C=3
A
B
C
1
2
3