jdk1.8新特性实战篇

218 阅读9分钟

一、在接口中提供默认的方法实现(有点像抽象类)

在jdk1.8里面,不仅可以定义接口,还可以在接口中提供默认的实现。这一个小小的改变却让整个抽象设计都随着改变了!

二、Lambda表达式

Lambda 表达式(是一种匿名函数)。它是推动 Java 8 发布的最重要新特性。是继泛型和注解以来最大的变化。

使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。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 方法,入参是一个 List 集合,和一个 Comparator 比较器,以便对给定的 List 集合进行排序。上面的示例代码创建了一个匿名内部类作为入参,这种类似的操作在我们日常的工作中随处可见。

Java 8 推荐使用 Lambda 表达:

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

更加简单优秀的写法:

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

再短点(如果你的实现不是一行代码,那么不能这么干):

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

三、函数式接口(Functional Interface)

并不是每个接口都可以缩写成Lambda表达式的开发方式。其实是只有那些函数式接口才能缩写成 Lambda 表示式。(函数式接口可以被隐式转换为lambda表达式)

所谓函数式接口就是只包含一个抽象方法的声明(但可以有多个非抽象方法的接口)。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。(另外,只是在接口上添加default并不算抽象方法)

总结:为了保证一个接口明确的被定义为一个函数式接口,我们需要为该接口添加注解:@FunctionalInterface。这样,一旦你添加了第二个抽象方法,编译器会立刻抛出错误提示。(不填写,但是只写一个default也可以)

//定义含有注解@FunctionalInterface的接口
@FunctionalInterface
public interface IConverter<F, T> {
    T convert(F from);
}

1、先来一个传统方式:

IConverter<String, Integer> converter01 = new IConverter<String, Integer>() {
@Override
public Integer convert(String from) {
	return Integer.valueOf(from);
}

2、稍微简化下,只有一个参数括号可以不要:

IConverter<String, Integer> converter02 = (from) -> {
    return Integer.valueOf(from);
};

3、继续简化,因为它的实现只有一行代码,可以更简短:

IConverter<String, Integer> converter03 = from -> Integer.valueOf(from);

4、还能短点,其实这个另类属于下一段的内容了,先放这有个印象

IConverter<Integer, String> converter04 = String::valueOf;

四、方法和构造函数的便捷应用

五、Lambda作用范围

Lambda表达式访问外部的变量(局部变量,成员变量,静态变量,接口的默认方法),它与匿名内部类访问外部变量非常相似。

六、内置的函数式接口

JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。

例如我们旧版本的Jdk中常用的 Comparator 和 Runnable 外,还有一些新的函数式接口,可以通过函数注解实现Lamdba支持,它们很多都借鉴于知名的 Google Guava库。

七、Optionals

Optional不是一个函数式接口,设计它的目的是为了防止空指针异常(NullPointerException)。

当你定义了一个方法,这个方法返回的对象可能是空,也有可能非空的时候,你就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。

@Test
public void test16(){
    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"
    Optional<Person> optionalPerson = Optional.of(new Person());
    optionalPerson.ifPresent(s -> System.out.println(s.firstName));
}

八、Stream流

我们可以使用 java.util.Stream 对一个包含一个或多个元素的集合做各种操作(只能对实现了 java.util.Collection 接口的类做流的操作)。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream 流支持同步执行,也支持并发执行(注意:Map不支持Stream流,但是它的key和value是支持的!)。在 Java 8 中,集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

1、forEach 迭代

Stream 提供了新的方法 'forEach' 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

2、Filter 过滤

filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:

List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.stream().filter(string -> string.isEmpty()).count();

3、Sorted 排序

Sorted 用于对流进行排序。是一个中间操作,它的返参是一个 Stream 流。另外,我们可以传入一个 Comparator 用来自定义排序,如果不传,则使用默认的排序规则。

@Test
public void test18() {
	stringCollection.stream().sorted()
			.filter((s) -> s.startsWith("a"))
			.forEach(System.out::println);
}

注意:这个sorted 只是做了一个排序的视图进行输出,实际没有将List内的数据进行排序

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

4、Map 转换

map 方法用于映射每个元素到对应的结果。如下,通过 map 我们将每一个 string 转成大写:

@Test
public void test19(){
    stringCollection
            .stream()
            .map(String::toUpperCase)
            .sorted(Comparator.reverseOrder())  //等同于(a, b) -> b.compareTo(a)
            .forEach(System.out::println);
}   

这个可以用做DTO数据对象转换,领域驱动设计开发中将DTO转为DO向后台传输。

5、Match 匹配

match 用来做匹配操作,它的返回值是一个 boolean 类型。通过 match, 我们可以方便的验证一个 list 中是否存在某个类型的元素。

@Test
public void test20(){
    // anyMatch:验证 list 中 string 是否有以 a 开头的, 匹配到第一个,即返回 true
    boolean anyStartsWithA =
            stringCollection
                    .stream()
                    .anyMatch((s) -> s.startsWith("a"));
    System.out.println(anyStartsWithA);      // true
    // allMatch:验证 list 中 string 是否都是以 a 开头的
    boolean allStartsWithA =
            stringCollection
                    .stream()
                    .allMatch((s) -> s.startsWith("a"));
    System.out.println(allStartsWithA);      // false
    // noneMatch:验证 list 中 string 是否都不是以 z 开头的
    boolean noneStartsWithZ =
            stringCollection
                    .stream()
                    .noneMatch((s) -> s.startsWith("z"));
    System.out.println(noneStartsWithZ);      // true
}   

6、limit 限制

limit 方法用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

7、Count 计数

count 是一个终端操作,它能够统计 stream 流中的元素总数,返回值是 long 类型。

@Test
public void test21() {
    // count:先对 list 中字符串开头为 b 进行过滤,让后统计数量
    long startsWithB =
            stringCollection
                    .stream()
                    .filter((s) -> s.startsWith("b"))
                    .count();
    System.out.println(startsWithB);    // 3
}   

8、Reduce

Reduce 中文翻译为:减少、缩小。通过入参的 Function,我们能够将 list 归约成一个值。它的返回类型是 Optional 类型。

@Test
public void test22() {
    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 并行流

流可以是顺序的,也可以是并行的。顺序流上的操作在单个线程上执行,而并行流上的操作在多个线程上并发执行。

下面的示例演示了使用并行流来提高性能是多么的容易。亲测提升了1倍性能!

首先,我们创建一个较大的List:

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

1、Sequential Sort 顺序流排序

@Test
public void test23() {
    int max = 1000000;
    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("顺序流排序耗时: %d ms", millis));
    //顺序流排序耗时: 712 ms
}

2、Parallel Sort 并行流排序

@Test
public void test24(){
    int max = 1000000;
    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.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: 385 ms
}

这两个代码片段几乎相同,但并行排序大约快50%。你只需将stream()更改为parallelStream()。

十、Map 集合

Map是不支持 Stream 流的,因为 Map 接口并没有像 Collection 接口那样,定义了 stream() 方法。但是,我们可以对其 key,values,entry 使用流操作,如 map.keySet().stream(),map.values().stream() 和 map.entrySet().stream().

另外,JDK 8 中对 map 提供了一些其他新特性:

@Test
public void test25() {
    Map<Integer, String> map = new HashMap<>();
    for (int i = 0; i < 10; i++) {
        // 与老版不同的是,putIfAbent() 方法在 put 之前,  不用在写if null continue了
        // 会判断 key 是否已经存在,存在则直接返回 value, 否则 put, 再返回 value
        map.putIfAbsent(i, "val" + i);
    }
    // forEach 可以很方便地对 map 进行遍历操作
    map.forEach((key, value) -> System.out.println(value));
}

之后我们做一个Map对象的转换输出;(定义两个类BeanA、BeanB)

@Test
public void test26() {
    Map<Integer, BeanA> map = new HashMap<>();
    for (int i = 0; i < 10; i++) {
        // 与老版不同的是,putIfAbent() 方法在 put 之前,不用在写if null continue了
        // 会判断 key 是否已经存在,存在则直接返回 value, 否则 put, 再返回 value
        map.putIfAbsent(i, new BeanA(i, "明明" + i, i + 20, "89021839021830912809" + i));
    }
    Stream<BeanB> beanBStream00 = map.values().stream().map(new Function<BeanA, BeanB>() {
        @Override
        public BeanB apply(BeanA beanA) {
            return new BeanB(beanA.getName(), beanA.getAge());
        }
    });
    Stream<BeanB> beanBStream01 = map.values().stream().map(beanA -> new BeanB(beanA.getName(), beanA.getAge()));
    beanBStream01.forEach(System.out::println);
}

除了上面的 putIfAbsent() 和 forEach() 外,我们还可以很方便地对某个 key 的值做相关操作:

@Test
public void test27() {
	// 如下:对 key 为 3 的值,内部会先判断值是否存在,存在,则做 value + key 的拼接操作
	map.computeIfPresent(3, (num, val) -> val + num);
	map.get(3);             // val33

	// 先判断 key 为 9 的元素是否存在,存在,则做删除操作
	map.computeIfPresent(9, (num, val) -> null);
	map.containsKey(9);     // false

	// computeIfAbsent(), 当 key 不存在时,才会做相关处理
	// 如下:先判断 key 为 23 的元素是否存在,不存在,则添加
	map.computeIfAbsent(23, num -> "val" + num);
	map.containsKey(23);    // true

	// 先判断 key 为 3 的元素是否存在,存在,则不做任何处理
	map.computeIfAbsent(3, num -> "bam");
	map.get(3);             // val33
}

关于删除操作,JDK 8 中提供了能够新的 remove() API:

@Test
public void test28() {
	map.remove(3, "val3");
	map.get(3);             // val33

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

如上代码,只有当给定的 key 和 value 完全匹配时,才会执行删除操作。

关于添加方法,JDK 8 中提供了带有默认值的 getOrDefault() 方法:

@Test
public void test29() {
    // 若 key 42 不存在,则返回 not found
    map.getOrDefault(42, "not found");  // not found
}

对于 value 的合并操作也变得更加简单:

@Test
public void test30() {
    // merge 方法,会先判断进行合并的 key 是否存在,不存在,则会添加元素
    map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
    map.get(9);             // val9
    // 若 key 的元素存在,则对 value 执行拼接操作
    map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
    map.get(9);             // val9concat
}

十一、日期 Date API

  1. Clock
  2. Timezones时区
  3. LocalTime
  4. LocalDate
  5. LocalDateTime

十二、Annotations 注解

Java8中的注释是可重复的。让我们直接深入到一个例子中来解决这个问题。{在SpringBoot的启动类中就可以看到这中类型的注解}

首先,我们定义一个包装器注释,它包含一个实际注释数组:

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

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

Java 8通过声明注释@Repeatable,使我们能够使用同一类型的多个注释。

第一种形态:使用注解容器(老方法)

 @Test
 public void test40() {
     @Hints({@Hint("hint1"), @Hint("hint2")})
     class Person {
     }
 }  

第二种形态:使用可重复注解(新方法)

@Test
public void test41() {
    @Hint("hint1")
    @Hint("hint2")
    class Person {
    }
}  

java编译器使用变量2隐式地在引擎盖下设置@Hints注释。这对于通过反射读取注释信息很重要。

@Test
public void test41() {
    @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
}  

尽管我们绝对不会在 Person 类上声明 @Hints 注解,但是它的信息仍然是可以通过 getAnnotation(Hints.class) 来读取的。 并且,getAnnotationsByType 方法会更方便,因为它赋予了所有 @Hints 注解标注的方法直接的访问权限。

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

参考【有修改】:有点干货 | Jdk1.8新特性实战篇