sOlOHsU's Blogβ

灯火阑珊处

Java 8 简明教程(10): Annotations

Annotations

Java 8 中 annotations 是可重复的。我们通过下面这里例子来看一下。

首先,我们定义一个包装器annotation,其中存放着真正的annotations的数组:

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

Java 8 中我们可以使用添加@Repeatable来使用多个同种类型的注解。

变体1:使用容器annotation(过去的方法)

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

变体2:使用可重复的annotations(新式方法)

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 中的annotations还扩展了两个新的target:

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

Java 8 简明教程(9): Date API

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对象。

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对象来表示,可以使用ZoneId的静态工厂方法很方便的获取一个时区对象。时区实际上就是定义了一个偏移量,这个偏移量在进行instant与本地的date、time进行转换时很重要。

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表示一个无时区的时间,例如 10pm 或者 17:30:15。下面的例子中分别为上面定义的两个timezone创建了一个localtime,然后对两个时间进行比较,并计算小时和分钟的差值。

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

LocalDate表示一个明确的日期。它是不可变的,工作方式与LocalTime类似。下面的例子演示了如何通过加/减天数、月数、年数来计算新的日期。需要注意的是每次操作返回的都是一个新的实例。

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

同样可以将字符串解析成一个LocalDate对象:

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

LocalDateTime表示一个date-time。它将date和time放到一个对象中。LocalDateTime是不可变的,工作方式和LocalTime,LocalDate也类似。我们可以利用方法来获取date-time中的各个字段:

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

给定一个时区信息的话可以将它转换成一个instant对象。然后可以将Instant对象转换成遗留的时间对象java.util.Date

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

date-time的格式化与date、time的格式化类似。除了使用预定义的格式之外,我们还可以使用自定义的pattern创建新的formatter对象:

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对象是不可更改并且线程安全的。

关于pattern语法的详细信息参看官方文档

Java 8 简明教程(8): Map

Map

虽然map是不支持stream的。但是Java 8中为map增加了多种很有用的方法,方便我们执行各种针对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));

上面的代码很简明:putIfAbsent方法使我们不用再写额外的if null检查语句了;forEach方法接受一个「消费者」,对map中的每一个值执行相应操作。

下面的例子展示了如何使用函数来对指定的value进行计算:

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

下面来看一下如何移除完全匹配给定的key/value的entries:

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

另外一个很有用的方法:

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

进行entries的合并也非常简单:

1
2
3
4
5
6
 
  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方法在给定的key不存在时,将key/value添加进map中,否则对现有的value进行修改。

Java 8 简明教程(7): Streams

Streams

java.util.Stream表示一个多元素的数组,在上面可以进行一项或者多项操作。Stream操作分为两种,一种是intermediate操作,一种是terminal操作。terminal操作返回的是某一特定类型的结果,而intermediate操作返回的stream对象本身,这样以来我们就可以在一行中将多个方法链接起来。Streams创建的时候必须指定一个source,例如java.util.Collection中的lists或者sets(maps是不被支持的)。Streams操作既可以是串行的也可以是并行的。

首先来看一个串行执行streams操作的例子,这个例子中的source是一个string数组:

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对Collections进行了扩展,我们可以直接调用Collection.stream()或者Collection.parallelStream()来创建stream。下面是一些常见的stream操作。

Filter

Filter接受一个判断条件并对stream中的所有元素进行过滤操作。这是一个intermediate操作,我们可以在它的返回结果上继续调用其他的stream操作(比如这里的forEach)。ForEach接受一个「消费者」,这个「消费者」对经过过滤后的stream中的每一个元素进行「消费」。ForEach是一个terminal操作。它的返回值是void,所以我们无法再继续调用其他操作了。

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

Sorted

Sorted是一个intermediate操作,它返回经过排序后的stream。默认的排序是按照自然顺序,想要实现自定义的比较方式需要向sorted方法传递一个自定义的Comparator。

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

需要注意的是,sorted仅创建了当前stream经过排序后的一个视图,而没有真正修改后方的collection。stringCollection中的顺序依然保持不变:

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

Map

map也是一个intermediate操作,它使用给定的函数将每个元素转换成另一个对象。下面的例子中将每一个字符串进行了upper操作。另外,也可以使用map将对象转换成另一种类型。得到的stream的泛型取决于传递给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

各式各样的匹配操作可以用来检查一个特定的谓词表达式是否与stream相匹配。所有的匹配操作都是terminal类型的,并且返回的是boolean类型的结果。

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

count是一个terminal操作,它以long类型返回stream中元素的个数。

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

Reduce

reduce是一个terminal操作,它使用给定的函数对stream中的元素进行reduction操作。返回的是一个Optional对象,其中存放着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

上面提到了streams分为串行和并行两类。串行streams中的操作都是在一个线程中执行的,而并行sterams上的操作是在多个线程中并发执行的。

下面的例子向我们展示了使用并行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());
  }

接下来我们来测一下对这个数组的stream进行排序所消耗的时间。

串行排序

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

Java 8 简明教程(6): Built-in Functional Interfaces

内置的函数式接口

JDK 1.8 的API中包含很多内置的函数式接口。它们中有些从旧版本就开始被人们所熟知,如ComparatorRunnable。这些现存的接口通过添加@FunctionalInterface注解进行扩展,以提供对lambda的支持。

Java 8的API同样拥有大量的函数式接口。许多接口都来自著名的Google Guava library。就算你对这个库不熟悉,你也应该去研究研究这些接口是如何进行的方法扩展。

Predicates

Predicates是一些接受一个参数并返回boolean类型值的函数。这个接口包含各种用来组成复杂的逻辑术语的谓词的默认方法(and,or,negate)。

1
2
3
4
5
6
7
8
9
10
 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接受一个参数,并产生一个结果。默认方法可以用来将多个Functions链接到一起(compose,andThen)。

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

Suppliers

Suppliers产生一个给定的泛型结果。与Functions不同的是,Suppliers不接受任何参数。

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

Consumers

Consumers表示在一个输入参数上进行的操作。

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

Comparators

Comparators在旧版本中就很有名了。Java 8又向这个接口中添加了许多默认方法。

1
2
3
4
5
6
7
 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是下一节中的重要概念,这里我们先大体看一下它是如何工作的。

Optional是一个值的简单容器,这个值可以为null,也可以不为null。想象有一个可以返回非null的结果但有时什么都不返回的方法。在Java 8中你可以返回一个Optional而不返回null。

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

Java 8 简明教程(5): Lambda Scope

Lambda的作用域

在lambda表达式中访问外部变量的方式与匿名对象相似。从lambda表达式中可以访问外部作用域中的final局部变量,外部对象的成员字段和静态变量。

访问局部变量

从lambda表达式中访问外部的final局部变量:

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

但是与匿名对象不同的是,num不必显式的声明为final变量。所以下面的代码也是正确的:

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

但是为了代码能成功编译,num必须隐含为final变量。像下面的代码就无法编译通过:

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

在lambda表达式里对num进行写操作也是被禁止的。

访问字段和静态变量

与访问局部变量时相反,在lambda表达式内我们可以对外部对象的成员字段和静态变量进行读写操作。这个特性在匿名对象中已经广为人知了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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);
          };
      }
  }

访问默认接口方法

还记得教程(1)中的formula的例子吗?Formula接口定义了一个默认的方法sqrt,任何一个formula的实例都可以访问这个方法,包括匿名对象在内。但在lambda表达式中这个就不适用了。

在lambda表达式内是无法访问默认方法的。下面的代码无法通过编译:

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

Java 8 简明教程(4): Method and Constructor References

方法和构造器的引用

上个教程中的例子可以通过使用静态方法的引用来进一步简化:

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

Java 8 中可以使用::关键字来传递对方法或构造器的引用。上面的例子展示了如何引用一个静态方法。同样我们也可以引用对象中的方法:

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

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

接下来定义一个person类的工厂接口来创建person实例。

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

通过使用构造器的引用我们就可以避免手动去实现上面的工厂接口,代码简洁了很多。

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

我们使用Person::new创建了一个对Person类的构造器的引用。Java编译器会通过匹配PersonFactory.create方法的签名自动选用合适的构造器。

Java 8 简明教程(3): Functional Interface

函数式接口

lambda表达式是如何符合Java的类型系统的呢?每个lambda表达式对应一个指定的函数式接口。所谓的函数式接口(functional interface)必须包含且仅包含一个抽象方法。每个lambada表达式都会与对应的函数式接口中的这个抽象方法进行匹配。而由于default方法不是抽象的,所以我们可以向函数式接口中随意添加default方法。

只要一个接口只含有一个抽象方法,我们就可以将它用作lambda表达式。我们可以使用@FunctionalInterface注解来确保我们的接口能够满足这一需求。当我们尝试向添加了此注解的接口中添加第二个抽象方法时,编译器会提示存在编译错误。

例子:

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

需要注意的是去掉@FucntionalInterface注解后代码依然是正确有效的。

Java 8 简明教程(2): Lambda Expression

Lambda 表达式

先来看一个对字符串数组进行排序的例子,在之前的Java版本中,我们通常这样来实现:

1
2
3
4
5
6
7
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接受一个数组和一个比较器来对这个数组进行排序。因而我们经常需要创建一个匿名的比较器并把它传递给sort方法。

为了避免整天没完没了的创建匿名对象,Java 8使用了一种更简洁的语法,lambda表达式:

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

可以看到代码比以前短了不少并且变得更加易读。但是它还能变得更短:

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

如果方法中只有一行语句,可以省略大括号和return关键字。但是,这还没完:

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

Java编译器会自动进行参数类型检查,所以这些我们也可以省略掉。接下来我们将更深入的讲解一下lambda表达式更广泛的用途。

Java 8 简明教程(1): Default Method

接口的默认方法

Java 8 中我们可以利用 default 关键字来为接口添加非抽象方法。这个特性也被称为扩展方法

例子如下:

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

除了calculate方法这个抽象方法之外,Formula接口还定义了一个默认方法sqrt,接口的实现类只需要实现抽象方法calculate,而默认方法sqrt是直接可以拿来用的。

1
2
3
4
5
6
7
8
9
 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对象使用匿名对象的方式实现了Formula接口。代码看起来比较啰嗦,用了6行代码实现了一个简单的计算sqrt(a * 100)。在后面的教程中,我们将会看到在Java 8 中如何使用更简单的方式来实现一个单方法对象。