interface 允许默认方法
Java 8 允许接口中包含非 abstract 关键字修饰的方法,但是需要在有实现体的方法的声明前加上 default 关键字,这种特性也被称为 Extension Methods。下面是这种特性的第一个代码示例。
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
接口 Formula 除了声明了一个 抽象方法 calculate 外,也定义了一个 defualt 方法 sqrt。实现 Formula 接口的类只需要实现 abstract 方法 calculate 即可。defalut 方法可以直接被实现类的对象使用。
Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
}
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0
上面代码中的 formula 对象是一个匿名类的实例,代码实现起来相当繁琐:一个简单的计算函数 sqrt(a * 100) 却需要 6 行代码。接下来的小结中,我们接下来将看到在 Java 8 中有更好的方式来生成这种只有一个方法的对象。
Lambda 表达式
先通过一个简单例子来看一下在 Java 8 以前是如何来实现对字符串列表进行排序的。
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 有两个参数,分别为待排序的 集合 和用来指定排序规则的 排序器(comparator)。在使用排序器的时候,一般是以匿名类的对象的形式传递给排序方法。在 Java 8 中除了这种匿名类的方式外,还支持一种称为 lambda 表达式 的简短语法。
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
})
可以看到,上面的代码比之前的代码短得多,也更加容易阅读,其实这段代码还可以变得更短。
Collections.sort(names, (a, b) -> b.compareTo(a));
Java 编译器能知道 lambda 表达式中各个参数的类型,所以也可以省略对参数类型的声明。接下来让我们继续深入 lambda 表达式是如何工作的。
Functional Interfaces
Java 是如何处理 lambda 表达式的呢?每个 lambda 表达式对应着一个被称之为函数接口(functional interface)的接口。函数接口指只包含一个抽象方法的接口。lambda 表达式将匹配到函数接口的抽象方法。因为 default 方法不是抽象方法,所以函数接口可以包含任意多个 default 方法。
只要接口只有一个抽象方法,我们就可以对其使用 lambda 表达式。为了保证接口在编写过程中只满足一个抽象方法的要求,可以在接口上面加上 @FunctionalInterface 注解,如果一个加上了该注解的接口中出现了第二个抽象方法,Java 编译器将会抛出编译异常。下面是 @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
注意:上面的代码在去掉***@FunctionalInterface*** 注解依然是有效的。
方法和构造器引用
上面的代码示例可以通过使用静态引用对其做进一步的简化。
Coverter<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"
我们接着看如何通过 :: 关键字来引用构造器。
首先我们先定义一个有不同构造器的 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");
我们通过 Person::new 来引用 Person 类的构造器。Java 编译器将自动选择合适的构造器来匹配 PersonFacoty.create 方法。
Lambda 域
通过 lambda 表达式访问外部域变量与匿名类相似:可以访问 final 修饰的局部变量、实例变量以及静态变量。
访问局部变量
我们可以在 lambda 表达式中读取 final 修饰的局部变量。
final int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
但与匿名对象不同的是,在 lambda 表达式中,局部变量 num 不一定需要被 final 修饰,即下面的代码也是正确的。
int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.converet(2); // 3
但在编译的时候,必须隐式地保证局部变量 num 是 final 的,即下面的代码将不会通过编译。
int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
num = 3;
也不允许在 lambda 表达式中修改 num 变量。
访问成员以及静态变量
与局部变量相比,在 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);
}
}
}
访问 default 接口方法
还记得第一个小结中的 formula 例子吗?接口 Formula 定义了一个 default 方法 sqrt,每个 Formula 实例包括该接口的匿名类实例都可以访问该方法,但这在 lambda 表达式是不被允许的。
在 lambda 表达式中,不能访问 default 方法,即下面的代码将不会通过编译。
Formula formula = (a) -> sqrt(a * 100);
内置的方法接口
JDK 1.8 API 包含了许多内置的方法接口。一些方法接口在 JDK 1.8 之前的版本就已经被大家广泛使用了,比如 Comparator 或 Runable。这些已经存在的接口加上了 @FunctionalINterface 注解,也支持 lambda 表达式。
Java 8 API 也提供了许多新的方法接口来使编程更加简单。一些方法接口在 Google Guava 库中已经很有名了。即使你熟悉这个库,你也应该仔细看一下 JDK 8.0 是如何添加一些额外有用的方法来扩展这些接口的。
Predicate
Predicate 接口的抽象方法的只接收一个参数,且返回值为 boolean 类型,其抽象函数签名为:boolean apply(T t)。该接口包含了许多 default 方法来处理一些复杂的逻辑运算(与、或、非)。
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();
Function
Function 接口的抽象方法有一个参数和一个返回值,其抽象函数签名为 R apply(T t),其 default 方法可以用来将多个函数接口链接在一起(compose、andThen)
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Supplier
Supplier 接口的抽象方法只有一个返回值,不接收任何参数,其抽象函数签名为:T get()。
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
Consumer
Consumer 接口只接收一个参数,没有返回值,其抽象函数签名为:void accept(T t),其 default 方法可以用来将多个函数接口链接在一起(andThen)。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker")).andThen(() -> System.out.println("after the greet."));
// Hello, Luke Skywalker
// after the greet.
Comparator
Comparator 在老版本的 Java 中就已经为人所知了。Java 8 给该方法接口添加了许多 default 方法。
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
compator.reversed().compare(p1, p2); // < 0
Operator
Operator 接口代表参数和返回结果类型一致,JDK 提供了两种标准函数接口:UnaryOperator,其抽象函数签名为:T apply(T t),BinaryOperator,其抽象函数签名为:T apply(T t1, T t2)。
Optional
Optional 不是函数接口,但是它是一种极好的工具类来防止程序抛出 NullPointerException。在接下来的各个小结中是一个重要的概念,所以让我们快速过一下 Optional 是如何工作的。
Optional 是一个为 null 或 非 null 值的容器,可以认为是一个可能会返回非 null 值或什么都不返回的函数。在 Java 8 中,不应该返回 null,而应该返回 Optional 对象。
Optiona<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"
Stream
java.util.Stream 代表了一系列可以执行一个或多个操作的元素。流操作可以分为 intermediate 和 terminate 两类。terminal 操作将返回确定类型的结果;intermediate 操作将返回 流对象 本身,所以你可以将许多操作链接到一行代码中。流对象 是基于数据源创建的,比如像 List、Set(Map 是不支持转换成流对象的)这类 java.util.Collection 对象。流操作可以按序执行,也可以并发执行。
先看一下按序执行的流操作。
我们首先创建一个字符串列表作为数据源。
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() 方法或 parallelStream() 来创建 流对象。下面的章节将对绝大多数流操作进行说明。
Filter
filter 方法接收一个 Predicate 函数接口来对流对象中的所有元素执行过滤操作,该操作属于 intermediate 类型,允许我们继续调用其它流操作,比如 forEach 方法,是一种 terminal 操作,该操作接收一个 Consumer 函数接口,过滤后得到的流对象中的每个元素都将执行该函数接口的方法。
stringCollection
.stream()
.filter(s -> s.startsWith("a"))
.forEach(System.out::println); // "aaa2", "aaa1"
Sorted
sorted 方法是一个 intermediate 操作,将返回一个流对象的有序视图,在没有给定自定义的 Comparator 的情况下,该视图中的元素都是以自然序进行排序的。
stringColletion
.stream()
.filter(s -> s.startsWith("a"))
.sorted()
.forEach(System.out::println);// "aaa1", "aaa2"
需要注意的是,sorted 方法仅创建了一个流对象的有序视图,并没有对其依赖的集合对象进行排序操作,即 stringCollection 里面元素的顺序并没有发生变化。
System.out.println(stringCollection); // ddd2, aaa2, bbb1, bbb3, ccc, bbb2, ddd1
Map
map 方法是一个 intermediate 操作,可以将流对象中的每个元素转换成其它对象。下面的示例将每个字符串转换为大写的字符串,当然你也可以将它转换为其它类型,转换后的流对象中的元素的泛型类型取决于你传递给 map 函数的泛型类型。
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println); // DDD2, DDD1, CCC, BBB3, BBB2, AAA2, AAA1
Match
可以使用多种 match 操作来检查流对象是否匹配给定 Predicate 函数接口,这些 match 操作都是 terminal 类型的。
boolean anyStartsWithA = stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a")); // true
boolean allStartsWithA = stringCollection
.stream()
.allMatch((s) -> s.startsWith("a")); // false
boolean noneStartsWithZ = stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z")); // true
Count
count 方法是一种 terminal 操作,它返回一个 long 型值来表示流对象中的元素个数。
long startsWithB = stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count(); // 3
Reduce
reduce 方法是一种 terminal 操作,它将对流对象中的元素执行按照给定的函数接口执行约简操作,返回值是一个带有约简结构的 Optional 对象。
Optional<String> reduced = stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2); // aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2
parallelStream
正如前文所提到的那样,流操作既可以是按序执行的,也可以是并发执行的。有序流操作在单个线程内按序执行,而并发流操作则在多个线程内并发执行。
下面的示例展示了通过并发流操作来提升程序性能是多么容易。
首先先创建一个元素各不相同的大数据集。
int max = 100_0000;
List<String> values = new ArrayList<>(max);
for(int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}
接下来我们将对这个集合所产生的流对象进行排序消耗的时间进行测量。
有序流操作
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
并发流操作
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("sequential sort took: %d ms", millis)); // sequential sort took: 472 ms
正如你看到的这两段代码几乎完全一样,但并发流排序几乎要快 50%,你只需要将 stream() 改成 parallelStream() 即可。
Map
前面提到过,Map 是不支持流操作的,但 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));
上面的代码通过函数名就可以知道是什么意思:putIfAbsesnt 函数可以防止我们写下额外的 null 检查语句;forEach 函数接收一个 BiCosumer 函数接口来对 map 对西那个中的每个键值执行操作。
下面的示例展示了如何使用函数对 map 中的数据执行 compute 操作。
map.computeIfPresent(3, (key, value) -> value + key);
map.get(3); // val33
map.computeIfPresent(9, (key, value) -> null);
map.containsKey(9); // false
map.computeIfAbsent(23, key -> "val");
map.containsKey(23); // true
map.computeIfAbsent(3, key -> String.valueOf(key));
map.get(3); // val33
接下来,我们将学习如何在给定一个 key 的情况下,且该 key 被映射到一个给定的 value 时,才能移除 entry。
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
其它有用的方法
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
merge 函数在 map 中不存在 entry 时,会将键值对放入到 map 中,否则将会调用 BiFunction 函数接口提供的方法来改变 entry 中的值。
Date API
Java 8 在包 java.time 下创建了全新的 Date 和 Time API。新版的 Date API 类似于 Joda-Time,但是不完全一样。下面的示例覆盖了新 API 的最重要的部分。
Clock
Clock 提供了访问当前 date 和 time 的方法。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);
TimeZone
TimeZone 由 ZoneId 来表征,ZoneId 可以方便地通过静态工厂方法来访问。TimeZone 定义了偏移量,该偏移量对于在 Instant 对象、Date 对象和 Time 对象 之间进行转换很重要。
ZoneId.getAvailableZoneIds();
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules()); // ZoneRules[currentStandardOffset=+01:00]
System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=-03:00]
LocalTime
LocalTime 对象代表了某个时区下的时间,比如 10pm 或者 17:30:15。下面的代码基于上面代码中创建的时区来创建了两个 LocalTime 对象。接着我们将比较两个 time 以及来计算两个 time 之间小时和分钟之间的差别。
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // 19
System.out.println(minutesBetween); // 1140
LocalTime 提供了多种工厂方法来简化新实例的创建,包括解析 time 串。
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 对象进行加减一天,一个月或一年来计算得到新的 LocalDate 对象。需要注意的是,每个操作都会返回一个新的实例。
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(); // 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 和 LocaDate 类似。我们可以使用方法来检索日期时间对象的特定字段。
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
提供额外的时区信息,LocalDateTime 对象还可以转换成 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 CST 2014
格式化日期时间对象与格式化日期对象和时间对象相似,除了使用预定义的格式外我们还可以从自定义的格式模板中创建格式化对象(formatter)。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("11 03, 2014-07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string); // 11 03, 2014 - 07:13
与 java.text.NumberFormat 不同的是,DateTimeFormatter 是不可变且线程安全的。
注解
Java 8 中的注解允许重复,让我们直接用例子来进行说明吧。
首先,我们定义一个装有一组注解的包装器注解。
@interface Hints {
Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
String value();
}
Java 8 允许我们使用 @Repetable 注解来多次使用同一类型的注解。
方式一: 使用容器注解(老版本)
@Hint({@Hint("hint1"), @Hint("hint2")})
Class Person {}
方式二: 使用可重复的注解(新版本)
@Hint("hint1")
@Hint("hint2")
Class Person {}
使用第二种方式时,Java 编译器将隐式地使用 @Hints 注解,这对于通过反射来读取注解信息非常重要。
Hint hint = Person.class.getAnnotation(Hint.class);
Hints hints1 = Person.class.getAnnotation(Hints.class);
on
Hint[] hints = Person.class.getAnnotationsByType(Hint.class);
尽管我们没有在 Person 类上声明使用 @Hints 注解,但依然能通过方法 getAnnotation(Hints.class) 读到该注解的信息。然而,更方便的方法是使用方法 getAnnotationsByType,它可以访问所有的 @Hint 注解信息。
另外,Java 8 新增了两种新的 @Target 参数。
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}