为什么我们要使用 Stream ?
使用 Stream 对 集合中存储的数据 进行操作,可以写出更加高效、简洁且易于使用的数据处理方式。
Stream 的执行流程
- 创建流
- 一系列中间操作
- 终止操作
注意: Stream 中的操作是延迟的,需要最后一个终止操作才会执行运算,如 collect();
1.创建流的几种方式
使用 Collection 接口的默认方法,处理集合中的数据
List<String> collection=Collections.EMPTY_LIST;
Stream<String> collectionStream = collection.stream();
使用 Arrays.stream(),处理数组类型的数据
IntStream stream = Arrays.stream(new int[]{1, 2, 3, 4});
使用用例中的 Stream.of(),合并同类型的数据集生成流
创建无限流,生成需要的数据
//迭代10次,添加以2开始的偶数到集合中
List<Integer> collect = Stream.iterate(2, t -> t + 2).limit(10).collect(Collectors.toList());
System.out.println(collect.toString());
//生成10个随机数并打印
Stream.generate(Math::random).limit(10).forEach(System.out::println);
2. 一系列中间操作
Ⅰ 筛选与切片
- filter( e->… …):从流中保留返回 true 的数据,eg:过滤空字符串filter(StringUtils::isNotEmpty)
- limit( n ):从流中拿到前 n 个元素
- skip( n ):从流中跳过前 n 个元素
- distinct():去除流中重复的元素。注意去重时,需要对象的 hashCode 和 equals 方法。
Ⅱ 映射
- map( e->…… ):将集合中的元素转换为其他类型的元素。
与 mapToLong、mapToDouble、mapToInt 类似,但他们返回LongStream … ,而 map 返回 Stream<(lambda返回的类型)> - flatMap( e->…… ):将流 e 转换为其他类型的流,并将所有的流合并为一个流
Ⅲ 排序
- sorted():按集合或者类中实现的 Comparable 接口进行排序
- sorted(Comparator com):按自定义的 com 对象进行排序
public static void main(String[] args) {
ArrayList<ForTest> list = new ArrayList<>();
list.add(new ForTest("3",3));
list.add(new ForTest("c",2));
list.add(new ForTest("a",2));
list.add(new ForTest("11",1));
list.stream()
.sorted((e1,e2)->{
//前-后为升序
return e1.b==e2.b?e1.a.compareTo(e2.a):e1.b-e2.b;
})
.forEach(System.out::println);
}
static class ForTest {
String a;
int b;
public ForTest(String s, int i) {
a = s;
b = i;
}
int getInt() {
return b;
}
@Override
public String toString() {
return "ForTest{" +
"a='" + a + '\'' +
", b=" + b +
'}';
}
}
3. 终止操作
Ⅰ 匹配查找
- allMatch( e->…… ):检查集合中所有元素是否匹配,返回一个 boolean 类型。有一个元素返回 false ,返回结果就为 false 。
- anyMatch( e->…… ):检查集合中至少有一个元素匹配,返回一个 boolean 类型。有一个元素返回 true ,返回结果就为 true 。
- noneMatch( e->…… ):检查集合中没有元素匹配,返回一个 boolean 类型。所有元素返回 flase 时,返回结果为 true。
- findFirst():找到第一个元素。给到该操作的数据必须不为空,否则抛出 NPE 异常。
当 map 的值为 NULL 时,findFirst 将抛出 NPE;当 list 为空是,不会执行到 findFirst 函数 - findAny():找到任意一个元素。NPE 的抛出同 findFirst 。
- count():找到流中元素的个数
- max():返回流中最大的元素
- min():返回流中最小的元素
- forEach():内部迭代
Ⅱ 归约
- reduce( identity, BinaryOperator ):将流中的元素反复结合起来,计算出一个值。返回一个 T
Integer reduce = Stream.of(1, 2, 3, 4)
.reduce(5, Integer::sum);
System.out.println(reduce);//15
- reduce( BinaryOperator ):
//ForTest类在上面的例子中
Optional<Integer> optional = list.stream()
.map(ForTest::getInt)
.reduce(Integer::sum);
System.out.println(optional.get());//8
Ⅲ 收集
collect( Collectors. ):收集流为 toList()、toSet()、joining(delimiter,prefix,suffix) 等
eg: 传统 for 循环的函数式替代方案
场景:现需要将三个拥有共同父类的 Enum 对象中的值,封装到另外一个对象的 List 集合中去
解决方案:
- 使用传统的外部迭代 for 循环解决:第一层 for 循环三个 Enum 对象,第二层循环每一个 Enum 对象,完成对象的转换。缺点显而易见,嵌套 for 循环带来的时间复杂度非常高,可读性差等。
- 使用流进行内部迭代,配合 Lambda 完成遍历功能,如下代码所示。优点:代码量少,可读性增强。
//三个 Enum 类是需要读取信息的类
List<FourDetection> list = Stream.of(GdDetectionProjectMapping.values(), YJDetectionProjectMapping.values(), BCDetectionProjectMapping.values())
.flatMap(e -> Arrays.stream(e))//合并流,合并后,其类型还是DetectionMapping
.map(e->toFour(e))//转化DetectionMapping为FourDetection流
.filter(i->isAddToList(i,set))//过滤元素是否添加到list中
.collect(Collectors.toList());//收集流转为list集合
//读取 DetectionMapping 的信息封装到 FourDetection 中
private FourDetection toFour(DetectionMapping value) {
FourDetection fourDetection = new FourDetection();
fourDetection.setDetectionProject(value.toString());
fourDetection.setDetectionCode(value.getName());
fourDetection.setFourDetection(value.getDetectionClass().getValue());
fourDetection.setDetectionType(value.getDetectionType().getValue());
fourDetection.setDetectionDescription(value.getDescription());
fourDetection.setCheckProject("true");
fourDetection.setLinkType(value.getDetectionLinkType().getValue());
return fourDetection;
}
//Set 为已有的 FourDetection 集合
private boolean isAddToList(FourDetection fourDetection, Set<FourDetection> set) {
if (!set.contains(fourDetection)) {
return true;
}
return false;
}
什么是外部迭代?什么是内部迭代?谁的效率高?
外部迭代:意为显式地进行迭代操作,即集合中的元素访问是由一个处于集合外部的东西来控制的,如控制着循环的迭代器
内部迭代:遍历将在集合内部进行,我们不会显式地去控制这个循环
/**
size 1000 10_000 1_000_000
外部迭代 1 3 102
内部迭代 73 109 140
*/
public static void main(String[] args) {
final long count = 10_000_000;
List<Long> list = new ArrayList<>((int)count);
for (long i = 0; i < count; i++) {
list.add(i);
}
//=========传统方式进行外部迭代=========
Instant begin = Instant.now();
for (Long i : list) {
System.out.print("");
}
System.out.println("--------------------------");
Instant end = Instant.now();
System.out.println("传统方式进行外部迭代" + count + "次,耗时(ms):" + Duration.between(begin, end).toMillis());
//=========java8内部迭代,用lambda处理=========
begin = Instant.now();
list.stream().forEach(i -> System.out.print(""));
System.out.println("--------------------------");
end = Instant.now();
System.out.println("内部迭代forEach" + count + "次,耗时(ms):" + Duration.between(begin, end).toMillis());
}
Lambda 中使用外部局部变量,为什么要 final?
结论:
为了防止 Lambda 内外部使用数据不一致而规定的。
Lambda 中使用的局部变量是通过值拷贝方式得到的(引用类型拷贝的是指向堆中的地址值),为避免在声明 Lambda 后(即完成了数据拷贝)修改变量值,导致 Lambda 真正执行时,内外部数据不同步的问题。所以规定 Lambda 使用的外部局部变量为 final 类型,禁止修改其值。
JDK 8中,Lambda 使用的外部局部变量为什么没有显示的声明 final ?
int i=888;
new Thread(
() -> {
System.out.println("lambda runnable interface"+i);//此处访问i后,i的就变为final类型了
}
).start();
//i=1;//此处会出现编译错误
局部变量未声明为 final 类型,一旦在 Lambda 中使用了,就会被强行加上了 final 属性,所以后面就不允许修改该 局部变量 了
声明的变量为 final,如果想要要从 Lambda 内向外传值该怎么做?
int[] i = {8};
new Thread(
() -> {
System.out.println("lambda runnable interface "+ i[0]);//lambda runnable interface 8
i[0] =10;
//i={8};//由于i已经是final类型了,所以此处会出现编译错误
}
).start();
TimeUnit.SECONDS.sleep(1);//保障thread中的代码被执行到
System.out.println(i[0]);//10
如上例所示,使用引用类型的变量,操作引用类型指向堆中地址的值,就可以把 Lambda 中的值传递到外部了。
Optional
orElse 与 orElseGet 的区别
orElse 方法传入的应该是默认值,所以如果在 orElse 中如果传入执行语句,那么该函数一定会被执行。
但如果使用 orElseGet ,如果不会走到 orElseGet,那么其中的函数不会别执行。