java随笔之--Stream流

    技术2026-04-14  5

    在Java8(JDK8)中.得益于Lambda表达式的发明,让Stream流开始大放异彩.本文主要讨论stream流引入的背景,stream的语法,以及它的实际应用小案例.

    Lambda是什么

    即使是完全的java小白也应该知道,java是完全面向对象的语言.对于执行一个动作,java实际上更关注如何执行而不是执行的目标.简单点说,任何一个方法都需要借助一个对象去调用,至于方法本身的内容,反倒不是java所真正关心的.这是典型的面向对象的思想.面向对象的思想,好处是很多的,比如它可以对事物进行分类,寻找类别之间的相似性,封装类的方法,使得程序具有很高的适应性和耐用性.在实现一个动作时,我们需要知道谁可以执行这个动作,还有谁可以执行这个动作,通过封装,重写等操作.我们可以不断完善这个执行者而它特有的动作.根据执行者的差异还可以对动作进行分化.只要我们稍加记忆,我们就能够随时随地地调用这个方法.虽然可能会费点功夫,但为了今后更方便地调用打下了基础. 但是面向对象的思想在某些情况下也有弊端.有时候我们想完成一个很简单的操作,比如排序.但是在java中就必须得构造一个比较器对象,还要重写比较器对象的compare方法才能够做到.这中间虽然达到了排序的目的,但产生了大量的冗余代码,可读性不是很好.这就为lambda表达式的发明提供了一个契机.其实开发者大概也是注意到了这一点,要完成一个很简单的操作,可不可以不要创建一个对象,只为了调用它的一个方法,而直接现场写一个方法呢?我这样说肯定让大家很懵.接下来举个栗子. 比如我们考虑一个Person类,它的属性有名字,年龄.设计一个有参构造器和set,get方法.就基本完成了一个对象的封装.再重写一个toString方法,方便在输出的时候能够可视化(而不是输出地址这种没有实际价值的内容).

    public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }

    现在刘备,关羽,张飞三个人要结拜,哪个当老大呢?我们必然是考虑年龄嘛.首先,我们设计一个ArrayList类用于保存这三个对象.然后重写Comparator类的compare()方法.具体细节我就不说了,大家可以自己复习一下.最后我们遍历并输出这三个对象.

    public class ComparatorDemo { public static void main(String[] args) { //这三个人按照年龄排序,决定哪个当老大. List<Person> array = new ArrayList<>(); array.add(new Person("刘备", 29)); array.add(new Person("关羽", 26)); array.add(new Person("张飞", 25)); // 匿名内部类 Comparator<Person> comp = new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o2.getAge() - o1.getAge(); } }; Collections.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例 for (Person person : array) { System.out.println(person); } } }

    我们这里用到了匿名内部类的写法.这是我们对一个引用类型的数组排序的常规写法. 下面我们来搞清楚上述代码真正要做什么事情。

    为了排序,Collections.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;为了指定compare的方法体,不得不需要Comparator接口的实现类;为了省去定义一个Comparator实现类的麻烦,不得不使用匿名内部类;必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;实际上,只有参数和方法体才是关键。

    为了突出我们想要的,尽可能地摒弃那些繁琐的对象的创建和方法的重写,我们就需要用到lambda表达式.lambda表达式的语法是这样的

    public class ComparatorDemo1 { public static void main(String[] args) { //这三个人按照年龄排序,决定哪个当老大. List<Person> array = new ArrayList<>(); array.add(new Person("刘备", 29)); array.add(new Person("关羽", 26)); array.add(new Person("张飞", 25)); //lambda表达式一行搞定 Collections.sort(array, (o1, o2) -> o2.getAge() - o1.getAge()); for (Person person : array) { System.out.println(person); } } }

    读者可以自己去运行,结果都是一样的: 至于lambda表达式的省略情况和语法等大家不熟悉地自己去查就好了,我举这个例子主要是想说明lambda表达式关注的是"做什么",至于怎么做,谁来做,都被省略了.所以它更像是一种面向函数的写法. 事实上,lambda表达式的应用场景正是用于这种函数式接口的方法的重写上. 这就基本解决了lambda表达式是什么和能干什么的问题. 那么这跟我们的stream又有什么关系呢?关系可大了.

    Stream流介绍

    Stream流是一种针对集合的一种模型,这个模型对集合类进行了包装,对集合进行遍历.stream流对集合有这样几种操作:过滤(filter),映射(map),统计(count),逐一处理(forEach)等.

    同lambda的思想一样,stream的出现也是为了简化对数组的操作.因为在stream引入之前,要对数组的每个元素进行操作,就必须要执行for循环.然后根据我们的条件对循环到的每个元素进行对应操作,比如下面这个栗子,就是根据条件打印集合中的某些元素的值. 假如有这么几个人,都存放在一个数组中,我们想要根据条件筛选出我们需要的名字.比如把姓张的单独拿出来存放在张姓数组中,再从张姓数组中找到名字长度为3的拿出来放到长名字数组中,最后我们打印出所有这样的名字.如果是常规做法,下面是一个参考代码:

    public class Demo02NormalFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); List<String> zhangList = new ArrayList<>(); for (String name : list) { if (name.startsWith("张")) { zhangList.add(name); } } List<String> threeList = new ArrayList<>(); for (String name : zhangList) { if (name.length() == 3) { threeList.add(name); } } for (String name : threeList) { System.out.println(name); } } }

    这段代码中含有三个循环,每一个作用不同:

    首先筛选所有姓张的人;然后筛选名字有三个字的人;最后进行对结果进行打印输出。

    每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是.循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。

    那么,Stream能给我们带来怎样更加优雅的写法呢?

    Stream流定义

    java.util.stream.Stream< T > 是Java 8新加入的最常用的流接口。它有两种声明方式: 1.根据collection获取流. 首先,java.util.Collection接口中加入了default方法stream用来获取流,所以其所有实现类均可获取流。

    import java.util.*; import java.util.stream.Stream; public class Demo04GetStream { public static void main(String[] args) { List<String> list = new ArrayList<>(); // ... Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); // ... Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); // ... Stream<String> stream3 = vector.stream(); } }

    2.根据Map获取流.

    import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; public class Demo05GetStream { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // ... Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }

    借助Stream流的filter()方法和forEach()方法,我们可以优雅地写出以下代码:

    public class Demo03StreamFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); list.stream() .filter(s -> s.startsWith("张")) .filter(s -> s.length() == 3) .forEach(System.out::println); } }

    是不是简单多了?

    Stream语法

    Stream(流)是一种函数模型,就跟上面的lambda表达式一个意思.它主要用来实现对数组的遍历操作.而且这种遍历是单向的,不回头的,你可以考虑成原数组的包装,但这种包装做出的任何操作都不会对原数组产生任何的更改.并且这个函数模型也并不是数组. 上述方式也被称作链式调用,意思是可以不断地调用,直到返回类型不是Stream类型为止. 类似于lambda的语法,它也包含一些东西.比如说箭头.箭头后面是内容,至于内容是什么,lambda是重写方法的方法体,Stream中要根据它的api函数来定.通常来说,stream具有筛选,映射,组合,提取,跳过,统计等操作.这里就介绍两个,也就是上面的.filter()和.forEach().

    .filter()意为筛选,stream调用此方法表示要对操作数组中的每个元素按照筛选条件进行筛选,筛选条件就是filter()的参数.比如"长度为3", "字符串以"张"打头"这样的条件就在上面的代码中实现了.值得注意的是,经过.filter()的Stream类型并没有被改变,因此,它可以继续实现其他Stream类型的相关操作..forEach()意为逐一处理.他表示对每一个流元素执行某种操作,最常见的就是输出操作.相当于在for循环下对每个流元素做点什么.但需要注意的是它的循环是无序的. 此外还有skip(). limit(). map()等方法,就不一一赘述了.
    Processed: 0.008, SQL: 9