一、Stream流的介绍
1、介绍:
Stream流是在jdk8当中提供的一个新特性,它可以去操作数组或者集合,并且是把里面的数据像流一样的形式来进行操作。
如果流的一系列的操作要成功触发并生效,那必须需要有终结操作,一个流只能做一次终结操作,如果没有终结操作,中间操作根本不会被执行,所有的中间操作返回的都是一个Stream对象,所以可以一直链式编程。
二、Stream流的创建方式
对于java来说,集合分为两大类:
一个是单列集合,它的父接口是Connection, 另外一个是双列集合,它的父接口是map。
1、单列集合:
- 集合对象.stream()
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();
List<String> list = Arrays.asList("a", "b", "c", "d");
Stream<String> stream = list.stream();
- 数组: Arrays.stream(数组) 或者使用 Stream.of(数组) 来创建
Integer[] arr = {1,2,3,4,5} ;
Stream<Integer> stream1 = Arrays.stream(arr);
Stream<Integer> stream2 = Stream.of(arr);
2、双列集合:
转换成单列集合后再创建
-
ps: 关于Map的四种遍历方式、entrySet()、Stream流中的Map举例可以看我这篇文章:
@Test
public void Test08(){
Map<String, Integer> map = new HashMap<>();
map.put("蜡笔小新",5);
map.put("小黑子",22);
map.put("日向翔阳",16);
Set<Map.Entry<String,Integer>> entries = map.entrySet();
Stream<Map.Entry<String,Integer>> stream1 = entries.stream();
Stream<Map.Entry<String,Integer>> stream2 = map.entrySet().stream();
}
我们创建完流后,还要经过一系列中间操作,最后还要加上终结操作,这样的话这个流才能真正的使用起来。
三、Stream流"中间操作"常用的API
如果流的一系列的操作要成功触发并生效,那必须需要有终结操作,一个流只能做一次终结操作,如果没有终结操作,中间操作根本不会被执行,所有的中间操作返回的都是一个Stream对象,所以可以一直链式编程
Stream 有很多的 API 供我们使用,如下:
根据操作分类:
0、案例准备:
实体类Author:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 去重
public class Author {
private Long id;
private String name;
private Integer age;
private String intro;
private List<Book> books;
}
实体类Book
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Book {
private Long id;
private String name;
private String category;
private Integer score;
private String intro;
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class StreamDemo {
private static List<Author> getAuthors() {
Author author = new Author(1L, "蒙多", 17, "一个祖安人", null);
Author author2 = new Author(2L, "亚拉索", 18, "艾欧尼亚", null);
Author author3 = new Author(3L, "易大师", 19, "黑色玫瑰", null);
Author author4 = new Author(3L, "易大师", 19, "黑色玫瑰", null);
List<Book> book1 = new ArrayList<>();
List<Book> book2 = new ArrayList<>();
List<Book> book3 = new ArrayList<>();
List<Book> book4 = new ArrayList<>();
book1.add(new Book(1L,"刀的两侧是光明与黑暗","哲学,爱情", 84, "*"));
book1.add(new Book(2L,"一个人不能死在同一把刀下","爱情,个人成长", 80, "**"));
book2.add(new Book(3L,"那风吹不到的地方","爱情,传记", 75, "***"));
book2.add(new Book(3L,"那风吹不到的地方","爱情,传记", 75, "****"));
book2.add(new Book(4L,"吹或不吹","哲学", 76, "*****"));
book3.add(new Book(5L,"你的剑就是我的剑","个人成长", 78, "******"));
book3.add(new Book(6L,"风与剑","传记", 82, "*******"));
book3.add(new Book(6L,"风与剑","传记", 82, "********"));
book4.add(new Book(5L,"你的剑就是我的剑","个人成长", 78, "******"));
book4.add(new Book(6L,"风与剑","传记", 82, "*******"));
book4.add(new Book(6L,"风与剑","传记", 82, "*******"));
author.setBooks(book1);
author2.setBooks(book2);
author3.setBooks(book3);
author4.setBooks(book4);
List<Author> authors = new ArrayList<>(Arrays.asList(author,author2,author3,author4));
return authors;
}
}
常用的中间操作API如下:
1、filter:
对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中
- 案例:需要打印所有年龄小于18的作家的名字,并且注意去重:
private static void test01() {
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.filter(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge()< 18;
}
})
.forEach(new Consumer<Author>(){
public void accept(Author author){
System.out.println(author.getName());
}
});
}
- Lambda表达式优化匿名内部类:
private static void test01() {
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.filter(author -> author.getAge()<18)
.forEach(author -> System.out.println(author.getName()));
}
2、distinct:
可以去除流中的重复元素,distinct方法是依赖Object的equals方法来判断是否是相同对象的,所以需要注意重写equals方法,即加注解 @EqualsAndHashCode
- 案例:对作家进行去重:
List<Author> authors = getAuthors();
authors.stream().distinct()
3、map:
对Stream流中的元素进行计算或者对Stream流中的元素进行数据类型的转换
-
3.1、案例一:打印所有作家的姓名
map方法,它的参数是function ,它有两个泛型,第一个泛型Author就是流元素的类型,第一个泛型不能修改,改了报错,第一个泛型必须和流当中的泛型是一样的。
但是第二个是非常重要的,它代表了你期望通过map把Author转换成什么类型,如果说你还是转换成Author,那你就写Author:
但是我期望是把它转换成一个字符串。我希望到时候流当中的每个元素都是一个字符串儿,就是这个作家的姓名,就写String
经过这一步map操作之后,其实流里面都是一个一个的字符串了:
- Lambda表达式优化匿名内部类:
-
3.2、也可以对流中的元素进行计算,案例二:
private static void test02() {
List<Author> authors = getAuthors();
authors.stream()
.map(author -> author.getAge())
.map(age -> age + 10)
.distinct()
.forEach(age -> System.out.println(age));
}
4、sorted:
可以对流中的元素进行排序
- 我们在调用sorted时会发现它有两种传参形式:
-
4.1、空参的sorted()方法
案例:对流中的元素按照年龄进行降序排序
如果调用空参的sorted()方法,需要流中的元素实现了Comparable接口,否则会抛出
java.lang.ClassCastException 类型转换异常:
实际上这里在进行排序的时候,他会把流当中的Author类型的元素,转换成Comparable接口类型,再去进行相应方法的调用。
但是现在的Author 和 Comparable接口是没有关系的,它并没有实现Comparable这个接口,所以肯定会出现类型转换异常:
如果一个类想让它具有比较的能力,你可以选择去实现这个Comparable接口,在compareTo()方法当中去定义如何去比较两个元素/两个对象:
我们这里要求是按照年龄进行排序,所以我们就可以拿this当前作家和你所传入进来的Author o对象去进行一个年龄的比较,去进行一个相减的操作,比较的结果是int类型,其实只要去返回他们的比较结果就可以了:
以this为准,this在左是升序,this在右是降序
o1-o2是升序,o2-o1是降序,o1是当前项
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 去重
public class Author implements Comparable<Author>{
private Long id;
private String name;
private Integer age;
private String intro;
private List<Book> books;
@Override
public int compareTo(Author o) {
return o.getAge()- this.getAge();
}
}
结果输出:
-
4.2、传入对 Comparator 接口的实现:
案例:对流中的元素按照年龄进行降序排序
以this为准,this在左是升序,this在右是降序
o1-o2是升序,o2-o1是降序,o1是当前项
private static void test02(){
List<Author> authors = getAuthors();
authors.stream()
.sorted(new Comparator<Author>(){
public int compare(Author o1 , Author o2){
return o2.getAge()-o1.getAge();
}
})
.forEach(author -> System.out.println(author.getAge()));
}
- Lambda表达式优化匿名内部类:
private static void test02(){
List<Author> authors = getAuthors();
authors.stream()
.sorted((o1,o2) -> o2.getAge()-o1.getAge())
.forEach(author -> System.out.println(author.getAge()));
}
5、flatMap:
map只能把一个对象转换成另一个对象来作为流中的元素,而flatMap可以把一个对象转换成多个对象作为流中的元素
它的参数是传进来Author作家,也就是说每个元素都是作家类型的,然后你要注意它返回的类型:要求我们返回的是Stream流对象:
5.1、案例一:
要求打印所有书籍的名字, 书籍当中如果有重复的话,我们要进行去重
- 很多同学没有学过flatMap,很有可能做的一件事情是这样的:
private static void tesst04(){
List<Author> authors = getAuthors();
authors.stream()
.map(author -> author.getBooks())
.forEach(new Consumer<List<Book>>() {
@Override
public void accept(List<Book> books) {
// 这样写的话,流当中的每一个元素都是List<Book>,不是Book
// 整体是 List<List<Book>>
// 我们还要在这里面再进行forEach遍历
}
});
}
- 这种方式其实就不太符合我们的期望了,我们其实就是想把List<List<Book>>集合里面的每个List<Book>元素都转换成一个Book对象:即让流中的每一个元素都变成Book对象;
-
所以应该使用到flatMap:首先我们先调用author.getBooks()方法, 拿到的是一个List<Book>集合,然后把这个集合转换成对应的Stream流对象 , 并且现在流当中的元素类型就是我们的Book类型,他会自动把这些流进行一个拼接。
相对于是把List<List<Book>>集合当中的每一个元素转化为多个元素返回:
- Lambda表达式优化匿名内部类:
private static void tesst04(){
List<Author> authors = getAuthors();
authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.forEach(book -> System.out.println(book.getName()));
}
5.2、案例二:
打印现有书籍的所有分类。要求对分类进行去重、不能出现这种格式:哲学,爱情:
如果说这本书是属于多个分类”哲学、爱情“,那我们应该在多个分类之间用逗号进行分割,把它转换成两个分类:一个是哲学,一个是爱情,相当于就是进行一个字符串的分割:
调用String.split()方法,将一个字符串分割为子字符串,然后将结果作为字符串数组返回:返回的是一个字符型数组,如果说他只有一种分类的这种情况,它其实也会转换成一个字符串数组,只不过数组里只有一个元素。
flatMap() 方法 它要求返回的是Stream流对象,所以你要把这个数组转换成流对象,数组怎么转换成流对象,前面讲过:
.flatMap(book -> Stream.of(book.getCategory().split(",")))
.flatMap(book-> Arrays.stream(book.getCategory().split(",")))
//打印现有书籍的所有分类。要求对分类进行去重
//1、先获得所有的书籍 Stream<Book>
//2、再获得所有的书籍的分类
private static void test05(){
List<Author> authors = getAuthors();
authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct() //这里的去重是对流当中的书籍去重
.flatMap(book -> Arrays.stream(book.getCategory().split(",")))
.distinct()//这里的去重是对流当中的类别去重
.forEach(name -> System.out.println(name));
}
6、limit:
可以设置流的最大长度,超出的部分将被抛弃掉,相当于进行了一个截取
- 案例:对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名:
可以先进行排序再去重,也可以先去重再排序,实际上先去重效果会更好一点,因为你去重之后,它参与排序的元素就不用那么多了:
private static void test06(){
//对流中的元素按照年龄进行降序排序,
// 并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted((o1,o2)->o2.getAge()-o1.getAge())
.limit(2)
.forEach(author -> System.out.println(author.getName()));
}
7、sikp:
跳过流中的前n个元素,返回剩下的元素
- 案例:打印除了年龄最大的作家以外的其他作家,要求不能有重复元素,并且按照年龄降序排列:
private static void test07(){
//打印除了年龄最大的作家以外的其他作家,
// 要求不能有重复元素,并且按照年龄降序排列
List<Author> authors = getAuthors();
authors.stream()
.distinct()
.sorted(new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o2.getAge()-o1.getAge();
}
})
.skip(1)
.forEach(author -> System.out.println(author.getName()));
}
四、Stream流"终结操作"常用的API
如果流的一系列的操作要成功触发并生效,那必须需要有终结操作,一个流只能做一次终结操作,如果没有终结操作,中间操作根本不会被执行,所有的中间操作返回的都是一个Stream对象,所以可以一直链式编程
1、forEach:
对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体的操作
- 案例:输出所有作家的名字
// 输出所有作家的名字
List<Author> authors = getAuthors();
authors.stream()
.map(author -> author.getName())
.distinct()
.forEach(name-> System.out.println(name));
2、count:
可以用来获取当前流中的元素
- 案例:打印这些作家的所出书籍的数目,注意删除重复元素,不同的作家没有同名字的书籍
private static void test08(){
//打印这些作家的所出书籍的数目,注意删除重复元素
List<Author> authors = getAuthors();
Long counts = authors.stream()
.distinct()
.flatMap(author -> author.getBooks().stream())
.count();
System.out.println(counts);
}
3、max、min
可以用来获取流中的最值
- 案例: 分别获取这些作家的所出书籍的最高分和最低分并打印。
最高分:
private static void test09(){
List<Author> authors = getAuthors();
//分别获取这些作家的所出书籍的最高分和最低分并打印。
Book book = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.max(new Comparator<Book>() {
@Override
public int compare(Book o1, Book o2) {
return o1.getScore() - o2.getScore();
}
})
.get();
System.out.println(book.getScore());
}
private static void test10(){
List<Author> authors = getAuthors();
Integer max_score = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.map(book -> book.getScore())
.max((o1,o2) -> o1-o2)
.get();
System.out.println(max_score);
}
最低分:
4、collect
把当前流转换成一个集合
我们使用Stream流对数据进行了一系列的处理之后, 我最终的这些数据可能还要返回给别人去使用,例如返回成集合的一个形式,那这个时候就需要把流再转换成集合。
我们知道集合其实主要有两种,一种是单列集合,单列集合其实主要就两种:List和Set;另一种双列集合里面,就是map集合。
接下去我们就来看一下如何使用collect方法,把Stream流转分别转换成这三种集合:
这里要注意collect方法,这个参数会比较复杂一点。实际上我们在用它的时候,是不需要直接写匿名内部类的,因为这个参数是非常复杂的,我们用的是它的工具类,它提供好的一个工具类Collectors,专门供我们这个collect方法去使用的:
Collectors工具类,它里面提供了一些静态方法,因为工具里面基本上都是静态的成员,所以我们直接类名点就可以了:
4.1 转化成list集合——toList()方法:
- 案例:获取一个存放所有作者名字的集合
private static void test12(){
//获取一个存放所有作者名字的集合
List<Author> authors = getAuthors();
List<String> names = authors.stream()
.map(author -> author.getName())
.distinct()
.collect(Collectors.toList());
System.out.println(names);//[蒙多, 亚拉索, 易大师]
}
4.2 转化成set集合——toSet()方法:
toSet()方法和toList()方法相比较而言,toSet()方法可以自动去重,
因为Set集合本身是无序不可重复嘛
private static void test12(){
//获取一个存放所有作者名字的集合
List<Author> authors = getAuthors();
Set<String> names = authors.stream()
.map(author -> author.getName())
.collect(Collectors.toSet());
System.out.println(names);//[亚拉索, 蒙多, 易大师]
}
4.3 转化成Map集合——toMap()方法:
- 案例:获取一个Map集合,map的key为作者名,value为List<Book>
转化成Map集合相对来说比较复杂一点,
我们要把流当中的元素转换成一个map,map是有key和value的,那你得告诉流当中的元素怎么转换成key,怎么转换成value,所以它要求你传入两个参数都是Function类型,实际上这个接口是可以进行转换的,我们前面的map方法,它的参数其实都是这个function接口类型的。
所以我们要传入两个Function,一个是告诉他怎么转换成key,另外一个是告诉他怎么转换成value: 你要去指定一下从哪个类型转换成哪个类型。