4.2 Stream
新增加的Stream API (java.util.stream)引入了在Java里可以工作的函数式编程。这是目前为止对java库最大的一次功能添加,希望程序员通过编写有效、整洁和简明的代码,能够大大提高生产率。
Stream API让集合处理简化了很多(我们后面会看到不仅限于Java集合类)。让我们从一个简单的类Task开始来看看Stream的用法。
01 |
public class Streams {
|
06 |
private static final class Task {
|
07 |
private final Status status;
|
08 |
private final Integer points;
|
10 |
Task( final Status status, final Integer points ) {
|
15 |
public Integer getPoints() {
|
19 |
public Status getStatus() {
|
24 |
public String toString() {
|
25 |
return String.format( "[%s, %d]" , status, points );
|
Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子:
1 |
final Collection< Task > tasks = Arrays.asList(
|
2 |
new Task( Status.OPEN, 5 ),
|
3 |
new Task( Status.OPEN, 13 ),
|
4 |
new Task( Status.CLOSED, 8 )
|
第一个问题是所有的开放的Task的点数是多少?在java 8 之前,通常的做法是用foreach迭代。但是Java8里头我们会用Stream。Stream是多个元素的序列,支持串行和并行操作。
1 |
// Calculate total points of all active tasks using sum() |
2 |
final long totalPointsOfOpenTasks = tasks
|
4 |
.filter( task -> task.getStatus() == Status.OPEN )
|
5 |
.mapToInt( Task::getPoints )
|
8 |
System.out.println( "Total points: " + totalPointsOfOpenTasks );
|
控制台的输出将会是:
Total points: 18
上面代码执行的流程是这样的,首先Task集合会被转化为Stream表示,然后filter操作会过滤掉所有关闭的Task,接下来使用Task::getPoints 方法取得每个Task实例的点数,mapToInt方法会把Task Stream转换成Integer Stream,最后使用Sum方法将所有的点数加起来得到最终的结果。
在我们看下一个例子之前,我们要记住一些关于Stream的说明。Stream操作被分为中间操作和终点操作。
中间操作返回一个新的Stream。这些中间操作是延迟的,执行一个中间操作比如filter实际上不会真的做过滤操作,而是创建一个新的Stream,当这个新的Stream被遍历的时候,它里头会包含有原来Stream里符合过滤条件的元素。
终点操作比如说forEach或者sum会遍历Stream从而产生最终结果或附带结果。终点操作执行完之后,Stream管道就被消费完了,不再可用。在几乎所有的情况下,终点操作都是即时完成对数据的遍历操作。
Stream的另外一个价值是Stream创造性地支持并行处理。让我们看看下面这个例子,这个例子把所有task的点数加起来。
1 |
// Calculate total points of all tasks |
2 |
final double totalPoints = tasks
|
5 |
.map( task -> task.getPoints() ) // or map( Task::getPoints )
|
6 |
.reduce( 0 , Integer::sum );
|
8 |
System.out.println( "Total points (all tasks): " + totalPoints );
|
这个例子跟上面那个非常像,除了这个例子里使用了parallel()方法 并且计算最终结果的时候使用了reduce方法。
输出如下:
Total points (all tasks): 26.0
经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子:
1 |
// Group tasks by their status |
2 |
final Map< Status, List< Task > > map = tasks
|
4 |
.collect( Collectors.groupingBy( Task::getStatus ) );
|
5 |
System.out.println( map ); |
控制台的输出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。
01 |
// Calculate the weight of each tasks (as percent of total points) |
02 |
final Collection< String > result = tasks
|
03 |
.stream() // Stream< String >
|
04 |
.mapToInt( Task::getPoints ) // IntStream
|
05 |
.asLongStream() // LongStream
|
06 |
.mapToDouble( points -> points / totalPoints ) // DoubleStream
|
07 |
.boxed() // Stream< Double >
|
08 |
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
|
09 |
.mapToObj( percentage -> percentage + "%" ) // Stream< String>
|
10 |
.collect( Collectors.toList() ); // List< String >
|
12 |
System.out.println( result ); |
控制台输出如下:
[19%, 50%, 30%]
最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。
1 |
final Path path = new File( filename ).toPath();
|
2 |
try ( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
|
3 |
lines.onClose( () -> System.out.println( "Done!" ) ).forEach( System.out::println );
|
Stream的方法onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行。
Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。
4.3日期时间API(JSR310)
Java 8引入了新的日期时间API(JSR 310)改进了日期时间的管理。日期和时间管理一直是Java开发人员最痛苦的问题。java.util.Date和后来的java.util.Calendar一点也没有改变这个情况(甚至让人们更加迷茫)。
因为上面这些原因,产生了Joda-Time ,可以替换Java的日期时间API。Joda-Time深刻影响了 Java 8新的日期时间API,Java 8吸收了Joda-Time 的精华。新的java.time包包含了所有关于日期、时间、日期时间、时区、Instant(跟日期类似但精确到纳秒)、duration(持续时间)和时钟操作的类。设计这些API的时候很认真地考虑了这些类的不变性(从java.util.Calendar吸取的痛苦教训)。如果需要修改时间对象,会返回一个新的实例。
让我们看看一些关键的类和用法示例。第一个类是Clock,Clock使用时区来访问当前的instant, date和time。Clock类可以替换 System.currentTimeMillis() 和 TimeZone.getDefault().
1 |
// Get the system clock as UTC offset |
2 |
final Clock clock = Clock.systemUTC();
|
3 |
System.out.println( clock.instant() ); |
4 |
System.out.println( clock.millis() ); |
控制台输出如下:
2014-04-12T15:19:29.282Z
1397315969360
其他类我们看看LocalTime和LocalDate。LocalDate只保存有ISO-8601日期系统的日期部分,有时区信息,相应地,LocalTime只保存ISO-8601日期系统的时间部分,没有时区信息。LocalDate和LocalTime都可以从Clock对象创建。
01 |
// Get the local date and local time |
02 |
final LocalDate date = LocalDate.now();
|
03 |
final LocalDate dateFromClock = LocalDate.now( clock );
|
05 |
System.out.println( date ); |
06 |
System.out.println( dateFromClock ); |
08 |
// Get the local date and local time |
09 |
final LocalTime time = LocalTime.now();
|
10 |
final LocalTime timeFromClock = LocalTime.now( clock );
|
12 |
System.out.println( time ); |
13 |
System.out.println( timeFromClock ); |
控制台输出如下:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime类合并了LocalDate和LocalTime,它保存有ISO-8601日期系统的日期和时间,但是没有时区信息。让我们看一个简单的例子。
1 |
// Get the local date/time |
2 |
final LocalDateTime datetime = LocalDateTime.now();
|
3 |
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
|
5 |
System.out.println( datetime ); |
6 |
System.out.println( datetimeFromClock ); |
输出如下:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
如果您需要一个类持有日期时间和时区信息,可以使用ZonedDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。让我们看一些例子:
1 |
// Get the zoned date/time |
2 |
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
|
3 |
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
|
4 |
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
|
6 |
System.out.println( zonedDatetime ); |
7 |
System.out.println( zonedDatetimeFromClock ); |
8 |
System.out.println( zonedDatetimeFromZone ); |
输出如下:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
最后让我们看看Duration类,Duration持有的时间精确到纳秒。它让我们很容易计算两个日期中间的差异。让我们来看一下:
1 |
// Get duration between two dates |
2 |
final LocalDateTime from = LocalDateTime.of( 2014 , Month.APRIL, 16 , 0 , 0 , 0 );
|
3 |
final LocalDateTime to = LocalDateTime.of( 2015 , Month.APRIL, 16 , 23 , 59 , 59 );
|
5 |
final Duration duration = Duration.between( from, to );
|
6 |
System.out.println( "Duration in days: " + duration.toDays() );
|
7 |
System.out.println( "Duration in hours: " + duration.toHours() );
|
上面的例子计算了两个日期(2014年4月16日和2014年5月16日)之间的持续时间(基于天数和小时)输出如下:
Duration in days: 365
Duration in hours: 8783
对于Java 8的新日期时间的总体印象还是比较积极的。一部分是因为有经历实战的Joda-Time的基础,还有一部分是因为日期时间终于被认真对待而且听取了开发人员的声音。关于更多的详细信息,请参考官方文档。
4.4 Nashorn javascript引擎
Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。Nashorn javascript引擎只是javax.script.ScriptEngine另一个实现,而且规则也一样,允许Java和JavaScript互相操作。这里有个小例子:
1 |
ScriptEngineManager manager = new ScriptEngineManager();
|
2 |
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
|
4 |
System.out.println( engine.getClass().getName() ); |
5 |
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
|
输出如下:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
4.5 Base64
对Base64的支持最终成了Java 8标准库的一部分,非常简单易用:
01 |
package com.javacodegeeks.java8.base64;
|
03 |
import java.nio.charset.StandardCharsets;
|
04 |
import java.util.Base64;
|
06 |
public class Base64s {
|
07 |
public static void main(String[] args) {
|
08 |
final String text = "Base64 finally in Java 8!" ;
|
10 |
final String encoded = Base64
|
12 |
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) ); |
13 |
System.out.println( encoded ); |
15 |
final String decoded = new String(
|
16 |
Base64.getDecoder().decode( encoded ), |
17 |
StandardCharsets.UTF_8 ); |
18 |
System.out.println( decoded ); |
控制台输出的编码和解码的字符串
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder()).
4.6 并行数组
Java 8新增加了很多方法支持并行的数组处理。最重要的大概是parallelSort()这个方法显著地使排序在多核计算机上速度加快。下面的小例子演示了这个新的方法(parallelXXX)的行为。
02 |
<pre class = "brush:java" > package com.javacodegeeks.java8.parallel.arrays;
|
04 |
import java.util.Arrays;
|
05 |
import java.util.concurrent.ThreadLocalRandom;
|
07 |
public class ParallelArrays {
|
08 |
public static void main( String[] args ) {
|
09 |
long [] arrayOfLong = new long [ 20000 ];
|
11 |
Arrays.parallelSetAll( arrayOfLong,
|
12 |
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
|
13 |
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
|
14 |
i -> System.out.print( i + " " ) );
|
17 |
Arrays.parallelSort( arrayOfLong );
|
18 |
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
|
19 |
i -> System.out.print( i + " " ) );
|
这一小段代码使用parallelSetAll() t方法填充这个长度是2000的数组,然后使用parallelSort() 排序。这个程序输出了排序前和排序后的10个数字来验证数组真的已经被排序了。示例可能的输出如下(请注意这些数字是随机产生的)
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
4.7 并发
在新增Stream机制与lambda的基础之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法来支持聚集操作。同时也在java.util.concurrent.ForkJoinPool类中加入了一些新方法来支持共有资源池(common pool)(请查看我们关于Java 并发的免费课程)。
新增的java.util.concurrent.locks.StampedLock类提供一直基于容量的锁,这种锁有三个模型来控制读写操作(它被认为是不太有名的java.util.concurrent.locks.ReadWriteLock类的替代者)。
在java.util.concurrent.atomic包中还增加了下面这些类:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
5. 新的工具
Java 8 提供了一些新的命令行工具,在这节里我们将会介绍它们中最有趣的部分。
5.1 Nashorn引擎:jjs
jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。例如,我们创建一个具有如下内容的func.js文件:
我们可以把这个文件作为参数传递给jjs使得这个文件可以在命令行中执行
1 |
<span style= "font-size: 13px;" >jjs func.js</span>
|
输出结果如下
2
更多的详细信息请参考官方文档。
5.2 类依赖分析工具:jdeps
Jdeps是一个功能强大的命令行工具,它可以帮我们显示出包层级或者类层级java类文件的依赖关系。它接受class文件、目录、jar文件作为输入,默认情况下,jdeps会输出到控制台。
作为例子,让我们看看现在很流行的Spring框架的库的依赖关系报告。为了让报告短一些,我们只分析一个jar:org.springframework.core-3.0.5.RELEASE.jar.
jdeps org.springframework.core-3.0.5.RELEASE.jar 这个命令输出内容很多,我们只看其中的一部分,这些依赖关系根绝包来分组,如果依赖关系在classpath里找不到,就会显示not found.
02 |
<pre class = "brush:java" >org.springframework.core- 3.0 . 5 .RELEASE.jar -> C:\Program Files\Java\jdk1. 8.0 \jre\lib\rt.jar
|
03 |
org.springframework.core (org.springframework.core- 3.0 . 5 .RELEASE.jar)
|
06 |
-> java.lang.annotation
|
10 |
-> java.util.concurrent
|
11 |
-> org.apache.commons.logging not found
|
12 |
-> org.springframework.asm not found
|
13 |
-> org.springframework.asm.commons not found
|
14 |
org.springframework.core.annotation (org.springframework.core- 3.0 . 5 .RELEASE.jar)
|
16 |
-> java.lang.annotation
|
更多的详细信息请参考官方文档。
6. JVM的新特性
JVM内存永久区已经被metaspace替换(JEP 122)。JVM参数 -XX:PermSize 和 –XX:MaxPermSize被XX:MetaSpaceSize 和 -XX:MaxMetaspaceSize代替。
7. 结论
更多展望:Java 8通过发布一些可以增加程序员生产力的特性来推进这个伟大的平台的进步。现在把生产环境迁移到Java 8还为时尚早,但是在接下来的几个月里,它会被大众慢慢的接受。毫无疑问,现在是时候让你的代码与Java 8兼容,并且在Java 8足够安全稳定的时候迁移到Java 8。
作为社区对Java 8的认可,最近Pivotal发布了可在生产环境下支持Java 8的Spring Framework 4.0.3。
如果你喜欢这篇文章,请订阅我们的邮件列表来查看每周的更新以及免费赠送的白皮书。对于更高级的教程,请查看我们的[JCG学院][JCG]。
我们欢迎你对Java 8中激动人心的特性进行评论!
8. 资源
下面一些文章从不同层面上深度讨论了Java 8的特性:
时间: 2024-10-31 14:56:29