除了Lambda表达式,JAVA8还出现了一些新特性来配合Lambda表达式进行简化编程,如函数式接口,今天要分享的是JAVA8又一大特性——Stream API,这是一个方便我们对数据操作的新特性。
所谓Stream,从字面理解是“流”的意思,个人理解是它充当的是一个类似管道的中间者,通过将数据在一端接收过来,进行一系列我们需要的操作,最后将结果从另外一端输出,成为我们最后需要的数据。由于是起中间作用,Stream本身不会存储元素,也不会改变数据源,而只有当我们需要结果的时候,stream才会执行
Stream操作数据大致分为三个步骤
一是创建Stream
二是进行中间操作
三是终止操作
今天主要说的是创建Stream以及中间操作的几种方式:
创建Stream
有这样四种方式:
1.通过collection系列集合提供的Stream()方法创建串行流或是通过parallelStream() 方法创建串行流
List<String> list=new ArrayList<>();
//Stream()创建串行流
Stream<String> stream1=list.stream();
//parallelStream() 创建并行流
Stream<String> stream2=list.parallelStream();
串行指的是一个线程做完所有事情,存在一个先后顺序,而并行流指的是多个线程做一件事。
2.通过Arrays中的静态方法stream()获取数组流
Student[] stus=new Student[10];
Stream<Student> stream2=Arrays.stream(stus);
3.通过Stream类中的静态方法 of()
Stream<String> stream3=Stream.of("aa","bb","cc");
4.创建无限流
无限流的两种形式:迭代和生成
迭代:
Stream<Integer> stream4=Stream.iterate(0,(x) -> x+2);
stream4.limit(10).forEach(System.out::println);
查看iterate()的源码,发现它需要两个参数
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
变量名为seed(种子)的参数,指的是迭代的开始值,而后面的UnaryOperator接口,继承自函数型接口Function,用于对初始值进行一系列操作,比如上面的代码中,我们对初始值0进行了10次自增2的操作,并进行输出
生成:
Stream.generate(()->Math.random())
.limit(5)
.forEach(System.out::println);*/
}
查看generate()方法源代码,发现:
public static<T> Stream<T> generate(Supplier<T> s)
参数是一个供给型接口,会返回一个类型为T的值
那么说完了如何创建Stream,我们来看看Stream如何进行中间操作的
中间操作
Stream的中间操作可以大致分为筛选与切片、排序以及映射等等,而筛选与切片又包含了几种方式,本次主要说的是筛选与切片
在筛选与切片中,存在四种常用的方式:
filter():过滤,接收一个lambda表达式,丛流中排除元素
limit(n):截断流,使其元素不超过指定的值,有点像mysql中的limit限制查询
skip(n):它与limit是一个互补的关系,用于跳过n个元素,返回第n个元素之后满足条件的元素,若流中的元素不足n个,则会返回一个空流
distinct():筛选,通过流生成元素的hashCode()和equals()去除重复元素
接下来我们逐个用简单的例子来进行演示:
首先是filter过滤:
假设我们有一个类型为Student对象的List集合,Student类的属性如下:
private Integer id;//编号
private String name;//姓名
private Integer age;//年龄
private Double height;//身高
List<Student> stus= Arrays.asList(
new Student(001,"张三",16,168.25),
new Student(002,"李四",48,178.25),
new Student(007,"赵六",18,180.68),
new Student(003,"王五",12,148.45)
);
查看filter()方法的源码:
Stream<T> filter(Predicate<? super T> predicate);
发现该方法是一个参数为断言型接口,返回值为Stream类型的方法
假如我们需要得到集合中年龄大于或等于35岁的学生:
public void test1(){
// 中间操作:不会执行任何操作
Stream<Student> stream=stus.stream()
.filter((e)->{
System.out.println("Stream API的中间操作");
return e.getAge()>=35;
});
stream.forEach((e)-> System.out.println(JSONObject.toJSON(e)));
}
首先我们创建一个类型为Student的Stream,通过filter()方法传入一个lambda表达式进行判断,为了让过程更加清晰,我们加上一个输出语句,最后使用终止操作forEach循环输出满足条件的对象,查看forEach方法的源码:
void forEach(Consumer<? super T> action);
发现该方法其实是在循环对一个消费型接口进行操作,在这里我们其实可以使用方法引用的方式进行输出内存地址,更为简洁,但为了方便观察,我们还是将传入的对象进行JSON处理,最后得出的结果:
从结果不难看出,我们并没有对filter方法中传入的参数e进行迭代处理,但Stream API自动帮我们完成迭代操作,而如果我们不写最后的终止操作forEach,将不会输出一句“Stream API的中间操作”,也就是说明,只有终止操作存在时才会执行所有的内容,我们称这种方法为“惰性求值”
当然我们也可以使用传统的外部迭代方式:
@Test
public void test2(){
Iterator<Student> iterator=stus.iterator();
while (iterator.hasNext()){
if (iterator.next().getAge()>=35){
System.out.println(iterator.next());
}
}
}
接下来是截断流limit,这里我们将筛选filter也加入
比如我们现在的student集合存在多个同样的学生“张三”信息,而我们需要得到两个身高大于160cm的学生信息。
我们先查看limit方法的源码:
limit:
Stream<T> limit(long maxSize);
传入一个长整作为最大值,返回一个自定义类型的Stream
接下来完成我们的功能:
@Test
public void test3(){
stus.stream()
.filter((e)->{
System.out.println("短路!");
return e.getHeight()>=160;
})
.limit(2)
.forEach((e)-> System.out.println(JSONObject.toJSON(e)));
}
运行结果:
集合中我们可以看到,满足身高大于160的学生一共有三位,而使用limit(2)后,只输出两条信息,同样的,该集合中有4个student对象,但Stream API只进行了2次迭代,这与短路操作符&& ||存在同样的效果。
接下来我们看看skip跳过操作,同样是上面的功能,我们将limit(2)替换成skip(2):
@Test
public void test4(){
stus.stream()
.filter((e)->e.getHeight()>=160)
.skip(2)
.forEach((e)-> System.out.println(JSONObject.toJSON(e)));
}
运行结果如下:
输出了上面我们limit(2)后第3个满足条件的学生信息
最后我们将筛选distinct()放入上面的limit例子中。注意,因为distinct去重是根据元素的hashCode以及equals方法,所以我们需要在实体类中重写这两个方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return getId().equals(student.getId()) &&
getName().equals(student.getName()) &&
getAge().equals(student.getAge()) &&
getHeight().equals(student.getHeight());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getName(), getAge(), getHeight());
}
比如我们的Student集合现在是这样:
List<Student> stus= Arrays.asList(
new Student(001,"张三",16,168.25),
new Student(001,"张三",16,168.25),
new Student(001,"张三",16,168.25),
new Student(001,"张三",16,168.25),
new Student(002,"李四",48,178.25),
new Student(007,"赵六",18,180.68),
new Student(003,"王五",12,148.45)
);
存在4个相同的“张三”,并且都满足身高大于160的条件,我们查看distinct方法的源码
distinct:
Stream<T> distinct();
没有任何参数,只返回一个自定义类型的Stream
@Test
public void test3(){
stus.stream()
.filter((e)->{
System.out.println("短路!");
return e.getHeight()>=160;
})
.distinct()
.limit(2)
.forEach((e)-> System.out.println(JSONObject.toJSON(e)));
}
加入distinct后,我们看看运行结果:
一共迭代5次,分别是前四个相同的“张三”以及“李四”,我们限制了元素为2,所以不会继续向下迭代