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, 默认方法和静态方法的部分特征。
【Java8的特性复习】值得一提的是:@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

    那么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 counter = new Function() { @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 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 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 Function compose(Function 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 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 Function andThen(Function after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); }/** * Returns a function that always returns its input argument. * * @param the type of the input and output objects to the function * @return a function that always returns its input argument */ static Function identity() { return t -> t; }

    有三个方法,分别是:
    • default Function compose(Function before)
    • default Function andThen(Function after)
    • static Function 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 function = String::length; Function 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的功能,它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:
      Stream flatMap(Function> 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相关

      这一组方法用来检查流中的元素是否满足断言。
      allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
      anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
      noneMatch只有在所有的元素都不满足断言时才返回true,否则flase
    • count
      count方法返回此时流中的元素的个数
    • collect
      R collect(Collector collector) R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

      辅助类Collectors提供了很多的collector,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。
      我们也可以使用第二种重载方法,第一个参数是一个用来构造容器的Supplier,第二个参数是用来把元素整合在一起的累加器,第三个参数是用来合并两个容器的。
    • find
      findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
      findFirst()返回第一个元素,如果流为空,返回空的Optional。
    • forEach
      forEach遍历流的每一个元素, 跟peek用法相似,区别是peek是中间操作,forEach是终止操作
    • max/min
      max返回流中的最大值,
      min返回流中的最小值。
    • reduce
      reduce是常用的一个方法,常用来做累加操作,事实上很多操作都是基于它实现的。
      它有几个重载方法

      第一个方法使用流中的第一个值作为初始值,后面两个方法则使用一个提供的初始值。
    • 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):传入一个长整型代表当前日已经过去的毫微秒数

    推荐阅读