美文网首页
Java8 新特性

Java8 新特性

作者: lynnnnyl | 来源:发表于2022-10-13 10:06 被阅读0次

    一、Lambda表达式

    Lambda表示式也称为闭包,允许把函数作为参数传递进方法中,使用Lambda可以使表达式更简单。

    (parameters) -> expression或者(parameters) -> {statements;}

    重要特性:

    • parameters中的参数类型可以不声明,编译器可以统一识别参数值
    • 定义一个参数时,parameters中的圆括号不写;多个参数时需要用圆括号定义,中间用逗号隔开
    • 箭头后面只有一个语句时,不用写大括号,且不用写return语句,编译器会自动返回值

    Lambda表达式的简单例子:

    //1、不需要参数,返回值为5
    () -> 5
    
    //2、接收一个数字类型的参数,返回其2倍的值
    x -> 2*x
    
    //3、接收2个参数,并返回他们的差
    (x,y) -> x-y
    
    //4、接收2个int型整数,返回他们的和
    (int x, int y) -> x+y
    
    //5、接收一个String类型对象,并在控制台打印
    (String s) -> System.out.println(s)
    
    //6、接收2个int型整数,并在大括号中加上return语句
    (int a,int b) -> {return a+b;}
    

    注意点:

    • Lambda表达式只能引用标记了final的外层局部变量,即不能在lambda表达式内部修改定义在外部的局部变量,否则会报错。
      • 如果外部变量不定义为final,之后如果被重新赋值(引用类型内部状态修改除外),就会出现内部无法看见外部,外部也无法看见内部的问题;Java不支持引用传递,没有nonlocal这样的机制。
    • lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
    • 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

    二、方法引用

    方法引用(method reference)通过使用一对冒号::,通过方法的名字来指向一个方法。

    • 构造器引用:类名::new,或者更一般的Class<T>::new

      Supplier<Student> s1 = Student::new;   //无参构造器
      Function<String, Student> s2 = Student::new;   //一个参数构造器
      BiFunction<String, Integer, Student> s3 = Student::new; //两个参数构造器
      三个参数的构造器需要自定义函数式接口:
          public interface ThreeFunction<T, U, V, R> {
            R apply(T t, U u, V v);
          }
      使用方法:
          ThreeFunction<String, String, Integer, Student> s4 = Student::new;
      
      
    • 静态方法引用:类名::静态方法名

    • 类中普通方法引用:类名::普通方法名

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

    三、函数式接口

    函数式接口:只有一个抽象方法的接口,可以隐式转换为Lambda表达式。Java8提供了@FunctionalInterface注解显式地说明某个接口是函数式接口,用于编译级错误检查,当我们在函数式接口中定义了连个及以上抽象方法时,会报错。但是默认方法(default)和静态方法(static)不会破环函数式接口的定义。例如,如下接口符合函数式接口的定义:

    @FunctionalInterface
    public interface FunctionDefaultMethods {
        void method();
        default defaultMethod();
        static staticMethod();
    }
    

    默认方法 vs. 抽象方法

    区别在于:接口中的抽象方法必须实现,而实现默认方法会被实现类继承或者覆写,如下所示:

    @FunctionalInterface
    private interface Defaulable {
        default String notRequired() {
        return "Default implementation";
        }
    }
    // 实现类DefaultableImpl不覆写,则默认继承notRequired方法。
    private static class DefaultableImpl implements Defaulable {}
    // 实现类OverridableImpl覆写notRequired方法。
    private static class OverridableImpl implements Defaulable {
        @Override
        public String notRequired() {
            return "Overridden implementation";
        }
    }
    

    静态方法允许出现在函数式接口中,是因为它是一个已经实现的方法,符合函数式接口的定义。

    @FunctionalInterface
    private interface DefaulableFactory {
        static Defaulable create(Supplier<Defaulable> supplier) {
            return supplier.get();
        }
    }
    
    Defaulable defaulable = DefaulableFactory.create(DefaultableImpl::new);
    System.out.println(defaulable.notRequired());//Default implementation
    defaulable = DefaulableFactory.create(OverridableImpl::new);
    System.out.println(defaulable.notRequired());//Overridden implementation
    

    由于JVM为默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法可以在不打破现有继承体系的基础上改进接口,例如:java.util.Collection接口添加新默认方法,如stream()parallelStream()removeIf()Iterable接口中实现的forEach()方法等等。尽管默认方法有很多好处,但是在实际开发中应该谨慎使用,因为在复杂的继承体系中,默认方法可能会引起歧义和编译错误。

    四、Optional

    Java8引入的Optional类是为了解决空指针异常(NullPointerException)的问题。

    Optional中可以有值也可能为null,它提供了一些有用的方法来避免显式的null检查。

    empty()---新建一个空的Optional

    Optional<String> emptyOpt = Optional.empty();
    emptyOpt.get();
    

    访问emptyOpt变量的值会导致NoSuchElementException。

    of()与ofNullable()---新建非空的Optional

    两个方法的不同之处在于:将null值作为参数传递进去时,of()方法会抛出NullPointerException,而ofNullable()方法两者都可以处理。

    get()---访问Optional对象的值

    String name = "John";
    Optional<String> opt = Optional.ofNullable(name);
    assertEquals("John", opt.get());
    

    但是,get()方法会在值为null的时候抛出异常,为了避免异常,可以先使用下面的ifPresent()验证是否有值。

    ifPresent()---检查Optional对象是否有值

    该方法除了执行检查,还接受一个Consumer(消费者)参数。如果对象非空才会执行assertEquals断言。

    Optional<User> opt = Optional.ofNullable(user);
    assertTrue(opt.isPresent());
    assertEquals(user.getEmail(), opt.get().getEmail());
    

    orElse()---传入参数非空则返回参数值,否则返回设置的默认值

    User user = null;
    User user2 = new User("anna@gmail.com", "1123");
    User re = Optional.ofNullable(user).orElse(user2);
    
    assertEquals(user2.getEmail(), re.getEmail());
    

    这里user对象是空的,所以返回了作为默认值的user2。

    orElseGet()---传入参数非空则返回参数值,否则执行作为参数传入的Supplier(供应者)函数式接口,并将返回其执行结果

    User re = Optional.ofNullable(user).orElseGet(() -> user2);
    

    orElse()和orElseGet()的不同

    public void test() {
      User user = null;
      User re = Optional.ofNullable(user).orElse(createNewUser());
      User re2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
    }
    
    private User createNewUser() {
      return new User("extra@gamil.com", "1234");
    }
    

    当传入对象为空时,两者的结果时一样的;但是当传入对象为非空时,orElse()方法仍然会创建User对象,但是orElseGet()方法不会创建User对象。

    public void test() {
      User user = new User("john@gmail.com", "1234");
      User re = Optional.ofNullable(user).orElse(createNewUser());
      User re2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
    }
    
    private User createNewUser() {
      return new User("extra@gamil.com", "1234");
    }
    

    在执行较为密集的调用时,比如调用Web服务或数据查询,这个差异会对行能产生重大影响。

    orElseThrow()---对象为空时抛出指定的异常

    User re = Optional.ofNullable(user).orElseTrow(() -> new IllegalArgumentException());
    

    这个方法让我们可以决定抛出什么样的异常,而不总是NullPointerException。

    map()---对值调用作为参数的函数,然后将返回的值包装在Optional中

    flatMap()---对值调用作为参数的函数,然后将返回的值包装在Optional中

    filter()---按条件过滤值,接受一个Predicate参数,返回测试结果为true的值,如果测试结果为false,则返回一个空的Optional

    Java9为Optional类增加了三个方法:or()、ifPresentOrElse()和stream()。or()和orElse()、orElseGet()类似,or()的返回值是由Supplier参数产生的另一个Optional对象。
    ifPresentOrElse() 方法需要两个参数:一个 Consumer 和一个 Runnable。如果对象包含值,会执行 Consumer 的动作,否则运行 Runnable。如果你想在有值的时候执行某个动作,或者只是跟踪是否定义了某个值,那么这个方法非常有用:

    Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
      () -> logger.info("User not found"));
    

    最后介绍的是新的 stream() 方法,它通过把实例转换为 Stream 对象,让你从广大的 Stream API 中受益。如果没有值,它会得到空的 Stream;有值的情况下,Stream 则会包含单一值。

    我们来看一个把 Optional 处理成 Stream 的例子:

    @Test
    public void whenGetStream_thenOk() {
        User user = new User("john@gmail.com", "1234");
        List<String> emails = Optional.ofNullable(user)
          .stream()
          .filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
          .map( u -> u.getEmail())
          .collect(Collectors.toList());
    
        assertTrue(emails.size() == 1);
        assertEquals(emails.get(0), user.getEmail());
    }
    

    Optional 主要用作返回类型。在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。

    Optional 类有一个非常有用的用例,就是将其与流或其它返回 Optional 的方法结合,以构建流畅的API。

    我们来看一个示例,使用 Stream 返回 Optional 对象的 findFirst() 方法:

    @Test
    public void whenEmptyStream_thenReturnDefaultOptional() {
        List<User> users = new ArrayList<>();
        User user = users.stream().findFirst().orElse(new User("default", "1234"));
    
        assertEquals(user.getEmail(), "default");
    }
    

    Optional 是 Java 语言的有益补充 —— 它旨在减少代码中的 NullPointerExceptions,虽然还不能完全消除这些异常。

    它也是精心设计,自然融入 Java 8 函数式支持的功能。

    总的来说,这个简单而强大的类有助于创建简单、可读性更强、比对应程序错误更少的程序。

    五、Stream流式计算

    流Stream将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行筛选、排序、聚合等处理,元素Stream在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

    一个Stream数据输入 + 0个或多个中间操作函数 + 1个最终操作函数。它是一个流水线操作,用来对一系列元素做流水线操作,但不会改变原来输入的数据。

    中间操作(Intermediate Operations)

    filter用来对所给的元素按照某种条件筛选,只留下符合条件的元素;map返回的是一个stream对象,相当于将一个集合的元素通过一个函数进行映射,返回的是映射结果集合组成的stream对象;sorted用来对stream中的元素排序

    最终操作(Terminal Operations)

    collect方法用于手机中间操作的结果,将stream对象还原成原来的数据结构或者转为其它数据结构返回。

    List<Integer> number = Arrays.asList(2,3,4,5);
    Set square = number.stream().map(x->x*x).collect(Collectors.toSet());
    

    forEach方法可以迭代Stream中的所有元素,执行给定的操作(如打印或写数据流)

    reduce 方法用来将stream对象中的元素进行累计操作,最终变成一个值,并将这个值返回。如返回stream所有元素的累加值或者累积值等。

    List<Integer> number = Arrays.asList(2,3,4,5);
    // 先用filter将number中的偶数筛选出来,然后把所有的筛选结果加起来作为返回值。reduce的一个参数是累计的初始值,第二个参数指定累计的操作。
    int even = number.stream().filter(x->x%2==0).reduce(0,(ans,i)-> ans+i);
    

    这里ans变量初始值为0i 表示number中的元素,将number中的所有元素与ans相加并返回ans

    查找与匹配

    短路操作符:allMatch anyMatch noneMatch findFirst findAny limit skip

    anyMatch:流中是否有一个元素能匹配给定的谓词

    allMatch:看看流中的元素是否都能匹配给定的谓词

    noneMatch:以确保流中没有任何元素与给定的谓词匹配

    findAny:将返回当前流中的任意元素,可以与其他流操作结合使用

    findFirst:返回第一个元素

    findAny VS. findFirst:

    • 两者通常与Optional类一起使用(可能返回空)
    • 对于顺序流式处理,findFirst和findAny返回的结果一样
    • findFirst在并行上限制较多,如果不关心返回的元素,可以使用findAny

    limit(n):取前n个元素

    skip(n):跳过前n个元素

    归约---reduce

    Lambda反复调用每个元素,直到流被归约成一个值,这样的查询可以被归类为归约操作(将流归约为一个值)

    用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操 作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

    eg:求和:int sum = nums.stream().reduce(0, (a, b) -> a+b);//0作为第一个参数(a)的初始值,nums中的第一个元素作为第二个参数(b)的初始值,更简洁的写法:

    int sum = nums.stream().reduce(0, Integer::sum);

    无初始值

    reduce可以不传初始值,但是会返回一个Optional对象:

    Optional<Integer> sum = nums.stream().reduce((a, b) -> a + b));

    为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。

    最大值和最小值:

    Optional<Integer> max = nums.stream().reduce(Integer::max);

    Optional<Integer> min = nums.stream().reduce(Integer::min);

    Map-Reduce模型

    map与reduce的连接通常称为map-reduce模式,因为其很容易并行化,Google用它进行网络搜索。

    eg:用map和reduce统计菜单中一共有多少个菜?

    int cnt = menu.stream().map(d -> 1).reduce(0, (a, b) -> a+b);

    int cnt = menu.stream().count();

    归约方法的优势与并行化

    相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉 了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum, 这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升! 但现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。

    原始类型流转化

    1、映射到数值流

    • mapToInt

    • mapToDouble

    • mapToLong

      上述三个原始类型流接口可以将流中的元素转化为int、double和long,从而避免了暗含的装箱成本。eg:下面两段代码,第一段代码有装箱的成本,第二段代码直接对原始类型进行求和。

      int calories = menu.stream()
                       .map(Dish::getCalories)   //Stream<Integer>
                           .reduce(0, Integer::sum);
      
      int calories = menu.stream()
                       .mapToInt(Dish::getCalories) //IntStream
                       .sum(); //IntStream还支持max、min、average
      

      2、转换回对象流

      IntStream上的操作只能产生原始整数,并且IntStream 的 map 操作接受的 Lambda 必须接受 int 并返回 int (一个 IntUnaryOperator)。把原始流转换成为一般流,可以用boxed方法,eg:

      IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //Stream流转换为数值流
      Stream<Integer> stream = intStream.boxed();// 数值流转换为Stream流
      

      3、默认值Optional类

      在求IntStream的最大值时,如果元素为空返回值默认为0,如何区分没有元素的流和最大值就是0的流?Optional类提供了:OptionalInt、OptionalDouble和OptionalLong三种原始类型特化版本。eg:

      OptionalInt maxCalories = menu.stream()
                                  .mapToInt(Dish::getCalories)
                                  .max();
      // 如果流为空的话,可以显式处理OptionalInt去定义一个默认值
      int max = maxCalories.orElse(1); // 如果没有最大值,显式提供一个默认最大值
      

      4、数值范围

      Java8引入了静态方法range和rangeClosed,可以用于IntStream和LongStream,这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range是不包含结束值的,而rangeClosed则包含结束值。eg:

      IntStream evenNums = IntStream.rangeClosed(1, 100)  // 表示范围:(1,100]
                                  .filter(n -> n%2 == 0);
      System.out.println(evenNums.count());   // 统计偶数个数
      IntStream evenN = IntStream.range(1, 100)    // 表示范围:(1,100)
                               .filter(n -> n%3 == 0);
      

      5、创建流的方式
      1、由值创建流
      可以使用静态方法Stream.of,通过显式值创建一个流,它可以接受任意数量的参数。eg:

    Stream<String> stream = Stream.of("Java 8", "Lambda", "In", "Action");
    stream.map(String::toUpperCase).forEach(System.out::println);
    

    也可以用empty得到一个空流

    Stream<String> stream = Stream.empty();
    

    2、由数组创建流
    静态方法Arrays.stream表示从数组创建一个流,它接受一个数组作为参数,eg:将一个原始类型int的数组转换成一个IntStream:

    int[] nums = {1, 2, 3, 4};
    int sum = Arrays.stream(numbers).sum(); 
    

    3、由文件生成流

    Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。



    你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用split方法将行拆分成单词。应该注意的是,你该如何使用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinct和count方法链接起来,数数流中有多少各不相同的单词。

    4、由函数生成流:创建无限流

    Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

    Stream.iterate(0, n -> n+2)
          .limit(10)
          .forEach(System.out::println);
    

    请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。一般来说,在需要依次生成一系列值的时候应该使用iterate,比如一系列日期:1月31日,2月1日,依此类推。

    与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。

    Stream.generate(Math::random)
          .limit(5)
          .forEach(System.out::println);
    

    Stream的懒加载机制(Lazy)

            List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");
            final String firstNameWith3Letters = names.stream()
                .filter(name -> length(name) == 3)
                .map(name -> toUpper(name))
                .findFirst()
                .get();
    

    从经典Java Eager的角度下考虑,首先会遍历集合得到名字长度为3的所有名字(filter),然后再遍历filter之后的集合,将名字转换成大写(map),最后再从大写名字的集合中找到第一个(findFirst)并返回。然而,stream的Lazy机制的执行过程如下:只有当findFirst()方法被调用时。filter和map方法才会被真正触发,而filter也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map方法中。当终结操作获得了它需要的元素,整个计算过程结束;如果没有获得答案,那么它会要求中间操作对更多的集合元素进行计算,直到找到答案或者整个集合被处理完毕。

    对于Stream操作,更好的代码阅读顺序是从右到左,或者从下到上。Stream每一个操作都只会做到恰到好处。
    控制台的输出是这样的:

    getting length for Brad
    getting length for Kate
    getting length for Kim
    converting to uppercase: Kim
    KIM
    

    JDK会将所有的中间操作合并成一个,这个过程被称为熔断操作(Fusing Operation)。因此,在最坏的情况下(即集合中没有符合要求的元素),集合也只会被遍历一次,而不会像我们想象的那样执行了多次遍历,也许这就回答了官方文档中为什么说"Processing streams lazily allows for significant efficiencies"了。如果只声明Stream对象的中间操作,没有最终操作,这些中间操作并不会执行

    流操作:有状态和无状态

    判断有无状态的标准:是否需要知道先前的数据历史,前后数据是否有依赖关系

    map、filter等操作从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作不需要知道上一个元素的状态,只关注当前元素,没有内部状态(假设使用的Lambda或者方法引用没有内部可变状态),称为无状态操作

    但诸如reduce、sum、max等操作需要内部状态来累积结果。求最大值、最小值时,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。sortdistinctlimit、skip等操作一开始都和filter和map差不多:都是接受一个 流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操 作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么 呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作

    [图片上传失败...(image-6dfd0a-1665648613817)]

    相关文章

      网友评论

          本文标题:Java8 新特性

          本文链接:https://www.haomeiwen.com/subject/cfpfzrtx.html