Java 8 的特性介绍 ( 2 )

翻译于 http://winterbe.com/posts/2014/03/16/java-8-tutorial/

(接上篇)


流(Stream)

java.util.Stream表示可以执行一个或多个操作的元素序列。 流操作是中间或终端。 当终端操作返回某种类型的结果时,中间操作返回流本身,以便可以连续地连接多个方法调用。 流是在源上创建的,例如 一个java.util.Collection如列表或集(不支持映射)。 流操作可以顺序还是并行执行。

我们先来看看顺序流如何工作。 首先,我们以字符串列表的形式创建一个示例源:

1
2
3
4
5
6
7
8
9
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

扩展Java 8中的集合,因此您可以通过调用Collection.stream()或Collection.parallelStream()简单地创建流。 以下部分介绍最常见的流操作。

过滤(Filter)

Filter接受一个谓词来过滤流的所有元素。 这个操作是中间的,这使我们能够调用另一个流操作(forEach)来获得结果。 ForEach接受对过滤流中的每个元素执行的消费者。 ForEach是一个终端操作。 它是void,所以我们不能调用另一个流操作。

1
2
3
4
5
6
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

// "aaa2", "aaa1"

排序(Sorted)

Sorted是一个中间操作,返回流的排序视图。 元素按照自然顺序进行排序,除非您传递自定义的比较器。

1
2
3
4
5
6
7
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

// "aaa1", "aaa2"

要注意,排序的仅创建流的排序视图,是不会改变后端集合的排序。 stringCollection的顺序是不变的:

1
2
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

映射(Map)

中间操作图通过给定的功能将每个元素转换成另一个对象。 以下示例将每个字符串转换为上一个字符串。 但是您也可以使用map将每个对象转换成另一种类型。 结果流的通用类型取决于传递给映射的函数的通用类型。

1
2
3
4
5
6
7
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

匹配(Match)

可以使用各种匹配操作来检查某个谓词是否与流匹配。 所有这些操作都是终止的,并返回一个布尔结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA); // true

boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA); // false

boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ); // true

计数(Count)

1
2
3
4
5
6
7
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();

System.out.println(startsWithB); // 3

还原(Reduce)

1
2
3
4
5
6
7
8
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

并行流(Parallel Streams)

如上所述,流可以是顺序还是并行。 在单个线程上执行顺序流的操作,同时在多个线程上并行执行对并行流的操作。

以下示例说明通过使用并行流来增加性能是多么容易。
首先我们创建一个很大的独特元素列表:

1
2
3
4
5
6
int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}

现在我们测量排序此集合的流所需的时间。

顺序排序

1
2
3
4
5
6
7
8
9
10
11
long t0 = System.nanoTime();

long count = values.stream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));

// sequential sort took: 899 ms

并行排序

1
2
3
4
5
6
7
8
9
10
11
long t0 = System.nanoTime();

long count = values.parallelStream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));

// parallel sort took: 472 ms

您可以看到两个代码段几乎相同,但是并行排序速度大约提高了50%。所有你需要做的是将stream()更改为parallelStream()。

映射(Map)

1
2
3
4
5
6
7
Map<Integer, String> map = new HashMap<>();

for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}

map.forEach((id, val) -> System.out.println(val));
1
2
3
4
5
6
7
8
9
10
11
map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33

map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false

map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true

map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33
1
2
3
4
5
map.remove(3, "val3");
map.get(3); // val33

map.remove(3, "val33");
map.get(3); // null
1
map.getOrDefault(42, "not found");  // not found
1
2
3
4
5
map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9

map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat

日期API(Date API)

Java 8包含java.time包下的全新的日期和时间API。 新的Date API与Joda-Time库是差不多的,但不完全一样。 以下示例涵盖了此新API最重要的部分。

时钟(Clock)

Clock 可以访问当前的日期和时间。Clock 是与时区相关的,也可以用于替代System.currentTimeMillis() 来检索当前的毫秒数。 时间线上的这样一个瞬时点也由类Instant表示。 可以使用Instants来创建旧的java.util.Date对象。

1
2
3
4
5
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date

时区(Timezones)

时区由ZoneId表示。它们可以通过静态工厂方法轻松访问。时区定义了在时刻和本地日期和时间之间转换很重要的偏移量。

1
2
3
4
5
6
7
8
9
10
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)

LocalTime表示没有时区的时间,例如 晚上10点或17点30分15分。 以下示例为上面定义的时区创建两个本地时间。 然后我们比较两次,并计算两次之间的小时和分钟的差异。

1
2
3
4
5
6
7
8
9
10
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具有各种工厂方法,以简化新实例的创建,包括解析时间字符串。

1
2
3
4
5
6
7
8
9
10
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)

1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
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)

1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
1
2
3
4
5
6
7
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是不可变且线程安全的。

注解(Annotations)

Java 8中的注释是可重复的。 我们直接进入一个例子来弄清楚。

首先,我们定义一个包含实际注释数组的包装器注释:

1
2
3
4
5
6
7
8
@interface Hints {
Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
String value();
}

Java 8使我们能够通过声明@Repeatable注解来使用相同类型的多个注解.

变式1:使用容器注解

1
2
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

变式2:使用可重复的注解

1
2
3
@Hint("hint1")
@Hint("hint2")
class Person {}

使用变式2,java编译器会隐式设置@Hints,对于像反反射这样的注解是非常重要的。

1
2
3
4
5
6
7
8
Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint); // null

Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length); // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length); // 2

虽然我们从未在Person类上声明@Hints注解,但它仍然可以通过getAnnotation(Hints.class)来读取。 然而getAnnotationsByType更方便的方法是直接访问所有@Hint注解。

此外,Java 8中的注解的使用扩展到两个新的目标:

1
2
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}


(翻译完)

自己的看法

流的部分还是比较实用的
map部分,感觉变化不大
时间api方面,用得不多,要使用的再看
注解部分就是简化了一点