一篇文章彻底理解Java8新特性

Java8特性

目录

  1. FunctionalInterface注解的作用
  2. java.util.function包下的常用接口
  3. 方法引用
  4. Stream的常用方法
  5. LocalDate和LocalDateTime以及LocalTime的使用

1.1 @FunctionalInterface注解

用于添加在接口上的注解。它的作用是:在编译时抛出异常,除非满足下面两种情况:

  • 被注解标注的类型是一个interface类型,并且不是一个annotation类型、enum类型、或class类型
  • 被注解的对象满足方法接口的要求

那么问题来了,方法接口的要求是什么呢?

注释的文档是这样写的

Conceptually, a functional interface has exactly one abstract method.
复制代码

从概念上来讲,一个方法结构只有一个抽象的方法。 欸?只有一个 抽象的 方法?

注意,有两个词被加粗了分别是 一个 和 抽象的

一个 这个量词容易理解,之前也使用过,无非是接口里面就只留下一个方法呗。

但是 抽象的 方法 为什么还要单独拿出来说呢?

是因为在java8之后,接口里面不再只有抽象的方法,还可以添加两种非抽象的方法,他们分别是:default method 和 static method, 默认方法和静态方法。

1.2 默认方法和静态方法

  • 默认方法:提供默认的实现,不能够重写Object的方法,但是却可以重载, 默认方法允许我们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。 default方法可以被重写。
  • 静态方法:就像类中的静态方法一样,可以给出方法的实现,并且这个方法属于方法接口。
class Ca implements Ia {
    @Override
    public void fun3() {
    }
}

@FunctionalInterface
interface Ia {
    default void fun1() {
        System.out.println("fun1 invoked");
    }
    static void fun2() {
        System.out.println("fun2 invoked");
    }
    abstract void fun3();
}

复制代码

就像上面的例子,接口Ia里面有三个方法,同时它使用了@FunctionalInterface注解,此时编译器不会抛异常。因为它满足方法接口的条件,只有一个抽象方法。

class Ca实现方法接口Ia,此时编译器只强制要求我们实现抽象方法fun3(),此例可以证明我们上面说的关于@FunctionalInterface, 默认方法和静态方法的部分特征。

值得一提的是:@FunctionalInterface的作用只体现在编译期,通俗的讲:只要任何一个接口满足了 只有一个抽象接口 这个条件,我们就可以说他是方法接口,与是否标注注解无关,注解的作用只是帮助我们检查。

  1. java.util.function包下的常用接口

    只会介绍典型的几种方法接口, 其他的都是这几种的变形,所以不着重介绍,以下是重点的几个接口:

    1. Function
    2. Consumer
    3. Predicate
    4. Supplier

    2.1 Function接口的使用

    既然是方法接口,那么它肯定满足只有一个抽象方法,那么它的抽象方法是什么呢?打开文档,可以看到它的抽象方法是这样的:

    R apply(T t);
    复制代码

    其中T和R是方法接口声明的泛型类型

    public interface Function<T, R>
    复制代码

    那么apply方法的作用是什么呢,通过参数和返回值类型不难看出,这个方法要求调用者传入一个T类型的参数,然后返回一个R类型的值。T和R在实现接口的时候就已经明确了。

    那么,我们通常怎么使用这个方法接口呢?举一个简单的实例,如果我现在有一个String类型的对象,想要统计这个对象包含‘a’这个字符的个数,应该怎么做呢?

    我们之前的做法通常会写一个方法,把String类型的参数传进来,然后返回一个Integer类型的值,像是这样:

    public static void main(String[] args) {
      String str = "aaaaabcdefg";
      int cnt = countFun(str);
      System.out.println("a 的个数是" + cnt);
    }
    
    public static int countFun(String str) {
      int res = 0;
      int len = str.length();
      for (int i = 0;i < len;i++) {
          if (str.charAt(i) == 'a') {
              ++res;
          }
      }
      return res;
    }
    复制代码

    仔细一观察,发现这个方法似乎可以用方法接口来实现,就像这样:

    public static void main(String[] args) {
      String str = "aaaaabcdefg";
      Function<String, Integer> counter = new Function<String, Integer>() {
          @Override
          public Integer apply(String str) {
              int res = 0;
              int len = str.length();
              for (int i = 0;i < len;i++) {
                  if (str.charAt(i) == 'a') {
                      ++res;
                  }
              }
              return res;
          }
      };
      //        int cnt = countFun(str);
      int cnt = counter.apply(str);
      System.out.println("a 的个数是" + cnt);
    }
    复制代码

    这两种方法会得到的结果无疑是一样的,都是5个‘a’,使用方法接口的方式相较于之前的那种方式,counter仿佛变成了一种行为,这个行为描述了“有一个String类型的对象,想要统计这个对象包含‘a’这个字符的个数”这件事情,似乎增加了代码的可阅读性,但是下面的方式略显臃肿。别着急,其实java给提供了一种语法糖,可以让我们简化这种写法,我们可以这样做:

    public static void main(String[] args) {
      String str = "aaaaabcdefg";
      Function<String, Integer> counter = str1 -> {
          int res = 0;
          int len = str1.length();
          for (int i = 0;i < len;i++) {
              if (str1.charAt(i) == 'a') {
                  ++res;
              }
          }
          return res;
      };
      //        int cnt = countFun(str);
      int cnt = counter.apply(str);
      System.out.println("a 的个数是" + cnt);
    }
    复制代码

    这种方式是java8的新特性之一:使用lambda表达式的形式!

    2.1.1 Java的lambda表达式(此处是参考的菜鸟教程的描述)

    (parameters) -> expression

    (parameters) ->{ statements; }

    • **可选类型声明:**不需要声明参数类型,编译器可以统一识别参数值。
    • **可选的参数圆括号:**一个参数无需定义圆括号,但多个参数需要定义圆括号。
    • **可选的大括号:**如果主体包含了一个语句,就不需要使用大括号。
    • **可选的返回关键字:**如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

    ​ 实例:我们实例化一个Thread的时候通常要实现一个Runnable的子类的对象,而Runnable其实也是一个方法接口,也就是说只有一个抽象的方法run()待实现。其实没必要用匿名内部类的方式,使用lambda的形式会显得更简洁,更能容易被阅读。

    new Thread(() -> {
                System.out.println("这是用lambda形式实现的Runnable方法哦~");
            }).start();
    复制代码

    2.1.2 Function接口的其他方法

    /**
         * Returns a composed function that first applies the {@code before}
         * function to its input, and then applies this function to the result.
         * If evaluation of either function throws an exception, it is relayed to
         * the caller of the composed function.
         *
         * @param <V> the type of input to the {@code before} function, and to the
         *           composed function
         * @param before the function to apply before this function is applied
         * @return a composed function that first applies the {@code before}
         * function and then applies this function
         * @throws NullPointerException if before is null
         *
         * @see #andThen(Function)
         */
        default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
            Objects.requireNonNull(before);
            return (V v) -> apply(before.apply(v));
        }
    
        /**
         * Returns a composed function that first applies this function to
         * its input, and then applies the {@code after} function to the result.
         * If evaluation of either function throws an exception, it is relayed to
         * the caller of the composed function.
         *
         * @param <V> the type of output of the {@code after} function, and of the
         *           composed function
         * @param after the function to apply after this function is applied
         * @return a composed function that first applies this function and then
         * applies the {@code after} function
         * @throws NullPointerException if after is null
         *
         * @see #compose(Function)
         */
        default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return (T t) -> after.apply(apply(t));
        }
    
        /**
         * Returns a function that always returns its input argument.
         *
         * @param <T> the type of the input and output objects to the function
         * @return a function that always returns its input argument
         */
        static <T> Function<T, T> identity() {
            return t -> t;
        }
    复制代码

    有三个方法,分别是:

    • default Function<V, R> compose(Function<? super V, ? extends T> before)
    • default Function<T, V> andThen(Function<? super R, ? extends V> after)
    • static Function<T, T> identity()

    这三个方法有一个共同点,那就是它们的返回值都是Function类型的,用函数式的思想来看,可以把它们理解成返回的某种行为。

    compose 要求传入一个Function参数before,返回一个在调用当前Function的apply之前先调用参数的apply之前的Function。可以看做是这样的一个过程:本来当前的Function会通过调用apply传入一个T类型的参数得到一个R类型的返回值,而compose返回的复合函数的会通过调用apply传入一个V 类型的参数,得到一个R类型的返回值,其中的过程是这样的 T ------》 V -------》 R。假设有以下场景:有一个class Person有一个name属性,现在我们通过Person获取name的长度,可以这样写:

    class Person {
        String name;
    
        public Person(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    public class FunctionTest {
        public static void main(String[] args) {
            Function<String, Integer> function = String::length;
    
            Function<Person, Integer> nameLengthGetter = function.compose(Person::getName);
    
            Person person = new Person("张三");
            System.out.println(nameLengthGetter.apply(person)); // 将会输出person名字的个数
        }
    }
    复制代码

    andThencompose 类似,区别是顺序变了andThen的顺序将会是T ------》 R -------》 V,复合函数最终得到的值将会是在本身的apply的结果之上又进行操作的值。不再详细说明。

    // TODO 其他的方法接口与Function大同小异,暂时不详细说明,进行完下面的部分后再继续这部分

  2. 方法引用

    简单的来说,方法引用,就是借助目前已有的方法,将其作为某个方法接口的实现。这些方法需要具备这样的特征,参数类型或者本身的类型符合方法引用的类型,返回值类型也需要符合方法接口的 R 类型。

    引用方法有下面几种方式

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

    还可以引用构造器,像是这样:ClassName::new, int[]::new

  3. Stream的常用方法

    关于流和其它集合具体的区别:

    1. 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
    2. 函数式编程。流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
    3. 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终止操作才会将操作执行。
    4. 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n)findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
    5. 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。

    流的操作是以管道的方式串起来的。流管道包含一个数据源,接着包含零到N个中间操作,最后以一个终点操作结束。

    创建流的方式

    可以通过多种方式创建流:

    1、通过集合的stream()方法或者parallelStream(),比如Arrays.asList(1,2,3).stream()。 2、通过Arrays.stream(Object[])方法, 比如Arrays.stream(new int[]{1,2,3})。 3、使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier s)Stream.generate(Math::random)。 4、BufferedReader.lines()从文件中获得行的流。 5、Files类的操作路径的方法,如listfindwalk等。 6、随机数流Random.ints()

    中间操作

    Stream提供的方法中有许多方法会返回一个流,并且是延迟执行的,只有遇到终止操作的时候才会执行。

    这些方法有:

    • distinct

      distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。

    • filter

      filter返回的流中只包含满足断言(predicate)的数据。

    • map

      map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。

    • flatmap

      flatmap方法混合了map + flattern的功能,它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:

      <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
      复制代码

      可以看到mapper函数会将每一个元素转换成一个流对象,而flatMap方法返回的流包含的元素为mapper生成的所有流中的元素。

    • peek

      peek方法主要用来做调试用,比如可以这么做

      Stream.of("one", "two", "three", "four")
                    .filter(e -> e.length() > 3)
                    .peek(e -> System.out.println("Filtered value: " + e))
                    .map(String::toUpperCase)
                    .peek(e -> System.out.println("Mapped value: " + e))
                    .collect(Collectors.toList());
      复制代码
    • sorted

      sorted方法将流中的元素排升序,如果元素没有实现Comparable接口,会抛出 java.lang.ClassCastException异常。

      sorted(Comparator comparator)可以指定排序的方式。

    • skip

      skip放弃了流中前n个元素,如果流中元素个数小于或等于n则会返回一个空的流。

    终止操作

    • Math相关

      public boolean 	allMatch(Predicate<? super T> predicate)
      public boolean 	anyMatch(Predicate<? super T> predicate)
      public boolean 	noneMatch(Predicate<? super T> predicate)
      复制代码

      这一组方法用来检查流中的元素是否满足断言。 allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true

      anyMatch只有在任意一个元素满足断言时就返回true,否则flase,

      noneMatch只有在所有的元素都不满足断言时才返回true,否则flase

    • count

      count方法返回此时流中的元素的个数

    • collect

      <R,A> R collect(Collector<? super T,A,R> collector)
      <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
      复制代码

      辅助类Collectors提供了很多的collector,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。

      我们也可以使用第二种重载方法,第一个参数是一个用来构造容器的Supplier,第二个参数是用来把元素整合在一起的累加器,第三个参数是用来合并两个容器的。

    • find

      findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。 findFirst()返回第一个元素,如果流为空,返回空的Optional。

    • forEach

      forEach遍历流的每一个元素, 跟peek用法相似,区别是peek是中间操作,forEach是终止操作

    • max/min

      max返回流中的最大值, min返回流中的最小值。

    • reduce

      reduce是常用的一个方法,常用来做累加操作,事实上很多操作都是基于它实现的。 它有几个重载方法

      pubic Optional<T> 	reduce(BinaryOperator<T> accumulator)
      pubic T 	reduce(T identity, BinaryOperator<T> accumulator)
      pubic <U> U 	reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
      复制代码

      第一个方法使用流中的第一个值作为初始值,后面两个方法则使用一个提供的初始值。

    • toArray

      toArray 将流中的元素放入一个数组。

  4. LocalDate和LocalDateTime以及LocalTime的使用

    java1.8之前的Date类一直被各种诟病,在1.8版本中对这点进行了增强(此处以下文字来源)

    处理日期的 LocalDate

    不同于 Calendar 既能处理日期又能处理时间,java.time 的新式 API 分离开日期和时间,用单独的类进行处理。LocalDate 专注于处理日期相关信息。

    LocalDate 依然是一个不可变类,它关注时间中年月日部分,我们可以通过以下的方法构建和初始化一个 LocalDate 实例:

    • public static LocalDate now():截断当前系统时间的年月日信息并初始化一个实例对象
    • public static LocalDate of(int year, int month, int dayOfMonth):显式指定年月日信息
    • public static LocalDate ofYearDay(int year, int dayOfYear):根据 dayOfYear 可以推出 month 和 dayOfMonth
    • public static LocalDate ofEpochDay(long epochDay):相对于格林零时区时间的日偏移量

    处理时间的 LocalTime

    类似于 LocalDate,LocalTime 专注于时间的处理,它提供小时,分钟,秒,毫微秒的各种处理,我们依然可以通过类似的方式创建一个 LocalTime 实例。

    • public static LocalTime now():根据系统当前时刻获取其中的时间部分内容
    • public static LocalTime of(int hour, int minute):显式传入小时和分钟来构建一个实例对象
    • public static LocalTime of(int hour, int minute, int second):通过传入时分秒构造实例
    • public static LocalTime of(int hour, int minute, int second, int nanoOfSecond):传入时分秒和毫微秒构建一个实例
    • public static LocalTime ofSecondOfDay(long secondOfDay):传入一个长整型数值代表当前日已经过去的秒数
    • public static LocalTime ofNanoOfDay(long nanoOfDay):传入一个长整型代表当前日已经过去的毫微秒数

猜你喜欢

转载自juejin.im/post/5e611496f265da57397e2997