Java 8

  • Java 8 是 Java 语言开发的一个主要版本。是 Oracle 公司于 2014.3 发布,可以看成是自 Java 5 以来最具革命性的版本。java 8 为 Java 语言、编译器、类库、开发工具与 JVM 带来了大量新特性。

特性

  • 速度更快
  • 代码更少(新语法:Lambda 表达式)
  • 强大的 Stream API
  • 便于并行
  • 最大化减少空指针异常:Optional
  • Nashorn 引擎,允许在 JVM 上运行 JS 应用

Lambda 表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码,(将代码像数据一样进行传递)。使用他可以写出更简洁、更灵活的代码、作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升(个人感觉和ES6箭头函数一样)

  • 简单感受一下 Lambda 表达式

  • 测试用法:无参的

  • @Test
    public void test1(){
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Not Lambda");
            }
        };
        r1.run();
    	//---------------------------------------------
        Runnable r2 = () -> System.out.println("Lambda");
        r2.run();
    }
    
  • 测试用法:有参的

  • @Test
    public void test2(){
        Comparator<Integer> com1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return Integer.compare(o1,o2);
            }
        };
        com1.compare(10,20);
    
        Comparator<Integer> com2 = (o1,o2)->Integer.compare(o1,o2);
        com2.compare(10,20);
        
        //方法引用
        Comparator<Integer> com3 = Integer::compare;
        com3.compare(10,20);
    
    }
    
  • 解释一下上面的例子 (o1,o2)->Integer.compare(o1,o2);

    • ->:lambda 操作符或箭头操作符
    • 左边:lambda 形参列表(其实就是接口中的抽象方法的形参列表)
    • 右边:lambda 体(其实就是重写的抽象方法的方法体)

需要注意的是,在其他地方 lambda 表达式的本质可能是函数,但是在 java 中 lambda 表达式的本质其实是接口的实例(再一次体现万物皆对象)

  • 总结:对一个函数式接口实例化的时候可以使用 Lambda 表达式,所以说 lambda 表达式是函数式接口的实 例

六种使用形式

根据不同的情况,Lambda 表达式的使用 也不同

无参,无返回值:
  • 其实上面的第一个例子就是,无参左边就空的括号,右边直接方法体
  • Runnable r2 = () -> System.out.println("Lambda");
  • 其实本来方法体外面都有{},但是这就一行代码,所以可以省略,本来应该是
  • Runnable r2 = () -> { System.out.println("Lambda");}
有参,无返回值
  • 以 Consumer 为例

  • @Test
    public void test1(){
        Consumer<String> con1 = new Consumer<String>(){
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con1.accept("con1");
    	//------------------------
        Consumer<String> con2 = (String s)-> System.out.println(s);
        con2.accept("con2");
    }
    
  • 如上代码上面是普通的实现接口,重写方法,而下面只需要一行就能搞定,从这里其实也能看出 Lambda 表达式本质其实就是接口的实例,就像你创建了一个叫 Jack 的人类,这里是创建了一个 accept 方法为输出传参的 Consumer 类

数据类型可以省略
  • 上面的代码改进一下:
  • Consumer<String> con2 = (s)-> System.out.println(s);
  • 为啥可以省略,因为编译器会类型推断,比如 List<String> list = new ArrayList<>();,你前面都写了是 String 的 List 了,我后面就不用写了。就像 Consumer 你都写了是 String 类型的了,所以我可以省略
若只需要一个参数,括号可以省略
  • 上面的代码再再改进一下
  • Consumer<String> con2 = s-> System.out.println(s);
  • 就一个参数确实没必要加括号,换我我也这么设计
两个以上参数,多条执行语句,有返回值
  • 其实也就是参数的括号和方法体的花括号不能省略,直接拿上面的例子加个语句

  • Comparator<Integer> com2 = (o1,o2)->{
        System.out.println(o1);
        return Integer.compare(o1,o2);
    }
    
Lambda 体只有一条执行语句
  • 可以省略方法体的花括号,上面早就省略过了
总结

左边:类型可以省略,参数列表只有一个可以省略小括号否则不省

右边:Lambda 体只有一条执行代码可以省略大括号,如果 return xx,如果省略了大括号还得把 return 省了

  • 上面的例子都有一个特点,实现的接口都只有一个函数,不然你用 Lambda 重写一个方法谁知道你重写了哪个方法,这就是函数式接口

函数式(Functional)接口

如果一个接口只声明了一个抽象方法,就是函数式接口。上面一般有 @FunctionalInterface 注解,但是不加也没事,加了也就是一个校验,比如你声明了两个方法就会报错,就像 @Override ,你不写这个但是你重写了方法你也还是重写

  • 可以通过 Lambda 表达式来创建该接口对象,所以 Lambda 必须依赖于函数式接口
  • 可以在接口上使用 @FunctionalInterface 注解没这样做可以检查他是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个函数是一个函数式接口
  • 在 java.util.function 包下定义了 Java8 丰富的函数式接口
Java 内置四大核心函数式接口
  • Consumer<T>:消费型,接收参数但无返回值,方法为 void accept(T t)

  • Supplier<T>:供给型,不接受参数但有返回值,方法为 T get()

  • Function<T,R>:函数型,接收参数返回结果,方法为 R apply(T t)

  • Predicate<T>:断定型,判断 T 类型对象是否满足某种约束并返回 boolean 值,方法为 boolean test()

  • 玩一下断定型,或者叫断言型

  • @Test
    public void test2(){
        List<String> list = Arrays.asList("小明","小光","小刚","大熊");
        //不用 lambda 
        List<String> list1 = filterList(list, new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.contains("小");
            }
        });
        //用 lambda
        List<String> list2 = filterList(list, s -> s.contains("小"));
    }
    
    public List<String> filterList(List<String> list, Predicate<String> pre){
        List<String> filter = new ArrayList<>();
        list.forEach(str->{
            if(pre.test(str)){
                filter.add(str);
            }
        });
        return filter;
    }
    
  • 还会有一些 BiXXX的函数式接口,就是基于四种核心函数式接口的,只是传参可以为多个,还有一些子接口比如 Function 的传参和返回类型可以不一致,而他的子接口是传参和返回类型一致

方法引用与构造器引用

方法引用

当要传递给 Lambda 体的操作,已经有了实现的方法,就可以使用方法引用

  • 方法引用可以看做是 Lambda 表达式深层次的的表达,换句话说,方法引用也是 Lambda 表达式,所以肯定是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是 Lambda 表达式的一个语法糖

  • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(比如两个方法的传参都是泛型且无返回值)

  • 格式:使用操作符:: 将类(或对象)与方法名隔开

  • 如下三种主要使用情况

对象::实例方法名
  • //Consumer 中的 void accept(T t)
    // PrintStream 中的 void println(T t)
    @Test
    public void test3(){
        Consumer<String> con = s -> System.out.println(s);
        con.accept("无");
    }
    
  • 上面不就是我们说的情境,Lambda 操作符右边的不就是已经实现的函数式接口的一个实例,而我们说的要求不也满足了,accpet 和 println 传参返回值一致,所以可以写成如下

  • PrintStream ps = System.out;
    Consumer<String> con = ps::println;
    
  • 其实这也是强行 对象::实例方法名,可以用下面这种的

  • Consumer<String> con = System.out::println;

  • 这里可以直接省略传参 s,那就可以看出为什么有上面的要求,因为如果你 accept 有两个传参,println 怎么知道输出哪个

类::静态方法名
  • @Test
    public void test4(){
        // Comparator 的 compare(T t1,T t2)
        Comparator<Integer> com = (t1,t2)->Integer.compare(t1,t2);
        // Integer 的 compare(T t1,T t2)
        Comparator<Integer> com1 = Integer::compare;
    }
    
  • 符合要求和使用情境,所以可以用方法引用

类::实例方法名(理解有难度)
  • 简单看一个例子

  • Comparator<String> com = (s1,s2) -> s1.compareTo(s2);
    com.compare("abc","abd");
    
  • 实现的方法为 Comparator 的 int compare(T t1,T t2)

  • 但是右边的 Lambda 体用的方法为 String 的 int s1.compareTo(s2)

  • 参照上面的例子,这里虽然返回值都是 int ,传参类型也可以都是 String,但是传参个数好像不太一致,一个是两个参数,一个是对象使用方法传一个参数,但是也可以对象引用

  • Comparator<String> com = String::compareTo;

  • 我的理解。其实就是不管你什么形式,你用到的还是两个参数,本身 compare 也就是默认第一个传参和第二个传参比,compareTo 不也是第一个用方法的参数和第二个传进去的参数比,顺序个数其实也还是一致的,也就是上面的要求其实就是为了保证返回值以及传参的类型,还有传参个数和顺序的一致(个人理解仅供参考

  • 理解了上面这个那再来一个,比如我有一个 Employee 员工类,有 name 属性

  • Function<Employee,String> func = e->e.getName();
    
    Function<Employee,String> func = Employee::getName;
    
  • 这里一个是 Function 的 R apply(T t),一个是 Emmplyee 的 String getName(),看起来好像更加不符合要求,但是其实对应到这个例子还是一样。 String apply(Employee) 对应了 String Employee.getName(),返回类型都是 String,传参都是 Employee 类,只有一个传参也不存在顺序

  • 所以还是可以写成 Function<Employee,String> func = Employee::getName;

  • 总结:写 Lambda 表达式的时候如果和 Lambda 体的方法结构相同(传参和返回类型相同),那可以直接把 Lambda 表达式换成方法引用

构造器引用和数组引用

构造器引用
  • 其实理解了方法引用就不难理解这个,和方法引用同理,举个例子

  • //最原始
    Supplier<Employee> sup = new Supplier<>(){
        @Override
        public Employee get() {
            return new Employee();
        }
    }
    //lambda 表达式
    Supplier<Employee> sup = ()->new Employee();
    //构造器引用
    Supplier<Employee> sup2 = Employee::new;
    
  • 这里一个是 Supplier 的 T get(),一个是 Employee 的 Employee ()

  • 对应到这里就是返回值都是 Employee,传参都是空参,所以可以使用

数组引用
  • @Test
    public void test6(){
        Function<Integer,String[]> func1 = length -> new String[length];
        String[] apply1 = func1.apply(5);
        System.out.println(Arrays.toString(apply1));
    
        Function<Integer,String[]> func2 = String[]::new;
        String[] apply2 = func1.apply(5);
        System.out.println(Arrays.toString(apply2));
    }
    
  • 其实和构造器也一样,只不过把 String[] 看成整体,是调用数组的构造器

Stream API

Java8 中有两个最为重要的改变,第一个是 Lambda 表达式,另一个就是 Stream API

  • 使用 Stream API 可以对集合进行查找、过滤、映射等操作,类似于使用 SQL 执行数据库的查询
  • 现在的数据源还有类似 MongoDB、Redis 这种 NoSQL 的数据就需要 Java 层来处理
  • Stream 和 Collection 的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者主要面向内存,存储在内存中,后者主要面向 CPU,通过 CPU 实现计算

注意

  1. Stream 自己不会存储数据
  2. Stream 不会改变源对象,相反,会返回一个一个持有新结果的 Stream
  3. Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行

操作的三个步骤

  1. 创建 Stream:一个数据源(如:集合、数组),获取一个流
  2. 中间操作:一个中间操作链,对数据源的数据进行处理,比如数据映射,过滤
  3. 终止操作:一旦执行终止操作就执行中间操作链并产生结果,之后不会再被使用
    • 这也就是为什么说 Stream 操作是延时的,因为不执行终止操作你中间的一堆操作都不会执行

Stream 的实例化

通过集合
  • List<String> list = Arrays.asList("aa","bb","cc");
  • 返回顺序流:Stream<String> stream = list.stream();
  • 返回并行流:Stream<String> stream2 = list.parallelStream():
通过数组
  • String[] array = new String[]{"aaa","bbb","ccc"};
    Stream<String> stream1 = Arrays.stream(array);
    
通过 Stream.of
  • Stream<Integer> integerStream = Stream.of(1, 2, 3);
    
创建无限流
  • //生成前十个偶数
    //迭代
    Stream.iterate(0,s->s+2).limit(10).forEach(System.out::println);
    
  • //生成十个随机数
    //生成
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
    

Stream 的中间操作

筛选与切片
  • filter(Predicate p):接收 Lambda,从流中排除某些元素

    • //筛选偶数
      List<Integer> list = Arrays.asList(1, 2, 3, 4);
      Stream<Integer> stream = list.stream();
      stream.filter(i -> i % 2 == 0).forEach(System.out::println);
      
      //实际可以合并起来,因为执行终止操作后 Stream 流就失效了,得重新创建,所以还不如每次都直接新建然后使用也就是 list.stream().xxx
      list.stream().filter(i -> i % 2 == 0).forEach(System.out::println);//=>2,4
      
  • distinct:筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素

    • //去掉重复的元素
      List<Integer> list2 = Arrays.asList(1, 2, 2, 3);
      list2.stream().distinct().forEach(System.out::println); //=>1,2,3
      
  • limit(long maxsize):截断流,使其元素不超过给定数量

    • //输出集合前两个元素
      list.stream().limit(2).forEach(System.out::println);//=>1,2
      
  • skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个,则返回空流,与 limit 互补

    • //输出跳过前两个元素的集合
      list.stream().skip(2).forEach(System.out::println);//=>3,4
      
映射
  • map:接收一个函数作为参数,该函数被应用到每个元素上,并将其映射为一个新的元素

    • //所有元素乘以2
      List<Integer> list = Arrays.asList(1, 2, 3);
      list.stream().map(i->i*2).forEach(System.out::println);//=>2,4,6
      
  • mapToDouble:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 DoubleStream

  • mapToInt:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 IntStream

  • mapToLong:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 LongStream

  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

    • 他和 map 就像 List 的 addAll 和 add,比如有一个返回字符流的方法

    • public static Stream<Character> stringToCharacter(String s){
          char[] chars = s.toCharArray();
          ArrayList ary = new ArrayList();
          for (Character c:chars){
              ary.add(c);
          }
          return ary.stream();
      }
      
    • 如果使用 map 输出一个字符串数组的所有字符

    • List<String> list = Arrays.asList("aa","bb");
      Stream<Stream<Character>> mapStream = list.stream().map(StreamTest::stringToCharacter);
      mapStream.forEach(s->s.forEach(System.out::println));
      
    • 因为流中是 {{a,a},{b,b}},所以每个元素还需要各自循环输出

    • 而如果使用 flatMap

    • Stream<Character> flatStream = list.stream().flatMap(StreamTest::stringToCharacter);
      flatStream.forEach(System.out::println);
      
    • 他在映射的时候会把每个流合并成一个流,也就是把 {a,a} 和 {b,b} 自动合并成了 {a,a,b,b} 所以直接循环输出即可

排序
  • sorted():自然排序

    • List<Integer> list = Arrays.asList(12, 43, 65, 34);
      list.stream().sorted().forEach(System.out::println);
      
    • 默认会从小到大排序

    • 但是如果换成一个实体类比如 Employee 的排序,就会报错,因为 Employee 未实现 Comparable 接口,那你要么实现接口,要么用下面一种,临时定制规则

  • sorted(Comparator com):定制排序

    • 假设有一个 User 类

    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
          private long id;
          private int age;
      
          public static List<User> getUsers(){
              List<User> users = new ArrayList<>();
              for (int i = 0; i < 5; i++) {
                  char a = 'a';
                  User user = new User(i, i * 10);
                  users.add(user);
              }
              return users;
          }
      }
      
    • 如果要实现 Comparable 接口就要写成 public class User implements Comparable<User>,然后重写 compareTo 方法,而定制排序就可以暂时定制规则,例如按照年龄从大到小排列

    • List<User> list = User.getUsers();
              list.stream().sorted((u1,u2)->-Integer.compare(u1.getAge(),u2.getAge())).forEach(System.out::println);
      
    • 如果是从小到大排序还能简化如下

    • list.stream().sorted(Comparator.comparingInt(User::getAge)).forEach(System.out::println);
      

Stream 的终止操作

匹配与查找
  • allMatch(Predicate p):检查是否匹配所有元素
  • anyMatch(Predicate p):检查是否至少匹配一个元素
  • noneMatch(Predicate p):检查是否没有匹配的元素
  • findFirst:返回第一个元素
  • findAny:返回当前流中的任意元素
  • count:返回流中元素的总个数
  • max(Consumer c):返回流中最大值
  • min(Consumer c):返回流中最小值
  • forEach(Consumer c):内部迭代
    • 集合的 forEach 是外部迭代,可以理解为外部有个指针然后指向一个个迭代的数据,而内部迭代就是内部的数据自动取到下一个下一个的。
归约
  • reduce(T identity,BinaryOperator):可以将流中的元素反复结合起来,得到一个值,返回一个 T

    • 例如计算 1-10 的自然数的和

    • List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      Integer sum = list.stream().reduce(0, Integer::sum);
      System.out.println(sum);
      
    • 0 代表初始值,从 0 开始先执行 sum,也就是 0+1,然后以 0+1 也就是 1 为起点执行 sum,也就是 1+2,然后以 1+2 也就是 3 为起点执行 sum,也就是 3+4 …

  • reduce(BinaryOperator):可以将流中的元素反复结合起来,得到一个值,返回一个 optional<T>

    • 就是没有初始值的用法
收集
  • collect(Collector c):将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 元素做汇总

  • 这里的 Collector 一般用 Collectors 提供实例:

    • toList:把流中的元素收集到 List
    • toSet:把流中的元素收集到 Set
    • toCollection:把流中的元素收集到创建的集合
    • counting:计算流中元素的个数
    • summingInt:对流中元素的整数属性求和
    • averagingInt:计算流中元素 Integer 属性的平均值
    • summarizingInt:收集流中 Integer 属性的统计值。如:平均值
  • 示例:

    • List<User> users = User.getUsers();
      List<User> list = users.stream().filter(user -> user.getAge() > 20).collect(Collectors.toList());
      

Optional 类

Optional 类是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。它可以避免空指针异常。Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法会返回该对象。

创建 Optional 类对象的方法:

  • Optional.of(T t):创建一个 Optional 实例,t 必须非空
  • Optional.empty():创建一个空的 Optional 实例
  • Optional.ofNullable(T t):t 可以为 null

判断 Optional 容器中是否包含对象:

  • boolean isPresent():判断是否包含对象

  • void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer接口的实现代码,并且该值会作为参数传给它

获取 Optional 容器的对象:

  • T get():如果调用对象包含值,返回该值,否则抛异常
  • T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象(默认值的感觉)
  • T orElseGet(Supplier<? extends T> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常

测试

项目常见情况
  • 例如有两个类如下

  • @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Girl {
        private String name;
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Boy {
        private Girl girl;
    }
    
  • 此时我测试如下

  • public String getGirlName(Boy boy){
        return boy.getGirl().getName();
    }
    
    @Test
    public void test3(){
        Boy boy = new Boy();
        String girlName = getGirlName(boy);
        System.out.println(girlName);
    }
    
  • 此时会报空指针,虽然 Boy 不为空,但是他的 girl 字段为空所以无法获取到 name

  • 那么我们自己优化方法会用几个嵌套的 if 来判断是否为空

  • 但是使用 Optional 就可以很好地如下优化:

  • public String getGirlName2(Boy boy){
        //先创建 Boy 的 Optional
        Optional<Boy> boyOptional = Optional.ofNullable(boy);
        //Boy 为空就给个默认值,否则为原值
        Boy boy1 = boyOptional.orElse(new Boy(new Girl("aaa")));
        //此时 Boy 肯定不为空,就创建 Girl 的 Optional
        Optional<Girl> girlOptional = Optional.ofNullable(boy1.getGirl());
        //如果 Girl 为空就给个默认值,否则为原值
        Girl girl = girlOptional.orElse(new Girl("bbb"));
        //此时 Girl 是不可能为空了的,直接返回 girl 的名字就好
        return girl.getName();
    }
    
  • 总结,确定有值的用 of 和 get,不确定就 ofNullable 和 orElse,把 Optional 看成容器就行,前面的方法都是存取

接口的增强

静态方法

默认方法

新的时间和日期 API

其他新特性

重复注解

类型注解

通用目标类型推断

JDK 的更新

集合的流式操作
并发
Arrays
Number 和 Math
IO/NIO 的改进
Reflection 获取形参名
String:join()
Files

新编译工具:jjs、jdeps

JVM 中 Metaspace 取代 PermGen 空间

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐