在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()等方法,就不一一赘述了.