Stream
stream简介
- Java 8引入了全新的Stream API。这里的Stream和I/O流不同,它更像具有Iterable的集合类,但行为和集合类又有所不同。
- stream是对集合对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。
- 只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
为什么需要 Stream
- 函数式编程带来的好处尤为明显。这种代码更多地表达了业务逻辑的意图,而不是它的实现机制。易读的代码也易于维护、更可靠、更不容易出错。
- 高端
Filter
- 遍历数据并检查其中的元素时使用。
- filter接受一个函数作为参数,该函数用Lambda表达式表示。
/**
* 过滤所有的男性
*/
public static void filterSex(){
List<PersonModel> data = Data.getData();
//old
List<PersonModel> temp=new ArrayList<>();
for (PersonModel person:data) {
if ("男".equals(person.getSex())){
temp.add(person);
}
}
System.out.println(temp);
//new
List<PersonModel> collect = data
.stream()
.filter(person -> "男".equals(person.getSex()))
.collect(toList());
System.out.println(collect);
}
/**
* 过滤所有的男性 并且小于20岁
*/
public static void filterSexAndAge(){
List<PersonModel> data = Data.getData();
//old
List<PersonModel> temp=new ArrayList<>();
for (PersonModel person:data) {
if ("男".equals(person.getSex())&&person.getAge()<20){
temp.add(person);
}
}
//new 1
List<PersonModel> collect = data
.stream()
.filter(person -> {
if ("男".equals(person.getSex())&&person.getAge()<20){
return true;
}
return false;
})
.collect(toList());
//new 2
List<PersonModel> collect1 = data
.stream()
.filter(person -> ("男".equals(person.getSex())&&person.getAge()<20))
.collect(toList());
}
Map
- map生成的是个一对一映射,for的作用
- 比较常用
- 而且很简单
/**
* 取出所有的用户名字
*/
public static void getUserNameList(){
List<PersonModel> data = Data.getData();
//old
List<String> list=new ArrayList<>();
for (PersonModel persion:data) {
list.add(persion.getName());
}
System.out.println(list);
//new 1
List<String> collect = data.stream().map(person -> person.getName()).collect(toList());
System.out.println(collect);
//new 2
List<String> collect1 = data.stream().map(PersonModel::getName).collect(toList());
System.out.println(collect1);
//new 3
List<String> collect2 = data.stream().map(person -> {
System.out.println(person.getName());
return person.getName();
}).collect(toList());
}
FlatMap
-
顾名思义,跟map差不多,更深层次的操作
-
但还是有区别的
-
map和flat返回值不同
-
Map 每个输入元素,都按照规则转换成为另外一个元素。
-
还有一些场景,是一对多映射关系的,这时需要 flatMap。
-
Map一对一
-
Flatmap一对多
map和flatMap的区别:我个人认为,flatMap的可以处理更深层次的数据,入参为多个list,结果可以返回为一个list,而map是一对一的,入参是多个list,结果返回必须是多个list。通俗的说,如果入参都是对象,那么flatMap可以操作对象里面的对象,而map只能操作第一层。
public static void flatMapString() {
List<PersonModel> data = Data.getData();
//返回类型不一样
List<String> collect = data.stream()
.flatMap(person -> Arrays.stream(person.getName().split(" "))).collect(toList());
List<Stream<String>> collect1 = data.stream()
.map(person -> Arrays.stream(person.getName().split(" "))).collect(toList());
//用map实现
List<String> collect2 = data.stream()
.map(person -> person.getName().split(" "))
.flatMap(Arrays::stream).collect(toList());
//另一种方式
List<String> collect3 = data.stream()
.map(person -> person.getName().split(" "))
.flatMap(str -> Arrays.asList(str).stream()).collect(toList());
}
Reduce
- 感觉类似递归
- 数字(字符串)累加
- 感觉很少用到
public static void reduceTest(){
//累加,初始化值是 10
Integer reduce = Stream.of(1, 2, 3, 4)
.reduce(10, (count, item) ->{
System.out.println("count:"+count);
System.out.println("item:"+item);
return count + item;
} );
System.out.println(reduce);
Integer reduce1 = Stream.of(1, 2, 3, 4)
.reduce(0, (x, y) -> x + y);
System.out.println(reduce1);
String reduce2 = Stream.of("1", "2", "3")
.reduce("0", (x, y) -> (x + "," + y));
System.out.println(reduce2);
}
Collect
- collect在流中生成列表,map,等常用的数据结构
- toList()
- toSet()
- toMap()
- 自定义
/**
* toList
*/
public static void toListTest(){
List<PersonModel> data = Data.getData();
List<String> collect = data.stream()
.map(PersonModel::getName)
.collect(Collectors.toList());
}
/**
* toSet
*/
public static void toSetTest(){
List<PersonModel> data = Data.getData();
Set<String> collect = data.stream()
.map(PersonModel::getName)
.collect(Collectors.toSet());
}
/**
* toMap
*/
public static void toMapTest(){
List<PersonModel> data = Data.getData();
Map<String, Integer> collect = data.stream()
.collect(
Collectors.toMap(PersonModel::getName, PersonModel::getAge)
);
data.stream()
.collect(Collectors.toMap(per->per.getName(), value->{
return value+"1";
}));
}
/**
* 指定类型
*/
public static void toTreeSetTest(){
List<PersonModel> data = Data.getData();
TreeSet<PersonModel> collect = data.stream()
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(collect);
}
/**
* 分组
*/
public static void toGroupTest(){
List<PersonModel> data = Data.getData();
Map<Boolean, List<PersonModel>> collect = data.stream()
.collect(Collectors.groupingBy(per -> "男".equals(per.getSex())));
System.out.println(collect);
}
/**
* 分隔
*/
public static void toJoiningTest(){
List<PersonModel> data = Data.getData();
String collect = data.stream()
.map(personModel -> personModel.getName())
.collect(Collectors.joining(",", "{", "}"));
System.out.println(collect);
}
/**
* 自定义
*/
public static void reduce(){
List<String> collect = Stream.of("1", "2", "3").collect(
Collectors.reducing(new ArrayList<String>(), x -> Arrays.asList(x), (y, z) -> {
y.addAll(z);
return y;
}));
System.out.println(collect);
}
Optional
- Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。
- 人们对原有的 null 值有很多抱怨,甚至连发明这一概念的Tony Hoare也是如此,他曾说这是自己的一个“价值连城的错误”
- 用处很广,不光在lambda中,哪都能用
- Optional.of(T),T为非空,否则初始化报错
- Optional.ofNullable(T),T为任意,可以为空
- isPresent(),相当于 !=null
- ifPresent(T), T可以是一段lambda表达式 ,或者其他代码,非空则执行
public static void main(String[] args) {
PersonModel personModel=new PersonModel();
//对象为空则打出 -
Optional<Object> o = Optional.of(personModel);
System.out.println(o.isPresent()?o.get():"-");
//名称为空则打出 -
Optional<String> name = Optional.ofNullable(personModel.getName());
System.out.println(name.isPresent()?name.get():"-");
//如果不为空,则打出xxx
Optional.ofNullable("test").ifPresent(na->{
System.out.println(na+"ifPresent");
});
//如果空,则返回指定字符串
System.out.println(Optional.ofNullable(null).orElse("-"));
System.out.println(Optional.ofNullable("1").orElse("-"));
//如果空,则返回 指定方法,或者代码
System.out.println(Optional.ofNullable(null).orElseGet(()->{
return "hahah";
}));
System.out.println(Optional.ofNullable("1").orElseGet(()->{
return "hahah";
}));
//如果空,则可以抛出异常
System.out.println(Optional.ofNullable("1").orElseThrow(()->{
throw new RuntimeException("ss");
}));
// Objects.requireNonNull(null,"is null");
//利用 Optional 进行多级判断
EarthModel earthModel1 = new EarthModel();
//old
if (earthModel1!=null){
if (earthModel1.getTea()!=null){
//...
}
}
//new
Optional.ofNullable(earthModel1)
.map(EarthModel::getTea)
.map(TeaModel::getType)
.isPresent();
// Optional<EarthModel> earthModel = Optional.ofNullable(new EarthModel());
// Optional<List<PersonModel>> personModels = earthModel.map(EarthModel::getPersonModels);
// Optional<Stream<String>> stringStream = personModels.map(per -> per.stream().map(PersonModel::getName));
//判断对象中的list
Optional.ofNullable(new EarthModel())
.map(EarthModel::getPersonModels)
.map(pers->pers
.stream()
.map(PersonModel::getName)
.collect(toList()))
.ifPresent(per-> System.out.println(per));
List<PersonModel> models=Data.getData();
Optional.ofNullable(models)
.map(per -> per
.stream()
.map(PersonModel::getName)
.collect(toList()))
.ifPresent(per-> System.out.println(per));
}
Stream 流的处理顺序
上面已经了解stram的基本用法,接下来了解一下stream执行顺序
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {
System.out.println("filter: " + s);
return true;
});
执行此代码段时,会有什么效果呢?
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {
System.out.println("filter: " + s);
return true;
}).forEach(s -> System.out.println("forEach: " + s));
执行这代码段时,又会有什么效果呢?
是不是很神奇?为什么要设计成这样呢?
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A"); // 过滤出以 A 为前缀的元素
});
终端操作 anyMatch()表示任何一个元素以 A 为前缀,返回为 true,就停止循环。所以它会从 d2 开始匹配,接着循环到 a2 的时候,返回为 true ,于是停止循环。
因为流的链式调用是垂直执行的,map这里只需要执行两次。相对于水平执行来说,map会执行尽可能少的次数,而不是把所有元素都 map 转换一遍。
如果先进行过滤操作,可以大大的减少执行次数 map只用执行一次
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s)
return s.startsWith("a"); // 过滤出以 a 为前缀的元素
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.forEach(s -> System.out.println("forEach: " + s));
代码再添加一个中间操作 sorted,执行顺序又是怎么样的呢?
Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2); // 排序
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a"); // 过滤出以 a 为前缀的元素
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
上面运行结果可以看出,这次又不是垂直执行的了呢?sorted 是排序,需要在排序完成后,对结果进行下一步操作,所以sorted是水平执行的,sorted排序进行了8次元素组合排序,这里我们可以先利用filter操作先进过滤,再进行排序,进而减少执行次数
parallelStream
流是可以并行执行的,当流中存在大量元素时,可以显著提升性能。并行流底层使用的ForkJoinPool, 它由ForkJoinPool.commonPool()方法提供。底层线程池的大小最多为五个 - 具体取决于 CPU 可用核心数:
利用上面的代码使用 parallelStream,打印当前线程的信息:
Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
添加中间操作sort来扩展上面示例:
Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.sorted((s1, s2) -> {
System.out.format("sort: %s <> %s [%s]\n",
s1, s2, Thread.currentThread().getName());
return s1.compareTo(s2);
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
貌似sort只在主线程上串行执行。但是实际上,并行流中的sort在底层使用Java8中新的方法Arrays.parallelSort()。如 javadoc官方文档解释的,这个方法会按照数据长度来决定以串行方式,或者以并行的方式来执行。
如果指定数据的长度小于最小数值,它则使用相应的Arrays.sort方法来进行排序。
Effective Java 45.明智审慎地使用 Stream
考虑以下程序,该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词(anagram)组。如果两个单词由长度相通,不同顺序的相同字母组成,则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词,因此「staple」的键是「aelpst」,「petals」的键也是「aelpst」:这两个单词就是同位词,所有的同位词共享相同的依字母顺序排列的形式(或称之为 alphagram)。
map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 values() 的视图并打印每个大小符合阈值的列表:
// 普通方式
// 读取文件中的单词,检查单词的字母,相同字母的单词收集在一起
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
}
for (Set<String> group : groups.values())
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
// 过度使用流:虽然很简洁,但是对流不了解的开发人员可能无法理解。
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString())
)
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
// 合适使用流方式
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(group -> System.out.println(group.size() + ": " + group));
}
}
- 字母排序方法抽取出来增加程序的可读性。
- lambda中参数的命名尤为重要,好的命名能够提升可读性。
- 也许大家都希望使用lambda来消灭循环,但实际是不可取的(元素少时lambda存在性能问题)。
Stream的优缺点
- Stream的缺点
- 代码块能够读取或修改范围内的局部变量,lambda只能操作final变量和当前范围的局部变量。
- 代码块中能够return、抛出异常、跳出循环或是跳过循环,lambda中都无法做到。
- Stream的优点
- map:统一转换元素类型
- filter:过滤序列
- min、compute:计算最小值、合并序列等
- reduce:累计序列
- grouping:分组
优先选择流中无副作用的功能
概述:
- 为了得到stream的表现力、速度和并行度,我们必须遵守范式和使用API。
- stream范式最重要的部分:计算 -> 转换 ,每个转换(中间或终止操作)都是纯函数。
- 纯函数应该都是无副作用的(不依赖任何可变状态,不更新任何状态)。
谨慎使用并行流原则
//打印素数
public static void main(String[] args) {
long start = System.currentTimeMillis();
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
long end = System.currentTimeMillis();
System.out.println((end - start) / 1000);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
这个程序立即开始打印素数,运行到完成需要 12.5 秒。假设我天真地尝试通过向流管道中添加一个到 parallel() 的调用来加快速度。你认为它的表现会怎样?它会快几个百分点吗?慢几个百分点?遗憾的是,它不会打印任何东西,但是 CPU 使用率会飙升到 90%,并且会无限期地停留在那里 (liveness failure:活性失败)。
利用并行流把一个list里面的数据导入到另外一个list中,看起来看简单,接下来我们看一下执行结果
try {
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};
List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
List<Integer> parallelStorage = new ArrayList<>();
listOfIntegers
.parallelStream()
.map(e -> {
parallelStorage.add(e);
return e;
})
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println();
System.out.println("更改后的集合" + parallelStorage);
parallelStorage
.stream()
.forEachOrdered(e -> System.out.print(e + " "));
} catch (Exception e) {
e.printStackTrace();
}
运行结果:
Arraylist本身底层是一个数组,多线程并发下线程并不安全,操作出现的原因无非就是多个线程赋值可能同时操作同一个地址,后赋值的把先赋值的给覆盖掉了,才会出现这种问题。
-
ArrayList、HashMap、HsahSet、CouncurrentHashMap、数组、int范围流和long范围流的并行性性能效益最佳。
-
数组存储的元素在内存中相近,数据定位更快。而上面涉及的数据结构基本都基于数组实现。
-
流的终止操作会影响并行执行的有效性。而流的reduce操作或预先打包(min、max、count和sum)是并行流的最佳实践。
-
流的中间操作(anyMatch、allMatch和noneMatch)也适合并行操作。
流的collect操作则不适合。 -
自己实现Stream、Iterable或Collection且希望有良好的并行性能,则需要覆盖spliterator方法。
-
并行流是基于fork-join池实现的。
-
当无法写出正确的并行流,将导致异常或者错误的数据。
注:程序的安全性、正确性比性能更重要。
参考文章:
45. 明智审慎地使用 Stream
48. 谨慎使用流并行
java8的ParallelStream踩坑记录
[译] 一文带你玩转 Java8 Stream 流,从此操作集合 So Easy
Java 8 中的 Streams API 详解