当前位置:   article > 正文

Java 8 新特性

Java 8 新特性

本文译自Java8-tutorial,并对其中内容进行了一些修改和补充。

接口的默认方法

在 Java 8 中,我们可以通过default关键字来为接口添加非抽象方法。default关键字修饰的方法称为默认方法,它允许我们添加新的功能到现有库的接口中,并能确保与采用旧版本接口编写的代码之间相互兼容。
对于以下例子:

  1. interface Formula {
  2. double calculate(int a);
  3. default double sqrt(int a) {
  4. return Math.sqrt(a);
  5. }
  6. }

Formula接口中,除了有抽象方法calculate,还定义了默认方法sqrtFormula的实现类只需实现抽象方法calculate,默认方法sqrt可直接使用接口中的定义,也可以在具体类中重写。

  1. Formula formula = new Formula() {
  2. @Override
  3. public double calculate(int a) {
  4. return sqrt(a * 100);
  5. }
  6. };
  7. formula.calculate(100); // 100.0
  8. formula.sqrt(16); // 4.0

上面的代码中,formula 以匿名对象的方式实现了Formula接口,而这只是为了实现sqrt(a * 100),略显繁琐的,在下一部分,将讨论一种在 Java 8 中更为优雅的实现方式。

Lambda表达式

首先让我们用 1.8 之前的 Java 版本来对一串字符串进行排序:

  1. List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
  2. Collections.sort(names, new Comparator<String>() {
  3. @Override
  4. public int compare(String a, String b) {
  5. return b.compareTo(a);
  6. }
  7. });

静态工具方法Collections.sort接受一个列表和一个比较器来对给定的列表中的元素进行排序,你会发现你经常需要创建匿名比较器传给排序函数。
为了避免一直创建匿名对象,Java 8 通过lambad 表达式来简化语法规则:

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

上面的代码更加精简,可读性也更强,当然,还可以继续精简:

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

Lambda 表达式的主体只有一条语句时,花括号{}和return关键字可省略。
现在,列表有了一个sort方法,另外,当可以从上下文推断出参数的类型,同样可以省略掉参数类型。

Lambad表达式的结构

  • 一个 Lambda 表达式可以有零个或多个参数
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同
  • 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空圆括号代表参数集为空。例如:() -> 42
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
  • Lambda 表达式的主体可包含零条或多条语句
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空

函数式接口

Lambda 表达式如何匹配 Java 的类型系统的呢?每个 Lambda 对应一个特定的接口,与一个给定的类型相匹配,一个所谓的函数式接口只包含一个抽象方法声明,每个 Lambda 表达式都与那个类型的抽象方法匹配。因为默认方法并非抽象的,因此我们可以向函数式接口任意添加默认方法。
我们可以使用任意只包含一个抽象方法声明的接口来作为 Lambda 表达式,为了确保使用的是函数式接口,我们可以添加@FunctionalInterface注解,编译器就会察觉到这个注解,并且当我们尝试往函数式接口添加第二个抽象方法声明时抛出异常。

  1. @FunctionalInterface
  2. interface Converter<F, T> {
  3. T convert(F from);
  4. }
  5. Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
  6. Integer converted = converter.convert("123");
  7. System.out.println(converted); // 123

假使没有@FunctionalInterface注解,上述代码仍然是正确的。

方法和构造器引用

方法引用的分类:

  • 类名::静态方法名
  • 对象::实例方法名
  • 类名::实例方法名
  • 类名::new

类名::静态方法名

上述例子中的代码可以进一步通过静态方法引用来精简:

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

Java 8 中你可以通过::关键字来传递方法或者构造器引用,上述的例子说明了如何引用一个静态方法。

对象::实例方法名

我们也可以引用一个对象方法:

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

类名::实例方法名

  1. public class Person {
  2. private String name;
  3. public Person() {
  4. }
  5. public Student(String name){
  6. this.name = name;
  7. }
  8. public int compareByScore(Student student){
  9. return this.getScore() - student.getScore();
  10. }
  11. }
  1. students.sort(Student::compareByScore);
  2. students.forEach(student -> System.out.println(student.getScore()));

sort 方法接收的 Lambda 表达式本该有两个参数,而这个实例方法只有一个参数也满足 Lambda 表达式的定义。这就是 类名::实例方法名 这种方法引用的特殊之处,当使用 类名::实例方法名 方法引用时,一定是 Lambda 表达式所接收的第一个参数来调用实例方法,如果 Lambda 表达式接收多个参数,其余的参数作为方法的参数传递进去。

类名::new

让我们来看看::关键字在构造器引用中是如何使用的。首先,我们定义一个有多个构造函数的类:

  1. class Person {
  2. String firstName;
  3. String lastName;
  4. Person() {}
  5. Person(String firstName, String lastName) {
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. }
  9. }

接下来,我们创建一个用于创建新人员的工厂接口:

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

除了传统方式实现工厂接口外,通过构造器引用的方式

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

我们通过Person::new向 Person 构造器传了一个引用(注:Person类中需要有无参构造器),Java编译器会自动选择正确的构造器。

Lambda作用域

Lambda 表达式访问外部变量的方式与匿名对象非常相似,它可以访问局部外围的 final 变量、成员变量和静态变量。

访问局部变量

我们可以在 Lambda 表达式所在的外部范围访问final修饰的局部变量

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

不同于匿名对象的是,上述变量 num 不一定要被声明为 final(匿名内部类中的参数必须声明为 final,其值是 capture by value的),下述代码也是正确的:

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

值得注意的是,虽然 num 变量不需要显式声明为 final,但实际上,编译器要求 Lambda 表达式中捕获的变量必须实际上是最终变量(也就是初始化后不可再赋新值)所以 num 不可更改,下述代码无法通过编译,原因就是 num 的值被更改了:

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

访问成员变量和静态变量

与局部变量不同的是,Lambda 表达式中,可以对成员变量和静态变量进行读和写操作。

  1. class Lambda4 {
  2. static int outerStaticNum;
  3. int outerNum;
  4. void testScopes() {
  5. Converter<Integer, String> stringConverter1 = (from) -> {
  6. outerNum = 23;
  7. return String.valueOf(from);
  8. };
  9. Converter<Integer, String> stringConverter2 = (from) -> {
  10. outerStaticNum = 72;
  11. return String.valueOf(from);
  12. };
  13. }
  14. }

访问接口的默认方法

在第一部分中关于 formula 的例子,Formula接口定义了一个sqrt的默认方法,其可以被任意一个 formula 实例包括匿名对象访问,但是在 Lambda 表达式中却不行,Lambda 表达式无法访问接口的默认方法,下述代码是错误的:

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

内置函数式接口

JDK 1.8 API 中包含了很多内置的函数式接口,其中一部分例如ComparatorRunnable在之前的 JDK 版本中就被人熟知。这些现有的接口通过@FunctionalInterface注解被拓展来支持 Lambda。
Java 8中的 API 也提供了一些新的函数式接口来使得编程更加简单。
以下是常用的函数式接口

函数式接口参数类型返回类型抽象方法名描述其他方法
Runnablevoidrun作为无参数或返回值的动作运行
SupplierTget提供一个T类型的值
ConsumerTvoidaccept处理一个T类型的值andThen
BiConsumer<T,U>T,Uvoidaccept处理T和U类型的值andThen
Function<T,R>TRapply有一个T类型参数的函数compose,andThen,identity
BiFunction<T,U,R>T,URapply有T和U类型参数的函数andThen
UnaryOperatorTTapply类型T上的一元操作符compose,andThen,identity
BinaryOperatorT,TTapply类型T上的二元操作符andThen,maxBy,minBy
PredicateTbooleantest布尔值函数and,or,negate,isEqual
BiPredicate<T,U>T,Ubooleantest有两个参数的布尔值函数and,or,negate

Predicate

Predicate 是一个布尔类型的函数,该函数只有一个参数,该接口包含了多种默认方法,用于处理复杂的逻辑动词。

  1. Predicate<String> predicate = (s) -> s.length() > 0;
  2. predicate.test("foo"); // true
  3. predicate.negate().test("foo"); // false
  4. Predicate<Boolean> nonNull = Objects::nonNull;
  5. Predicate<Boolean> isNull = Objects::isNull;
  6. Predicate<String> isEmpty = String::isEmpty;
  7. Predicate<String> isNotEmpty = isEmpty.negate();

Function

Function接受一个参数并且返回一个结果,可以使用默认方法(compose,andThen)将多个函数链接起来。

  1. Function<String, Integer> toInteger = Integer::valueOf;
  2. Function<String, String> backToString = toInteger.andThen(String::valueOf);
  3. backToString.apply("123"); // "123"

Supplier

Supplier返回一个给定类型的结果,与Function不同的是,Supplier不接受任何参数。

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

Consumer

Comsumer代表了在一个输入参数上需要进行的操作.

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

Comparator

Comparator在之前的 Java 版本就已经被熟知,Java 8 在这个接口中增加了多个默认方法。

  1. Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
  2. Person p1 = new Person("John", "Doe");
  3. Person p2 = new Person("Alice", "Wonderland");
  4. comparator.compare(p1, p2); // > 0
  5. comparator.reversed().compare(p1, p2); // < 0

Optional

Optional并非是一个函数式接口,但却是一个精巧的工具接口,用来防止NullPointerException,这个概念对于下一部分显得很重要,所以我们在这快速浏览一下Optional是如何工作的。
Optional是一个简单的值容器,这个值可以是 null,也可以是 non-null 的。考虑一个方法可能返回一个 non-null 值的结果,也有可能返回一个空值。在 Java 8中,为了不直接返回 null,你可以返回一个Optional

  1. Optional<String> optional = Optional.of("bam");
  2. optional.isPresent(); // true
  3. optional.get(); // "bam"
  4. optional.orElse("fallback"); // "bam"
  5. optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"

Streams

java.util.Stream代表了可以在其上面执行一个或多个操作的元素序列。流操作是中间或者完结操作。完结操作会返回一个某种类型的值,而中间操作会返回流本身,因此你可以连续链接多个方法的调用。Stream 是在一个源的基础上创建出来的,例如java.util.Collection中的 lists 或 sets(不支持 maps)。流操作可以被顺序或者并行执行。
让我们先来了解下序列流是如何工作的,首先,我们通过字符串列表的形式创建一个示例代码:

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

Java 8 中的集合已被拓展,因此你可以直接调用Collection.stream()·Collection.parallelStream()来创建流。接下来的部分将会解释最常用的流操作。

Filter

Filter 接受一个 predicate 类型的接口来过滤流中的元素。该操作是一个中间操作,因此它允许我们在返回结果的时候再调用其他流操作(forEach)。ForEach 接受一个 Consumer 类型的接口变量,用来执行对多虑的流中的每一个元素的操作。ForEach是一个完结操作,并且不返回流,因此我们不能再调用其他的流操作。

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

Sorted

Sorted 是一个中间操作,其返回一个流排序后的视图,流中的元素默认按照自然顺序进行排序,除非你指定了一个Comparator接口来重定义排序规则。

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

需要注意的是,sorted只是创建了流排序后的视图,并没有操作操作集合,集合中元素的顺序是没有改变的。

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

Map

中间操作map通过特定的接口将每个元素转换为另一个对象,下面的例子将每一个字符串转换为全为大写的字符串。当然,你可以使用map将每一个对象转换为其他类型。对于带泛型结果的流对象,具体的类型还要由传递给 map 的泛型方法来决定。

  1. stringCollection
  2. .stream()
  3. .map(String::toUpperCase)
  4. .sorted((a, b) -> b.compareTo(a))
  5. .forEach(System.out::println);
  6. // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

有多种匹配操作可以用来检查某一种规则是否与流对象相匹配。所有的匹配操作都是完结操作,并且返回一个 boolean 类型的结果。

  1. boolean anyStartsWithA =
  2. stringCollection
  3. .stream()
  4. .anyMatch((s) -> s.startsWith("a"));
  5. System.out.println(anyStartsWithA); // true
  6. boolean allStartsWithA =
  7. stringCollection
  8. .stream()
  9. .allMatch((s) -> s.startsWith("a"));
  10. System.out.println(allStartsWithA); // false
  11. boolean noneStartsWithZ =
  12. stringCollection
  13. .stream()
  14. .noneMatch((s) -> s.startsWith("z"));
  15. System.out.println(noneStartsWithZ); // true

Count

Count 是一个完结操作,它返回一个 long 类型数值,用来标识流对象中包含的元素数量。

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

Reduce

这个完结操作通过给定的函数来对流元素进行削减操作,该缩减操作的结果保存在Optional变量中。

  1. Optional<String> reduced =
  2. stringCollection
  3. .stream()
  4. .sorted()
  5. .reduce((s1, s2) -> s1 + "#" + s2);
  6. reduced.ifPresent(System.out::println);
  7. // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallel Streams

正如上面提到的,stream 可以是顺序的也可以是并行的。顺序操作通过单线程执行,而并行操作通过多线程执行。
下面的例子说明了使用并行流提高运行效率是多么的容易。
首先我们创建一个包含不同元素的列表:

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

现在我们测量一下对这个集合进行排序需要花的时间。

  • Sequential Sort
  1. long t0 = System.nanoTime();
  2. long count = values.stream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
  6. System.out.println(String.format("sequential sort took: %d ms", millis));
  7. // sequential sort took: 899 ms
  • Parallel Sort
  1. long t0 = System.nanoTime();
  2. long count = values.parallelStream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
  6. System.out.println(String.format("parallel sort took: %d ms", millis));
  7. // parallel sort took: 472 ms

两个代码片段几乎一样,但是使用并行操作来排序的效率提高了接近一半,而你需要做得就仅是将stream替换为parallelStream

Map

正如前面提到的,map 是不支持流操作的。Map接口本身没有可用的stream()方法,但是你可以根据键-值对或项通过map.keySet().streammap.values().stream()map.entrySet().stream()来创建指定的流。
此外,map 支持多种新的、有用的方法来处理常规任务。

  1. Map<Integer, String> map = new HashMap<>();
  2. for (int i = 0; i < 10; i++) {
  3. map.putIfAbsent(i, "val" + i);
  4. }
  5. map.forEach((id, val) -> System.out.println(val));

上面的代码是自解释的,putIfAbsent防止我们写入额外的空值检查,forEach接受一个 Consumer 为 map 中的每一个值进行操作。
下面的例子说明了如何利用函数来计算 map 上的代码

  1. map.computeIfPresent(3, (num, val) -> val + num);
  2. map.get(3); // val33
  3. map.computeIfPresent(9, (num, val) -> null);
  4. map.containsKey(9); // false
  5. map.computeIfAbsent(23, num -> "val" + num);
  6. map.containsKey(23); // true
  7. map.computeIfAbsent(3, num -> "bam");
  8. map.get(3); // val33

接下来,我们学习如何删除给定键的条目,只有当前键值映射到给定值时,才能删除指定条目

  1. map.remove(3, "val3");
  2. map.get(3); // val33
  3. map.remove(3, "val33");
  4. map.get(3); // null

另一个有用的方法:

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

合并一个 map 的条目是很简单的:

  1. map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
  2. map.get(9); // val9
  3. map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
  4. map.get(9); // val9concat

如果不存在该键值的条目,合并或者将键/值放入 map 中,或者调用合并函数来更改现有值。

日期API

Java 8 在java.time包下包含了全新的日期和时间 API,这个新的日期 API 与 Joda-Time 库相似,但不完全一样。下面的例子涵盖了大部分新的 API。

Clock

Clock 提供了对当前日期和时间的访问,Clocks 知道当前时区,可以使用它替代System.currentTimeMillis()来获取当前的毫秒时间。时间线上的某一时刻也由类Instant表示,Instants 可以用来创建遗留的java.util.Date对象。

  1. Clock clock = Clock.systemDefaultZone();
  2. long millis = clock.millis();
  3. Instant instant = clock.instant();
  4. Date legacyDate = Date.from(instant); // legacy java.util.Date

Timezones

Timezone 由一个ZoneId来表示,他们可以通过静态工厂方法获得。时区定义了某一时刻和当地日期、时间之间转换的偏移量。

  1. System.out.println(ZoneId.getAvailableZoneIds());
  2. // prints all available timezone ids
  3. ZoneId zone1 = ZoneId.of("Europe/Berlin");
  4. ZoneId zone2 = ZoneId.of("Brazil/East");
  5. System.out.println(zone1.getRules());
  6. System.out.println(zone2.getRules());
  7. // ZoneRules[currentStandardOffset=+01:00]
  8. // ZoneRules[currentStandardOffset=-03:00]

LocalTime

LocalTime 表示了一个没有指定时区的时间,例如 10 p.m 或者 17:30:15。下面的例子为上面定义的时区创建了两个本地时间,然后我们比较两个时间,并计算它们之间的小时和分钟之间的不同。

  1. LocalTime now1 = LocalTime.now(zone1);
  2. LocalTime now2 = LocalTime.now(zone2);
  3. System.out.println(now1.isBefore(now2)); // false
  4. long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
  5. long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
  6. System.out.println(hoursBetween); // -3
  7. System.out.println(minutesBetween); // -239

LocalTime带有多种工厂方法,以简化新实例的创建,包括对时间字符串进行解析操作。

  1. LocalTime late = LocalTime.of(23, 59, 59);
  2. System.out.println(late); // 23:59:59
  3. DateTimeFormatter germanFormatter =
  4. DateTimeFormatter
  5. .ofLocalizedTime(FormatStyle.SHORT)
  6. .withLocale(Locale.GERMAN);
  7. LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
  8. System.out.println(leetTime); // 13:37

LocalDate

LocalDate 表示不同的日期,例如2014-03-11。它是不可变的,并且与LocalTime完全类似。下面的例子演示了如何通过加减日、月、年来计算新日期。需要注意的是,每一个操作都会返回一个新实例。

  1. LocalDate today = LocalDate.now();
  2. LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
  3. LocalDate yesterday = tomorrow.minusDays(2);
  4. LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
  5. DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
  6. System.out.println(dayOfWeek); // FRIDAY

从字符串中解析 LocalDate 就跟解析 LocalTime 一样简单:

  1. DateTimeFormatter germanFormatter =
  2. DateTimeFormatter
  3. .ofLocalizedDate(FormatStyle.MEDIUM)
  4. .withLocale(Locale.GERMAN);
  5. LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
  6. System.out.println(xmas); // 2014-12-24

LocalDateTime

LocalDateTIme 表示的是日期-时间。它将日期和时间组合成一个实例。LocalDateTime是不可变的,与 LocalTimeLocalDate工作原理类似。我们可以利用方法去获取日期时间中的某些字段值。

  1. LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
  2. DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
  3. System.out.println(dayOfWeek); // WEDNESDAY
  4. Month month = sylvester.getMonth();
  5. System.out.println(month); // DECEMBER
  6. long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
  7. System.out.println(minuteOfDay); // 1439

通过一个时区的附件信息可以转换为一个实例,这个实例很容易转为java.util.Date类型。

  1. Instant instant = sylvester
  2. .atZone(ZoneId.systemDefault())
  3. .toInstant();
  4. Date legacyDate = Date.from(instant);
  5. System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

日期-时间的格式化类似于 Date 或 Time。我们可以使用自定义模式来取代预定义的格式进行格式化。

  1. DateTimeFormatter formatter =
  2. DateTimeFormatter
  3. .ofPattern("MMM dd, yyyy - HH:mm");
  4. LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
  5. String string = formatter.format(parsed);
  6. System.out.println(string); // Nov 03, 2014 - 07:13

不像java.text.NumberFormatDateTimeFormatter是不可变的并且是线程安全的。
了解更多有关日期格式化的信息可以参考这里

注解

Java 8中的注解是可重复的,我们直接通过一个例子来了解它。
首先,我们定义了一个包装注解,它包括了一个实际注解的数组:

  1. @interface Hints {
  2. Hint[] value();
  3. }
  4. @Repeatable(Hints.class)
  5. @interface Hint {
  6. String value();
  7. }

Java 8 允许我们通过使用@Repeatable对同一类型使用多个注解

  • 变体一:使用注解容器(老方法)
  1. @Hints({@Hint("hint1"), @Hint("hint2")})
  2. class Person {}
  • 变体二:使用可重复注解(新方法)
  1. @Hint("hint1")
  2. @Hint("hint2")
  3. class Person {}

使用变体2,Java 编译器隐式地对@Hint进行设置,这对于通过反射来读取注解信息非常重要。

  1. Hint hint = Person.class.getAnnotation(Hint.class);
  2. System.out.println(hint); // null
  3. Hints hints1 = Person.class.getAnnotation(Hints.class);
  4. System.out.println(hints1.value().length); // 2
  5. Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
  6. System.out.println(hints2.length); // 2

尽管我们不会在Person类中声明@Hints注解,但是它仍然可以通过getAnnotation(Hint.class)来读取。然后,更便利的方法是getAnnotationByType,它可以直接访问@Hint注解。
此外,Java 8 中关于注解的使用,其还拓展了两个新的目标:

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

转载于:https://www.cnblogs.com/ZhaoxiCheung/p/Java8-tutorial.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/560926
推荐阅读
相关标签
  

闽ICP备14008679号