1

本文为翻译文章,原文地址 这里

欢迎来到本人对于Java 8的系列介绍教程,本教程会引导你一步步领略最新的语法特性。通过一些简单的代码示例你即可以学到默认的接口方法、Lambda表达式、方法引用以及重复注解等等。本文的最后还提供了譬如Stream API之类的详细的介绍。

Default Methods for Interfaces

Java 8 允许我们利用default关键字来向接口中添加非抽象的方法作为默认方法。下面是一个小例子:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

在接口Formula中,除了抽象方法calculate之外,还定义了一个默认的方法sqrt。实现类只需要实现抽象方法calculate,而sqrt方法可以跟普通方法一样调用。

Formula formula = new Formula() {
    @Override
    public double calculate(int a) {
        return sqrt(a * 100);
    }
};

formula.calculate(100);     // 100.0
formula.sqrt(16);           // 4.0

Lambda表达式

首先以简单的字符串排序为例来展示:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

静态的工具类方法Collections.sort接受一个列表参数和一个比较器对象来对于指定列表中的元素进行排序。我们常常需要创建匿名比较器并且将他们传递给排序方法。而Java 8中提供的一种更短的Lambda表达式的方法来完成该工作:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

可以发现用如上的方法写会更短并且更加的可读,并且可以更短:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

这种写法就完全不需要{}以及return关键字,再进一步简化的话,就变成了:

names.sort((a, b) -> b.compareTo(a));

Functional Interfaces

Lambda表达式是如何适配进Java现存的类型系统的呢?每个Lambda表达式都会关联到一个由接口确定的给定的类型。这种所谓的函数式接口必须只能包含一个抽象方法,而每个该类型的Lambda表达式都会关联到这个抽象方法。不过由于默认方法不是抽象的,因此可以随便添加几个默认的方法到函数式接口中。我们可以将任意的接口作为Lambda表达式使用,只要该接口仅含有一个抽象方法即可。为了保证你的接口满足这个需求,应该添加@FunctionalInterface这个注解。编译器会在你打算向某个函数式接口中添加第二个抽象方法时候报错。

//声明
@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}

//使用
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

Method and Constructor References

上述的代码可以使用静态方法引用而更加的简化:

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

Java 8允许将方法或则构造器的引用通过::关键字进行传递,上述的例子是演示了如何关联一个静态方法,不过我们也可以关联一个对象方法:

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

接下来看 :: 关键字怎么在构造器中起作用。首先我们定义一个有两个不同控制器的Java Bean:

class Person {
    String firstName;
    String lastName;

    Person() {}

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

然后,我们创建一个特定的Person工厂接口来创建新的Person对象:

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

不需要手动的去继承实现该工厂接口,我们只需要将控制器的引用传递给该接口对象就好了:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

Java的控制器会自动选择合适的构造器方法。

Lambda Scopes

从Lambda表达式中访问外部作用域中变量非常类似于匿名对象,可以访问本地的final变量、实例域以及静态变量。

Accessing local variables

在匿名对象中,我们可以从Lambda表达式的域中访问外部的final变量。

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

但是不同于匿名对象只能访问final变量,Lambda表达式中可以访问final变量:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

不过需要注意的是,尽管变量不需要声明为final,但是也是隐式的不可变:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

譬如如上的写法就会被报错。

Accessing fields and static variables

不同于本地变量,我们可以在Lambda表达式中任意的读写:

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

Accessing Default Interface Methods

注意,Lambda表达式中是不可以访问默认方法的:

Formula formula = (a) -> sqrt( a * 100);

上述代码是编译通不过的。

Built-in Functional Interfaces

JDK 1.8 的API中包含了许多的内建的函数式接口,其中部分的譬如Comparator、Runnable被改写成了可以由Lambda表达式支持的方式。除此之外,Java 8还添加了许多来自于Guava中的依赖库,并将其改造为了Lambda接口。

Predicates

Predicates是包含一个参数的返回为布尔值的接口,接口包含了许多的默认方法来进行不同的复杂的逻辑组合:

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo");              // true
predicate.negate().test("foo");     // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Functions

Functions接口接受一个参数并且产生一个结果,同样提供了部分默认的方法来链式组合不同的函数(compose,andThen)。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"

Suppliers

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Consumers

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

Comparators类似于旧版本中的用法,Java 8是添加了一些默认的用法。

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

Optionals

Optionals并不是一个函数式接口,但是非常有用的工具类来防止NullPointerException。Optional是一个简单的容器用于存放那些可能为空的值。对于那种可能返回为null的方法可以考虑返回Optional而不是null:

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Streams

一个Java的Stream对象代表了一系列可以被附加多个操作的元素的序列集合。常用的Stream API分为媒介操作与终止操作。其中终止操作会返回某个特定类型的值,而媒介操作则会返回流本身以方便下一步的链式操作。Streams可以从java.util.Collection的数据类型譬如lists或者sets(不支持maps)中创建。而Streams的操作可以顺序执行也可以并发地执行。

Stream.js是一个利用JavaScript实现的Java 8的流接口。

首先我们创建待处理的数据:

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");

接下来可以利用Collection.stream() or Collection.parallelStream()来转化为一个流对象。

Filter

Filter会接受一个Predicate对象来过滤流中的元素,这个操作属于媒介操作,譬如可以在该操作之后调用另一个流操作(forEach)。ForEach操作属于终止操作,接受一个Consumer对象来对于过滤之后的流中的每一个元素进行操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

Sorted

Sorted操作属于一个媒介操作,会将流对象作为返回值返回。元素会默认按照自然的顺序返回,除非你传入了一个自定义的Comparator对象。

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa1", "aaa2"

需要记住的是,Sorted操作并不会改变流中的元素的顺序,只会创建一个经过排序的视图,譬如:

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

Map

map操作也是媒介操作的一种,可以通过给定的函数将每个元素映射到其他对象。下面的代码示例就是将所有的字符串转化为大写字符串。不过map操作是可以将任意对象转化为任意类型,流返回的泛型类型取决于传递给map的函数的返回值。

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

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

Match

Java 8提供了一些列的匹配的终止操作符来帮助开发者判断流当中的元素是否符合某些判断规则。所有的匹配类型的操作都会返回布尔类型。

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

Count 也是终止操作的一种,它会返回流中的元素的数目。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

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

Reduce

该操作根据指定的方程对于流中的元素进行了指定的减少的操作。结果是Optional类型。

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

正如上文中提及的,流可以是顺序的也可以是并行的。所有在顺序流上执行的流操作都是在单线程中运行的,而在并行流中进行的操作都是在多线程中运行的。如下的代码演示了如何利用并行流来提供性能,首先我们创建一个待比较的序列:

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

Sequential Sort

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

Parallel Sort

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

Maps

正如上文所说,Map并不支持流操作,但是也提供了很多有用的方法来进行通用的操作。

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));

上述的代码中,putIfAbsent避免了写太多额外的空检查。forEach会接受一个Consumer参数来遍历Map中的元素。下面的代码演示如何进行计算:

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

还有,Map提供了如何根据给定的key,vaue来删除Map中给定的元素:

map.remove(3, "val3");
map.get(3);             // val33

map.remove(3, "val33");
map.get(3);             // null

还有一个比较有用的方法:

map.getOrDefault(42, "not found");  // not found

同时,Map还提供了merge方法来帮助有效地对于值进行修正:

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

Date API

Java 8包含了一个全新的日期与时间的API,在java.time包下,新的时间API集成了Joda-Time的库。

Clock

Clock方便我们去读取当前的日期与时间。Clocks可以根据不同的时区来进行创建,并且可以作为System.currentTimeMillis()的替代。这种指向时间轴的对象即是Instant类。Instants可以被用于创建java.util.Date对象。

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

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

Timezones

Timezones以ZoneId来区分。可以通过静态构造方法很容易的创建,Timezones定义了Instants与Local Dates之间的转化关系:

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代表了一个与时间无关的本地时间,譬如 10pm 或者 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

LocalDate代表了一个独立的时间类型,譬如2014-03-11。它是一个不可变的对象并且很类似于LocalTime。下列代码展示了如何通过增减时间年月来计算日期:

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代表了时间日期类型,它组合了上文提到的Date类型以及Time类型。LocalDateTime同样也是一种不可变类型,很类似于LocalTime以及LocalDate。

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 instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

从格式化字符串中解析获取到数据对象,也是非常简单:

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

Annotations

Java 8中的注解现在是可以重复的,下面我们用例子直接说明。首先,创建一个容器注解可以用来存储一系列真实的注解:

@interface Hints {
    Hint[] value();
}

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

通过添加 @Repeatable注解,就可以在同一个类型中使用多个注解。

Variant 1: Using the container annotation (old school)

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

Variant 2: Using repeatable annotations (new school)

@Hint("hint1")
@Hint("hint2")
class Person {}
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

Further Reading


王下邀月熊_Chevalier
22.4k 声望8.5k 粉丝

爱代码 爱生活 希望成为全栈整合师