最新添加的 Stream API(java.util.stream)
把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为 Stream API
可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
Stream API极大简化了集合框架的处理。让我们以一个简单的 Task
类为例进行介绍:
public class Streams { private enum Status { OPEN, CLOSED }; private static final class Task { private final Status status; private final Integer points; Task(final Status status, final Integer points) { this.status = status; this.points = points; } public Integer getPoints() { return points; } public Status getStatus() { return status; } @Override public String toString() { return String.format("[%s, %d]", status, points); } } }
Task
类中有一个分数的概念,其次是还有一个值可以为 OPEN
或 CLOSED
的状态.让我们引入一个 Task
的小集合作为演示例子:
final Collection<Task> tasks = Arrays.asList( new Task(Status.OPEN, 5), new Task(Status.OPEN, 13), new Task(Status.CLOSED, 8) );
我们下面要讨论的第一个问题是所有状态为 OPEN
的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用 stream
:一串支持连续、并行聚集操作的元素。
// Calculate total points of all active tasks using sum() final long totalPointsOfOpenTasks = tasks .stream() .filter(task -> task.getStatus() == Status.OPEN) .mapToInt(Task::getPoints) .sum(); System.out.println("Total points: " + totalPointsOfOpenTasks);
程序在控制台上的输出如下:
Total points: 18
这里有几个注意事项。第一,task集合被转换化为其相应的 stream
表示。然后, filter
操作过滤掉状态为 CLOSED
的task。下一步, mapToInt
操作通过 Task::getPoints
这种方式调用每个task实例的 getPoints
方法把Task的stream转化为 Integer
的 stream
。最后,用 sum
函数把所有的分数加起来,得到最终的结果。
.stream
操作被分成了中间操作与最终操作这两种。
中间操作返回一个新的 stream
对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始 stream
中符合给定谓词的所有元素。
像 forEach
、 sum
这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。
stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。
// Calculate total points of all tasks final double totalPoints = tasks .stream() .parallel() .map(task -> task.getPoints()) // or map(Task::getPoints) .reduce(0, Integer::sum); System.out.println("Total points (all tasks): " + totalPoints);
这个例子和第一个例子很相似,但这个例子的不同之处在于这个程序是并行运行的,其次使用 reduce
方法来算最终的结果。
下面是这个例子在控制台的输出:
Total points (all tasks): 26.0
经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。 Stream
也可以处理这样的需求,下面是一个例子:
// Group tasks by their status final Map<Status, List<Task>> map = tasks .stream() .collect(Collectors.groupingBy(Task::getStatus)); System.out.println(map);
这个例子的控制台输出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。
// Calculate the weight of each tasks (as percent of total points) final Collection<String> result = tasks .stream() // Stream<String> .mapToInt(Task::getPoints) // IntStream .asLongStream() // LongStream .mapToDouble(points -> points / totalPoints) // DoubleStream .boxed() // Stream<Double> .mapToLong(weigth -> (long) (weigth * 100)) // LongStream .mapToObj(percentage -> percentage + "%") // Stream<String> .collect(Collectors.toList()); // List<String> System.out.println(result);
下面是这个例子的控制台输出:
[19%, 50%, 30%]
最后,就像前面提到的, Stream API
不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。
final Path path = new File(filename).toPath(); try(Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) { lines.onClose(() -> System.out.println("Done!")).forEach(System.out::println); }
对一个 stream
对象调用 onClose
方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用 close()
方法时,与关闭相关的处理器就会执行。
到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的 Guava
项目引入了 Optional
类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发, Optional
类已经成为Java 8类库的一部分。
Optional
实际上是个容器:它可以保存类型T的值,或者仅仅保存null。 Optional
提供很多有用的方法,这样我们就不用显式进行空值检测。
我们下面用两个小例子来演示如何使用Optional类:一个允许为空值,一个不允许为空值。
Optional<String> fullName = Optional.ofNullable(null); System.out.println("Full Name is set? " + fullName.isPresent()); System.out.println("Full Name: " + fullName.orElseGet(() -> "[none]")); System.out.println(fullName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));
如果 Optional
类的实例为非空值的话, isPresent()
返回 true
,否从返回 false
。为了防止Optional为空值, orElseGet()
方法通过回调函数来产生一个默认值。 map()
函数对当前 Optional
的值进行转化,然后返回一个新的 Optional
实例。 orElse()
方法和 orElseGet()
方法类似,但是 orElse
接受一个默认值而不是一个回调函数。下面是这个程序的输出:
Full Name is set? false Full Name: [none] Hey Stranger!
让我们来看看另一个例子:
Optional<String> firstName = Optional.of("Tom"); System.out.println("First Name is set? " + firstName.isPresent()); System.out.println("First Name: " + firstName.orElseGet(() -> "[none]")); System.out.println(firstName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!")); System.out.println();
下面是程序的输出:
First Name is set? true First Name: Tom Hey Tom!
Java 8 在包 java.time
下包含了一组全新的时间日期API。新的日期API和开源的 Joda-Time
库差不多,但又不完全一样,下面的例子展示了这组新API里最重要的一些部分:
Clock
类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis()
来获取当前的微秒数。某一个特定的时间点也可以使用 Instant
类来表示, Instant
类也可以用来创建老的 java.util.Date
对象。代码如下:
Clock clock = Clock.systemDefaultZone(); long millis = clock.millis(); Instant instant = clock.instant(); Date legacyDate = Date.from(instant); // legacy java.util.Date
在新API中时区使用 ZoneId
来表示。时区可以很方便的使用静态方法 of
来获取到。时区定义了到UTS时间的时间差,在 Instant
时间点对象到本地日期对象之间转换的时候是极其重要的。代码如下:
System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids ZoneId zone1 = ZoneId.of("Europe/Berlin"); ZoneId zone2 = ZoneId.of("Brazil/East"); System.out.println(zone1.getRules()); System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=+01:00] // ZoneRules[currentStandardOffset=-03:00]
LocalTime
定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差。代码如下:
LocalTime now1 = LocalTime.now(zone1); LocalTime now2 = LocalTime.now(zone2); System.out.println(now1.isBefore(now2)); // false long hoursBetween = ChronoUnit.HOURS.between(now1, now2); long minutesBetween = ChronoUnit.MINUTES.between(now1, now2); System.out.println(hoursBetween); // -3 System.out.println(minutesBetween); // -239
LocalTime
提供了多种工厂方法来简化对象的创建,包括解析时间字符串。代码如下:
LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 23:59:59 DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN); LocalTime leetTime = LocalTime.parse("13:37", germanFormatter); System.out.println(leetTime); // 13:37
LocalDate
表示了一个确切的日期,比如 2014-03-11
。该对象值是不可变的,用起来和 LocalTime
基本一致。下面的例子展示了如何给 Date
对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。代码如下:
LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2); LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek(); System.out.println(dayOfWeek); // FRIDAY
从字符串解析一个LocalDate类型和解析LocalTime一样简单。代码如下:
DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedDate(FormatStyle.MEDIUM) .withLocale(Locale.GERMAN); LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter); System.out.println(xmas); // 2014-12-24
LocalDateTime
同时表示了时间和日期,相当于前两节内容合并到一个对象上了。 LocalDateTime
和 LocalTime
还有 LocalDate
一样,都是不可变的。 LocalDateTime
提供了一些能访问具体字段的方法。代码如下:
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59); DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); System.out.println(dayOfWeek); // WEDNESDAY Month month = sylvester.getMonth(); System.out.println(month); // DECEMBER long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); System.out.println(minuteOfDay); // 1439
只要附加上时区信息,就可以将其转换为一个时间点 Instant
对象, Instant
时间点对象可以很容易的转换为老式的 java.util.Date
。代码如下:
Instant instant = sylvester .atZone(ZoneId.systemDefault()) .toInstant(); Date legacyDate = Date.from(instant); System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
格式化 LocalDateTime
和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式。代码如下:
DateTimeFormatter formatter = DateTimeFormatter .ofPattern("MMM dd, yyyy - HH:mm"); LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter); String string = formatter.format(parsed); System.out.println(string); // Nov 03, 2014 - 07:13
和 java.text.NumberFormat
不一样的是新版的 DateTimeFormatter
是不可变的,所以它是线程安全的。
关于Java8中日期API更多的使用示例可以参考 Java 8中关于日期和时间API的20个使用示例 。
在Java 8中,Base64编码已经成为Java类库的标准。它的使用十分简单,下面让我们看一个例子:
import java.nio.charset.StandardCharsets; import java.util.Base64; public class Base64s { public static void main(String[] args) { final String text = "Base64 finally in Java 8!"; final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); System.out.println(encoded); final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8); System.out.println(decoded); } }
程序在控制台上输出了编码后的字符与解码后的字符:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ== Base64 finally in Java 8!
Base64类同时还提供了对URL、MIME友好的编码器与解码器( Base64.getUrlEncoder() / Base64.getUrlDecoder()
, Base64.getMimeEncoder() / Base64.getMimeDecoder()
)。
JavaFX
是一个强大的图形和多媒体处理工具包集合,它允许开发者来设计、创建、测试、调试和部署富客户端程序,并且和Java一样跨平台。从Java8开始,JavaFx已经内置到了JDK中。关于JavaFx更详细的文档可参考 JavaFX中文文档 。
JDBC4.2主要有以下几点改动:
REF Cursor
的支持 java.sql.DriverAction
接口 java.sql.SQLType
接口 java.sql.JDBCtype
枚举 java.time
包时间类型的支持 Java 8在类型推测方面有了很大的提高。在很多情况下,编译器可以推测出确定的参数类型,这样就能使代码更整洁。让我们看一个例子:
public class Value<T> { public static<T> T defaultValue() { return null; } public T getOrDefault(T value, T defaultValue) { return (value != null) ? value : defaultValue; } }
这里是 Value<String>
类型的用法。
public class TypeInference { public static void main(String[] args) { final Value<String> value = new Value<>(); value.getOrDefault("22", Value.defaultValue()); } }
Value.defaultValue()
的参数类型可以被推测出,所以就不必明确给出。在Java 7中,相同的例子将不会通过编译,正确的书写方式是 Value.<String>defaultValue()
。
Java8中,HashMap内部实现又引入了红黑树,使得HashMap的总体性能相较于Java7有比较明显的提升。以下是对Hash均匀和不均匀的情况下的性能对比
Java8 对 IO/NIO
也做了一些改进。主要包括:改进了 java.nio.charset.Charset
的实现,使编码和解码的效率得以提升,也精简了 jre/lib/charsets.jar
包;优化了 String(byte[], *)
构造方法和 String.getBytes()
方法的性能;还增加了一些新的 IO/NIO
方法,使用这些方法可以从文件或者输入流中获取流( java.util.stream.Stream
),通过对流的操作,可以简化文本行处理、目录遍历和文件查找。
新增的 API 如下:
BufferedReader.line()
: 返回文本行的流 Stream<String>
File.lines(Path, Charset)
: 返回文本行的流 Stream<String>
File.list(Path)
: 遍历当前目录下的文件和目录 File.walk(Path, int, FileVisitOption)
: 遍历某一个目录下的所有文件和指定深度的子目录 File.find(Path, int, BiPredicate, FileVisitOption...)
: 查找相应的文件 下面就是用流式操作列出当前目录下的所有文件和目录:
Files.list(new File(".").toPath()).forEach(System.out::println);
Java 8提供了一个新的 Nashorn javascript
引擎,它允许我们在JVM上运行特定的javascript应用。Nashorn javascript引擎只是 javax.script.ScriptEngine
另一个实现,而且规则也一样,允许Java和JavaScript互相操作。这里有个小例子:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); System.out.println(engine.getClass().getName()); System.out.println("Result:" + engine.eval("function f(){return 1;}; f() + 1;"));
输出如下:
jdk.nashorn.api.scripting.NashornScriptEngine Result: 2
在新增 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
包中还增加了下面这些类:
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.
C:/Program Files/Java/jdk1.8.0/jre/lib/rt.jar org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar) -> java.io -> java.lang -> java.lang.annotation -> java.lang.ref -> java.lang.reflect -> java.util -> java.util.concurrent -> org.apache.commons.logging not found -> org.springframework.asm not found -> org.springframework.asm.commons not found org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar) -> java.lang -> java.lang.annotation -> java.lang.reflect -> java.util
PermGen
空间被移除了,取而代之的是 Metaspace(JEP 122)
。JVM选项 -XX:PermSize
与 -XX:MaxPermSize
分别被 -XX:MetaSpaceSize
与 -XX:MaxMetaspaceSize
所代替。
参考文档: