java8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda,给我们操作集合(Collection)提供了极大的便利。Stream操作就是将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream可以由数组或集合创建,对流的操作分为两种:中间操作和终止操作。
-
中间操作/函数拼接,返回值类型仍然是 Stream 类型的方法,支持链式调用。(筛选filter、映射map、排序sorted、去重组合skip—limit)
-
终止/终端操作,返回值类型不再是 Stream 类型的方法,不再支持链式调用。终止/终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect)
注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
1、Stream常用方法及注意事项
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
1.1 常用方法
Stream流模型的操作很丰富,Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。下图列出一些常用的API。可以分成两类:中间操作/函数拼接,终止/终端操作。
中间操作符
对于数据流来说,中间操作符在执行制定处理程序后,数据流依然可以传递给下一级的操作符。
中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):
- map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
- flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
- limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
- distint 去重操作,对重复元素去重,底层使用了equals方法。
- filter 过滤操作,把不想要的数据过滤。
- peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
- skip 跳过操作,跳过某些元素。
- sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。
终止操作符
数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。
- collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
- count 统计操作,统计最终的数据个数。
- findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
- noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
- min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
- reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
- forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
- toArray 数组操作,将数据流的元素转换成数组。
1.2 注意事项
在使用Stream时有以下几个注意事项:
- stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
- stream不会改变数据源,通常情况下会产生一个新的集合或一个值。(返回的是新的流)
- stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。(不调用终结方法,中间的操作不会执行,即Stream只能执行一次)
2、Stream与传统遍历对比
几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。接下来我们就从集合遍历方面比较一下传统遍历方式及Stream方式。
在一个ArrayList集合中存储有以下数据: 张无忌、周芷若、赵敏、小昭、殷离、张三、张三丰。
需求1: 获取所有姓张的人员名单
需求2: 获取名字长度为3个字的
需求3: 将上述条件下获取到的数据打印出来
2.1 传统遍历
import java.util.ArrayList;
import java.util.List;
public class Demo1List {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("小昭");
list.add("殷离");
list.add("张三");
list.add("张三丰");
List<String> listA = new ArrayList<>();
for ( String s : list) {
if (s.startsWith("张") && s.length() == 3) {
listA.add(s);
}
}
for (String s: listA) {
System.out.println(s);
}
}
}
传统循环遍历的弊端
这段代码中含有2个循环,每一个作用不同:
-
首先筛选所有姓张的且名字有三个字的人;(有些写法会将这2个条件分别写2个循环)
-
最后对结果进行打印输出。
可以看出每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使用另一个循环从头开始。
Java 8的Lambda更加专注于做什么(What),而不是怎么做(How)。
循环 是做事情的方式,而不是目的。for循环的语法是“怎么做”,for循环的循环体才是“做什么”。
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
有没有更好的方式呢,答案是肯定的,可以参考对比下面的Stream方式。
2.2 Stream
import java.util.ArrayList;
import java.util.List;
public class Demo2Steam {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("小昭");
list.add("殷离");
list.add("张三");
list.add("张三丰");
list.stream()
.filter(name -> name.startsWith("张"))
.filter(name -> name.length() == 3)
.forEach(name -> System.out.println(name));
}
}
效果显而易见,首先代码更加简洁,其次代码更易理解,直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:
- 获取流
- 过滤姓张
- 过滤长度为3
- 逐一打印。
我们真正要做事情的内容被更好地体现在代码中。
2.3 原始集合操作与Stream集合操作对比
再举一个比较全面的对比例子。原始集合操作与Stream集合操作在 “过滤/映射/扁平化/遍历/排序/去重/跳过/截断” 的应用区别。
举例: 打印所有商品、图书类过滤掉、排序、要前面两个、求两件商品的总价、获取两件商品的名称、打印输入结果
1)以原始集合操作实现需求
public void oldCartHandle() {
//可以理解为获取数组对象为cartSkuList
List<Sku> cartSkuList = CartService.getCartSkuList();
/**
* 1 打印所有商品
*/
for (Sku sku : cartSkuList) {
System.out.println(JSON.toJSONString(sku, true));
}
/**
* 2 图书类过滤掉
*/
List<Sku> notBooksSkuList = new ArrayList<Sku>();
for (Sku sku : cartSkuList) {
if (!SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory())) {
notBooksSkuList.add(sku);
}
}
/**
* 排序
*/
notBooksSkuList.sort(new Comparator<Sku>() {
@Override
public int compare(Sku sku1, Sku sku2) {
if (sku1.getTotalPrice() > sku2.getTotalPrice()) {
return -1;
} else if (sku1.getTotalPrice() < sku2.getTotalPrice()) {
return 1;
} else {
return 0;
}
}
});
/**
* 前面两个
*/
List<Sku> top2SkuList = new ArrayList<Sku>();
for (int i = 0; i < 2; i++) {
top2SkuList.add(notBooksSkuList.get(i));
}
/**
* 求两件商品的总价
*/
Double money = 0.0;
for (Sku sku : top2SkuList) {
// money = money + sku.getTotalPrice();
money += sku.getTotalPrice();
}
/**
* 获取两件商品的名称
*/
List<String> resultSkuNameList = new ArrayList<String>();
for (Sku sku : top2SkuList) {
resultSkuNameList.add(sku.getSkuName());
}
/**
* 打印输入结果
*/
System.out.println(JSON.toJSONString(resultSkuNameList, true));
System.out.println("商品总价:" + money);
}
2)以Stream流方式实现需求
public void newCartHandle() {
//多线程安全,防止多线程计数出现冲突,用于计算金额而声明的
AtomicReference<Double> money =
new AtomicReference<>(0.0);
//CartService.getCartSkuList()可以理解为获取数组对象随后进入流操作
List<String> resultSkuNameList =
CartService.getCartSkuList()
.stream()
/**
* 1 打印商品信息
*/
.peek(sku -> System.out.println(
JSON.toJSONString(sku, true)))
/**
* 2 过滤掉所有图书类商品
*/
.filter(sku -> !SkuCategoryEnum.BOOKS.equals(
sku.getSkuCategory()))
/**
* 排序
*/
.sorted(Comparator.
comparing(Sku::getTotalPrice).reversed())
/**
* TOP2
*/
.limit(2)
/**
* 累加商品总金额
*/
.peek(sku -> money.set(money.get() + sku.getTotalPrice()))
/**
* 获取商品名称
*/
.map(sku -> sku.getSkuName())
/**
* 收集结果
*/
.collect(Collectors.toList());
/**
* 打印输入结果
*/
System.out.println(JSON.toJSONString(resultSkuNameList, true));
System.out.println("商品总价:" + money.get());
}
3、Stream的创建
Stream可以通过集合数组创建。
3.1 Stream的3种创建方式
1)通过 java.util.Collection.stream() 方法用集合创建流
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
2)使用 java.util.Arrays.stream(T[] array)
方法用数组创建流
int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);
3)使用Stream的静态方法:of()、iterate()、generate()
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);
3.2 stream和parallelStream的区分
stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:
- 如果流中的数据量足够大,并行流可以加快处速度。
- 除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流。
Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();
List<Integer> list = Arrays.asList(1, 3, 6, 8, 12, 4);
Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();
System.out.println("使用Stream的静态方法generate:" + findFirst.get());
4、Stream常用操作使用示例
4.1 stream、collect方法
//将List转化成stream流;
List.stream();
//将List按照转化规则转化成stream流;
Stream.collect(转化规则);
4.2 转换成其他容器
1. 转化成新的List
List<> new = old.stream().collect(Collectors.toList());
2. 转化成Map
Map<T,E> new= old.stream().collect(Collectors.toMap(T,E));
3. 转换成set集合
Set<> new =old.stream().Collectors.toSet();
4. 转换成特定的set集合
TreeSet<> new =old.stream().Collectors.toCollection(TreeSet::new);
4.3 常用方法
1. 过滤
保留Stream流中满足filter()中的条件的元素转换成新List,例中条件为id=5的元素。
old.stream().filter(f->f.id==5);
2. 映射
将Stream流中的元素映射成新的Stream流,例中为从f映射到f的id字段。
old.stream().map(f.getid());
3. 计算
下例中为求所有元素的和。可以理解为:
- sum = stream流中第一个元素
- sum = reduce中方法(sum,下一个元素)
- return sum
old.stream().reduce((a, b) -> a + b);
下例中为求所有元素的和并加上1。可以理解为:
- sum = reduce中第一个参数
- sum = reduce中方法(sum,下一个元素)
- return sum
old.stream().reduce(1,(a, b) -> a + b);
4. 取前几个元素
例中为取前四个元素
old.stream().limit(4);
5. 排序
按照sorted中方法对元素排序,例中为按照元素的weight排序。
old.stream().sorted((f1, f2) -> Integer.compare(f2.getWeight(), f1.getWeight()));
6. 去重
old.stream().distinct();
7. 查找
判断是否存在满足条件的元素
old.stream().anyMatch(f -> f.getid() == 5);
返回第一个满足条件的元素
old.stream().findFirst(f -> f.getid() == 5);
评论区