java8——流

8 篇文章 0 订阅

背景

自从lambda表达式成为Java语言的一部分之后,Java集合(Collections)API就面临着大幅变化。而 JSR 355(规定了 Java lambda 表达式的标准)的正式启用更是使得 Java 集合 API 变的过时不堪。尽管我们可以从头实现一个新的集合框架(比如“Collection II”),但取代现有的集合框架是一项非常艰难的工作,因为集合接口渗透了 Java 生态系统的每个角落,将它们一一换成新类库需要相当长的时间。因此,我们决定采取演化的策略(而非推倒重来)以改进集合 API:

  • 为现有的接口(例如 Collection,List 和 Stream)增加扩展方法;
  • 在类库中增加新的 流(stream,即
    java.util.stream.Stream)抽象以便进行聚集(aggregation)操作;
  • 改造现有的类型使之可以提供流视图(stream view);
  • 改造现有的类型使之可以容易的使用新的编程模式,这样用户就不必抛弃使用以久的类库,例如 ArrayList 和 HashMap(当然这并不是说集合 API 会常驻永存,毕竟集合 API 在设计之初并没有考虑到 lambda 表达式。我们可能会在未来的 JDK 中添加一个更现代的集合类库)。

除了上面的改进,还有一项重要工作就是提供更加易用的并行(Parallelism)库。尽管 Java 平台已经对并行和并发提供了强有力的支持,然而开发者在实际工作(将串行代码并行化)中仍然会碰到很多问题。因此,我们希望 Java 类库能够既便于编写串行代码也便于编写并行代码,因此我们把编程的重点从具体执行细节(how computation should be formed)转移到抽象执行步骤(what computation should be perfomed)。除此之外,我们还需要在将并行变的 容易(easier)和将并行变的 不可见(invisible)之间做出抉择,我们选择了一个折中的路线:提供 显式(explicit)但 非侵入(unobstrusive)的并行。(如果把并行变的透明,那么很可能会引入不确定性(nondeterminism)以及各种数据竞争(data race)问题)

内部迭代和外部迭代(Internal vs external iteration)

集合类库主要依赖于 外部迭代(external iteration)。Collection 实现 Iterable 接口,从而使得用户可以依次遍历集合的元素。比如我们需要把一个集合中的形状都设置成红色,那么可以这么写:

for (Shape shape : shapes) {
  shape.setColor(RED);
}

这个例子演示了外部迭代:for-each 循环调用 shapes 的 iterator() 方法进行依次遍历。外部循环的代码非常直接,但它有如下问题:
Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;
集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。
尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。
我们可以使用 内部迭代(internal iteration)替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。下面是前例的内部迭代代码:

shapes.forEach(s -> s.setColor(RED));

尽管看起来只是一个小小的语法改动,但是它们的实际差别非常巨大。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。
外部迭代同时承担了 做什么(把形状设为红色)和 怎么做(得到 Iterator 实例然后依次遍历)两项职责,而内部迭代只负责 做什么,而把 怎么做 留给类库。通过这样的职责转变:用户的代码会变得更加清晰,而类库则可以进行各种优化,从而使所有用户都从中受益。

什么是流

流是Java API的新成员,它允许你以声明性方式处理数据(通过查询语句来表达,而不是临时编写一个实现)。简短的定义就是”从支持数据处理操作的源生成的元素序列”。进一步剖析:

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储的访问元素(如ArrayList和LinkList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。
  • 源——流会使用一个提供数据的源,如集合、数据或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。从来列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。
    此外,流操作有两个重要的特点。
  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线,流水线的操作可以看做对数据源进行数据库试查询
    内部迭代——与使用迭代器显示迭代的集合不同,流的迭代操作是在背后进行的。

此外,流还可以通明地并行处理,你无需写任何多线程代码。
我们先来看一下流的使用体验一下流带来的便利:
首先是Java 7中的集合遍历处理数据代码:

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: menu){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName());
}

如果用Java 8来实现上面的操作呢?我们来看下代码:

List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());

可以看到代码很简洁,对于数据的处理也是一口气直截了当,极大的提高了开发效率。如果数据量很大想要提升效率,那么就可以使用多和架构并行执行这段代码,只需要将stream换成parallelStream,如下

List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dishes::getCalories))
.map(Dish::getName)
.collect(toList());

在这里我们只是了解一下并行架构。我们再来看一下stream带来的好处:

  • 代码是以声明式方式写的:说明想要完成什么而不是说明如何实现一个操作,利用lamdba表达式是你的代码简单明了,不需要写过多的for循环。
  • 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线,同时保持代码清晰可读。filter的结果被传递给了sort方法,在传给map方法,最后传给collect方法。
    将流操作链接起来构成流的流水线
    需要注意流只能遍历一次
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
 s.forEach(System.out::println);
s.forEach(System.out::println);//执行第二次时会抛出异常

流中的操作

Stream中的stream接口定义了很多操作。他们可以分为两大类:可以连起来的流操作成为中间操作,如filter、map和limit可以连成一条流水线;关闭流的操作称为中断操作:如collect出发流水线执行并关闭它。
这里写图片描述

中间操作:

  • filter——接受Lambda,从流中排出某些元素
  • map——接受一个lambda,将元素转换成其他形式或提取信息
  • limit——截断流,使其元素不超过给定数量。
  • sorted——排序
  • distinct——过滤重复元素
操作类型返回类型操作参数函数描述符
filter中间Stream< T >Predicate< T >T -> boolean
map中间Stream< T >Function< T, R >T -> R
limit中间Stream< T >
sorted中间Stream< T >Compartator< T >(T, T) -> int
distinct中间Stream
skip(n)中间Stream< T >

注意:limit与skip互补

中断操作

操作类型返回类型使用的类型/函数式接口目的
forEach终端Consumer消费流中的每个元素并对其应用Lambda。这一操作返回void
count终端long返回流中元素的个数。这一操作返回long
collect终端RCollector把流归约成一个集合,比如List、Map甚至Integer。
anyMatch终端boolean
noneMatch终端boolean
allMatch终端boolean
findAny终端Optinal< T >
findFirst终端Optinal
reduce终端OptinalBinaryOperator
怎么判断是终端操作?
答:看最后一个操作返回类型是否为stream类型。

流中的映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在sql里,你可以从表中选择一列。Stream API也可以通过map和flatMap方法提供了类似的工具。
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的席位差别在于它是“创建一个新版本”而不是去“修改”)

String[] arrayOfWords = {"Goodbye", "world"};

Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
words.stream().map(word -> word.split("")).map(Arrays::stream).distinct().collect(toList());  //在这里每个数组都会变成一个单独的流

流的扁平化

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流

List< String > uniqueCharacters = words.stream().map(w -> w.split("")).flatMap(Arrays::stream).distinct().collect(toList()); //在这里每个生成流扁平化为单个流

简而言之就是flatmap方法让你把一个流中的每个值都转换成另一个流。

查找和匹配

StreamAPI通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。

1.检查为此是否至少匹配一个元素

if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

anyMatch方法返回一个boolean,因此是一个最终操作。

2.检查谓词是否至少匹配所有元素

allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
noneMatch
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。

boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
查找元素

findAny方法将返回当前流中的任意元素。

Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();   //使用optinal是因为可能什么元素都没找到
查找第一个元素
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst(); // 9
何时使用findFirst和findAny

如果并不关心返回的元素是哪个则用findAny,在并行的情况下。findAnyx效率高限制少
归约
1.求和
reduce: 一个初始值,这里是0; 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是

lambda (a, b) -> a + b
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int sum = numbers.stream().reduce(0, Integer::sum);

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

并行求和:只需要将stream改为parallelStream

原始类型流特化

java 8 引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将六中的元素特化为int,long和double,从而避免了暗含的装箱成本

1.映射到数值流

将流转换为特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。
例如,你可以像下面这样用mapToInt对menu中的卡路里求和:
int calories = menu.stream().mapToInt(Dish::getCalories).sum();

2.转换回对象流

同样,一旦有了数值流,你可能会想把它转换回非特化流。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //将Stream转换为数值流
Stream stream = intStream.boxed(); //将数值流转换为Stream

3.默认值OptinalInt

对于三种原始流特化,也分别有一个Optional原始类型特化版本: OptionalInt、 OptionalDouble和OptionalLong。
例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);

4.数值范围

比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0); 这里生产1到100中的50个偶数,而使用range则只有49个,不包括100

构建流

1.由值创建流

可以使用静态方法Stream.of创建一个流。它接受任意数量的参数。如:

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

你可以使用empty得到一个空流,如下所示:

Stream<String> emptyStream = Stream.empty();

2.由数组创建流

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

3.由文件生成流

long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
}

该方法中流会自动关闭

4.由函数生成流:创建无限流

Stream API提供了两个静态方法来从函数生成流: Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

1.迭代
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println); 
2.生成

与iterate方法类似, generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。我们先来看一个简单的用法:

Stream.generate(Math::random).limit(5).forEach(System.out::println);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值