Stream流
Stream流
1、为什么有Stream流
流出现的原因是现在的项目发展的缘故,项目中已经不止使用一种数据库了,可能使用的有MySQL、Oracle、MongDB等等,从这些多数据源中查询出来的数据,还需要程序员手动的对这些数据来进行处理。将这里数据进行合并,所以这里Stream流出现的本质原因。所以这里首先体现出来的就是数据量的问题。
所以从这里可以判断出,stream流主要是用于对集合的处理。对于集合来进行处理,再次判断,集合中的元素都是相同的,不可能再会是Object类型的了,应该用一个泛型来进行指定。
Stream流最终会形成一个容器,来装在数据的。现实生活中河流是有开始和结束的,同样的,stream也是有开始和结束的,在中间的这一段经历了什么?我们可以通过需求来进行操作。
1.1、重点
根据上面的说明,所以stream流不是用来存数据的,而是对里面的数据来进行操作的。
2、获取Stream流的几种方式
2.1、集合获取得到Stream流
上面说的,用于集合。列出常见的集合:
List、Set、Map
得到流对象:
直接使用集合引用:list.stream(),set.stream(),map.stream(),即可获取得到一个流容器
例子如下:
public class Demo1 {
public static void main(String[] args) {
// 后去得到stream流。这里没有具体的数据,在之后进行讲解
List<Integer> list = new ArrayList<>();
Stream<Integer> stream = list.stream();
Set<Integer> integers = new HashSet<>();
Stream<Integer> stream1 = integers.stream();
Map<String,String> map = new HashMap<>();
Stream<String> stream2 = map.keySet().stream();
Stream<String> stream3 = map.values().stream();
Stream<Map.Entry<String, String>> stream4 = map.entrySet().stream();
}
}
2.2、使用流类中的静态方法获取
使用Stream.of(T... t):得到流对象
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Stream<String> stringStream = Stream.of("hello","world");
还有其他方式获取得到stream流,这里选取常用的,其他的可以去网上查下资料。
3、操作stream流
首先看一下,一个大概的流程图:
上面已经说过了怎么来获取得到Stream对象,这里介绍下里面的操作:
3.1、Stream流方向性
1、Stream流是不可逆转的,只能一直向前走,每次操作后都是一个新的Stream流容器。而每个流处理之后是不能再次被使用的
public class Demo2 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Stream<Integer> stream1 = list.stream();
Stream<Integer> stream2 = stream1.peek((n) -> System.out.println(n));
Stream<Integer> stream3 = stream1.limit(5);
}
}
启动就报错,错误日志显示stream已经被终止了或者是已经被操作做了:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
at java.util.stream.ReferencePipeline$StatefulOp.<init>(ReferencePipeline.java:647)
at java.util.stream.SliceOps$1.<init>(SliceOps.java:120)
at java.util.stream.SliceOps.makeRef(SliceOps.java:120)
at java.util.stream.ReferencePipeline.limit(ReferencePipeline.java:401)
at com.guang.stream.Demo2.main(Demo2.java:16)
从上面可以知道,处理过后就不会再次被利用了。stream has already been operated upon or closed
那么这里换一下:
public class Demo2 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Stream<Integer> stream1 = list.stream();
Stream<Integer> stream2 = stream1.peek((n) -> System.out.println(n));
// 将1换成2即可得到新的
Stream<Integer> stream3 = stream2.limit(5);
}
}
3.2、Stream流之懒加载
懒加载类似mybatis中的懒加载以及单例模式中的懒汉模式,在使用的时候才会进行加载。那么stream流中的懒加载是什么意思?
stream流中的懒加载就是没有遇到终止节点之前,都不去执行。
看下面的例子来说明:
public class Demo2 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Stream<Integer> stream1 = list.stream();
Stream<Integer> integerStream = stream1.filter((n) -> {
if (n > 2) {
System.out.println(n);
return true;
} else {
System.out.println(n);
return false;
}
});
}
}
可以看到上面的流在经过处理之后,没有任何的效果。因为还是一个流,既然是在流的状态下,那么就不会有任何输出。
3.3、流之中间节点、终止节点
这里就牵扯出来了:如何来判断是中间节点还是终止节点。
一句话:只要返回值不是一个stream及其子类,那么都是终止节点。
public class Demo2 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Stream<Integer> stream1 = list.stream();
Stream<Integer> integerStream = stream1.filter((n) -> {
if (n > 2) {
System.out.println(n);
return true;
} else {
System.out.println(n);
return false;
}
});
}
}
上面的代码如果不是懒加载节点,那么就应该会输出,但是执行的时候并没有进行输出,因为没有遇到终止节点。所以懒加载节点并不是进行就直接进行执行,而是遇到了终止节点才执行。
3.3、Stream流中元素顺序
流中的元素是一个一个的进入到后面的流容器中的,而不是将最开始流中的数据直接全部推送到后续的容器中去的。
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
list.stream().peek((n)-> System.out.println(n.getColor()))
.peek((n)-> System.out.println(n.getWeight()))
.toArray();
}
}
最终控制台输出:
red
23.0
red
3.0
blue
12.0
blue
13.0
再来一个案例:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
查看控制台输出:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
在流中的每一个元素,每一个元素经过所有的处理之后,才会执行到下一个元素的处理。
如果中间被过滤掉了,如同上面的filter过滤,那么才会经过让下一个元素进来继续执行。
所以流中元素是一个一个进去的。
4、Stream流操作方法
4.1、forEach方法
从名字意思可以看出是一个遍历方法,看下源码说明:
void forEach(Consumer<? super T> action);
这是一个对流中方法执行操作的方法,通过返回值类型来看,没有返回值。但是如果是引用类型,我们如果在其中做了一些操作的话,那么将会改变引用类型中的属性值。
看一下Consumer函数式接口:
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.对给定的元素进行操作
*
* @param t the input argument。给定的参数!所以参数是外部传入进来的
*/
void accept(T t);
// 排除掉默认方法。函数式接口
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
例子如下,对流中元素进行遍历:
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
// 并非只有这一种操作!一开始都是被这种操作给迷惑了!
list.stream().forEach(n-> System.out.println(n));
}
}
4.2、filter方法
从名字可以看到是过滤方法。那么过滤掉的将会进行拦截,没有过滤的将如何处理?所以从名字就可以推断出是一个中间节点。
/**
*返回一个流中的经过条件匹配的元素。
* @return the new stream:返回的是一个新的流容器对象。
*/
Stream<T> filter(Predicate<? super T> predicate);
看下Predicate
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.对给定的参数进行执行判断。
* @return {@code true} if the input argument matches the predicate,如果满足匹配,那么进行返回
* otherwise {@code false}:不满足匹配,返回的是false
*/
boolean test(T t);
通过注释,翻译一下:如果是true,那么表示条件满足,该元素会留在流中;如果是false,条件不满足,那么不会留在流中。
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
// 给定的条件是每个元素是否等于red,如果是red,那么是true,留下来;如果是false,不满足匹配,过滤掉
list.stream().filter(n->n.getColor().equals("red")).forEach(n-> System.out.println(n));
}
}
4.2、limit和skip方法
limit方法
/**
* Returns a stream consisting of the elements of this stream, truncated
* to be no longer than {@code maxSize} in length.
截断流中的不超过最大个数的元素,返回不超过最大个数元素所组成的流
*/
Stream<T> limit(long maxSize);
也就是说限制了maxsize最大个数之前的元素,maxsize之前的元素可以留下来,maxsize之后的元素不会留下来。
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
// 限制了两个元素,所以只能留下两个元素。看输出台打印即可看出。
list.stream().limit(2).forEach(n-> System.out.println(n));
}
}
skip方法
Stream<T> skip(long n);
跳过指定的参数n个元素,将第n+1个元素作为一个流元素。
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
// 跳过前两个元素,将第三个元素作为第一个元素。看下控制台打印。
list.stream().skip(2).forEach(n-> System.out.println(n));
}
}
4.3、map方法
map是我们最常见的方法,K-V键值对结构。
/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
* @param <R> The element type of the new stream:新的流中元素类型
* @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* function to apply to each element
* @return the new stream:返回一个新的流
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.将函数用在给定的参数上
* @param t the function argument:函数参数
* @return the function result:返回函数执行完成后的结果
*/
R apply(T t);
通过一系列的翻译可以知道,map就是利用一个函数,将流中的元素进行计算,返回新的新的元素。
所以这个是一个转换方法。将流中的元素通过函数来转换成对应的元素。
public class Demo3 {
public static void main(String[] args) {
List<Apple> list = new ArrayList<>();
list.add(new Apple("red",23D));
list.add(new Apple("red",3D));
list.add(new Apple("blue",12D));
list.add(new Apple("blue",13D));
// 自定义一个函数来进行实现。
list.stream().map(n->{
if (n.getColor().equals("red")){
return 1;
}else {
return 2;
}
}).forEach(n-> System.out.println(n));
}
}
如果流中元素的颜色是red,那么返回1;如果不是,那么返回2;也就是说流中的元素和现在流中的元素完全是不搭边的。但是现在流中的元素是通过算法得到的。与原来流中元素有很多的不同。
map也就是我们经常说的映射关系,通过这个映射关系,我们得到我们想要的值,然后将值收集起来,形成对应的集合即可。
4.4、 flatMap方法
关于这个方法,理解起来是真的不好理解,所以我这里多写几个例子来进行说明:
demo1:
将字符串数组{"hello","world"}对应的输出其中的每个字符即可。
@Test
public void testListFour(){
String[] strings = {"hello","world"};
Stream.of(strings).flatMap(t-> Arrays.stream(t.split(""))).forEach(System.out::println);
}
demo2:
将每个list集合中的元素放入到一个大的集合中来,然后从大的集合中来获取得到对应的每个元素
@Test
public void testListTwo(){
User user1 = User.builder().id(1).address("深圳").name("xuan").build();
User user2 = User.builder().id(2).address("深圳").name("huang").build();
User user3 = User.builder().id(3).address("深圳").name("chen").build();
List<User> userList1 = new ArrayList<>();
List<User> userList2 = new ArrayList<>();
List<User> userList3 = new ArrayList<>();
List<List<User>> users = new ArrayList<>();
userList1.add(user1);
userList2.add(user2);
userList3.add(user3);
users.add(userList1);
users.add(userList2);
users.add(userList3);
//流中的元素都来进行扁平化操作。也就是说会把相当类型的元素收集起来进行汇总
List<User> collect = users.stream().flatMap(t->t.stream()).collect(Collectors.toList());
collect.forEach(System.out::println);
}
从上面的两个结果来看,似乎都是容器包含容器的关系,容器中的数据都是一样的,那么用一个大容器来盛放所有的数据,然后从大的容器中来获取得到对应的值即可。
4.5、concat方法
// 发现了两个流中的数据类型都是一样的,所以concat方法中,传入的两个数据都是一样的。
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
@SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}
Demo来进行测试:
public class Demo4 {
public static void main(String[] args) {
Stream<Integer> s1 = Stream.of(1,2,5,6,3,9,10);
Stream<Integer> s2 = Stream.of(11,22,52,63,34,94,130);
Stream<Integer> concat = Stream.concat(s1, s2);
concat.forEach(n-> System.out.println(n));
}
}
4.6、collect方法
用来收集的方法,这个方法使用比较丰富。
最常见的使用方式:
分组、转换成另外一种集合等等方法。这个需要在使用的时候用到。
public class Demo4 {
public static void main(String[] args) {
Stream<Integer> s1 = Stream.of(1,2,5,6,3,9,10);
Stream<Integer> s2 = Stream.of(11,22,52,63,34,94,130);
Stream<Integer> concat = Stream.concat(s1, s2);
// 统计个数等等操作。
Long collect = concat.collect(Collectors.counting());
System.out.println(collect);
}
}
类似java原生API中的addAll方法。
4.7、sorted
排序算法不得不去介绍一下java原生API中的
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose", 18));
list.add(new Student("jack", 16));
list.add(new Student("abc", 20));
Collections.sort(list,((o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.getAge()-o2.getAge();
}));
System.out.println("-----------------");
list.forEach(System.out::println);
查看输出结果:
Student(name=jack, age=16)
Student(name=rose, age=18)
Student(name=abc, age=20)
Student(name=jack, age=16)
Student(name=abc, age=20)
Student(name=rose, age=18)
-----------------
Student(name=jack, age=16)
Student(name=rose, age=18)
Student(name=abc, age=20)
通过结果可以看到,每一个后来的元素,都会和之前已经排序好的来进行比较。所以有n!次
对于Comparator函数式接口来说,我们可以自定义排序规则。因为返回值决定了排序规则。如果是返回值是正数,代表的是由小到大;如果返回值是负数,那么就是由大到小。
Collections.sort(list,((o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o2.getAge()-o1.getAge();
}));
修改一下代码,可以看到顺序就是相反的。那么来一下Stream流中的排序
/**
* https://blog.csdn.net/qq_36763236/article/details/111469653
*/
@Test
public void testCompared2() {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose", 18));
list.add(new Student("jack", 16));
list.add(new Student("abc", 20));
// list.stream().sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getName).thenComparing(Student::getAge)).forEach(System.out::println);
list.stream().sorted(Comparator.comparing(Student::getAge,Comparator.reverseOrder())).forEach(System.out::println);
}
参考API即可。
4.8、分组函数
@Data
@AllArgsConstructor
public class Person {
Integer classNo;
String name;
Integer age;
}
对应的测试方法:
@Test
public void testGroup(){
List<Person> list = new ArrayList<>();
list.add(new Person(1,"张三",12)); // 1班 张三 12岁
list.add(new Person(1,"李四",13));
list.add(new Person(2,"王五",12));
list.add(new Person(2,"贼六",11));
list.add(new Person(3,"对七",12));
Map<Integer, List<Person>> collect = list.stream().collect(Collectors.groupingBy(Person::getClassNo));
collect.forEach((k,v)->{
System.out.println(k+"==========="+v);
});
}
查看输出:
1===========[Person(classNo=1, name=张三, age=12), Person(classNo=1, name=李四, age=13)]
2===========[Person(classNo=2, name=王五, age=12), Person(classNo=2, name=贼六, age=11)]
3===========[Person(classNo=3, name=对七, age=12)]
4.9、anyMatch方法
/**
* 如果又多个,匹配到了一个,那么就返回true;
* 如果一个都没有,那么返回false;
*/
@Test
void contextLoads() {
Person stu1 = new Person( "19", "张三");
Person stu2 = new Person( "23", "李四");
Person stu3 = new Person( "28", "王五");
Person stu4 = new Person( "23", "李四");
List<Person> list = new ArrayList<>();
list.add(stu1);
list.add(stu2);
list.add(stu3);
list.add(stu4);
// 判断学生年龄是否有大于27岁的
boolean anyMatchFlag = list.stream().anyMatch(t->t.getName().equals("李四1"));
System.out.println(anyMatchFlag);
}
断言函数来进行断言。如果匹配到了,那么就直接返回true;否则返回false。
4.10、Collectors.toMap
参考链接:https://zhangzw.com/posts/20191205.html
List 转 Map 操作这个使用起来还是比较爽的,先看看以前的用法。
@Accessors(chain = true)
@lombok.Data
class User {
private String id;
private String name;
}
然后准备一下集合数据:
List<User> userList = Lists.newArrayList(
new User().setId("A").setName("张三"),
new User().setId("B").setName("李四"),
new User().setId("C").setName("王五")
);
我们希望转成 Map 的格式为:
A-> 张三
B-> 李四
C-> 王五
过去使用的方式:
Map<String, String> map = new HashMap<>();
for (User user : userList) {
map.put(user.getId(), user.getName());
}
使用JDK8提供的方法来进行操作,一行代码搞定:
userList.stream().collect(Collectors.toMap(User::getId, User::getName));
对应的代码是:
@Test
public void testOne(){
List<UserVO> userList = Lists.newArrayList(
new UserVO().setId("A").setName("张三"),
new UserVO().setId("B").setName("李四"),
new UserVO().setId("C").setName("王五")
);
final Map<String, String> collect = userList.stream().collect(Collectors.toMap(UserVO::getId, UserVO::getName));
collect.forEach((k,v)-> System.out.println(k+"->"+v));
}
如果希望value是自身的时候,也可以这么来进行操作:
userList.stream().collect(Collectors.toMap(User::getId, t -> t));
或:
userList.stream().collect(Collectors.toMap(User::getId, Function.identity()));
Collectors.toMap 有三个重载方法:
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier);
参数含义分别是:
- keyMapper:Key 的映射函数
- valueMapper:Value 的映射函数
- mergeFunction:当 Key 冲突时,调用的合并方法
- mapSupplier:Map 构造器,在需要返回特定的 Map 时使用
还是用上面的例子,如果 List 中 userId 有相同的,使用上面的写法会抛异常:
List<User> userList = Lists.newArrayList(
new User().setId("A").setName("张三"),
new User().setId("A").setName("李四"), // Key 相同
new User().setId("C").setName("王五")
);
userList.stream().collect(Collectors.toMap(User::getId, User::getName));
// 异常:
java.lang.IllegalStateException: Duplicate key 张三
at java.util.stream.Collectors.lambda$throwingMerger$114(Collectors.java:133)
at java.util.HashMap.merge(HashMap.java:1245)
at java.util.stream.Collectors.lambda$toMap$172(Collectors.java:1320)
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at Test.toMap(Test.java:17)
...
这时就需要调用第二个重载方法,传入合并函数,如:
userList.stream().collect(Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1 + n2));
// 输出结果:
A-> 张三李四
C-> 王五
第四个参数(mapSupplier)用于自定义返回 Map 类型,比如我们希望返回的 Map 是根据 Key 排序的,可以使用如下写法:
List<User> userList = Lists.newArrayList(
new User().setId("B").setName("张三"),
new User().setId("A").setName("李四"),
new User().setId("C").setName("王五")
);
userList.stream().collect(
Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1, TreeMap::new)
);
// 输出结果:
A-> 李四
B-> 张三
C-> 王五
4.11、group函数
这个使用也是很简单的,所以需要记录一下:
4、方法引用
为什么会存在着方法引用??
因为我们在lambda表达式中,我们最终需要体中写我们自定义的逻辑来进行实现。
但是有时候想调用已经存在的方法,可以作为参数或者是作为调用者,都是可以的。
jdk8提供了方法引用。我们最常见的写法就是类名::函数名来进行调用。
为什么可以这样子来操作?因为我们最终想要调用的是流中元素,也就是对象来调用方法的。
比如最常见的:
List<String> stringList = Arrays.asList("1", "2", "3", "4");
stringList.forEach(System.out::println);
这里我们只是想要将流中的对象作为参数来进行输出,除了流中对象能够做参数之外,还可以作为方法的调用者。
List<String> stringList = Arrays.asList("a", "b", "c", "d");
stringList.stream().map(String::toUpperCase).forEach(System.out::println);
从这里看到参数在方法引用中既可以充当调用者,还可以充当参数的存在。所以我们可以在很多地方都这样来操作。
5、总结
对于流中的方法操作来说,可以看到流中的对象总是一个对象。注意:要和基本的数据类型区分开来,因为要避免掉字符串和基本类型的参数,因为我们操作的时候,如果peek和map,都需要改变对象,如果对象的地址已经发生改变了,那么流中相当于是不会有任何数据来进行输出了。