Java实战之Java8指南

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

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

Default Methods for Interfaces

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

interface Formula {
    double calculate(int a);

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

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

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

能够发现用如上的方法写会更短而且更加的可读,而且能够更短:intellij-idea

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

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

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

Functional Interfaces

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

//声明
@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

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

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