zoukankan      html  css  js  c++  java
  • Java 新特性总结——简单实用

    lambda 表达式

    简介

    在我看来 lambda 表达式就是简化了以前的一大堆繁琐的操作,让我们代码看起来更加简洁,让以前五六行甚至更多行的代码只需要两三行就能解决,但是对于 Java 初学者可能不是特别友好,可能一下子理解不过来该代码想表达什么。

    lambda 表达式是一段可以传递的代码,因此它可以被执行一次或多次

    lambda 表达式的语法

    我们先来看看老版本的排序字符串的办法,这里我们不按照字典序,而按照字符串的大小来排序

    Comparator<String> comparator = new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return Integer.compare(o1.length(),o2.length());
        }
    };
    List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
    Collections.sort(list,comparator);// [a, aa, aaa, aaaa, aaaaa]
    

    老版本的排序我们会先创建一个自定义比较器,然后按照比较器的规则进行排序。

    现在我们来看看 lambda 表达式如何来实现的

    List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
    Collections.sort(list,(String o1,String o2)->{
        return Integer.compare(o1.length(),o2.length());
    });//[a, aa, aaa, aaaa, aaaaa]
    

    可以看到,代码浓缩了不少,但是可读性没有原来好,原来需要先创建一个比较器然后将比较器传到 Collections 工具类进行排序,典型的面向对象编程,但是 lambda 表达式确是将代码传进去然后直接进行比较。如果你以为这样就是最简便的,那你就错了,有更简便的。

    List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
    Collections.sort(list,(String o1,String o2)
                     ->Integer.compare(o1.length(),o2.length()));
    

    如果返回值只有一行,可以省略大括号和 return 关键字。

    List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
    Collections.sort(list,(o1,o2)->Integer.compare(o1.length(),o2.length()));
    

    如果是带泛型的容器,参数的类型可以省略,JVM 会自己进行上下文判断出类型。

    我们可以看到从原来的那么多行代码浓缩成了一行,看着清爽了很多,但是可读性却没有原来那么友好了。

    变量作用域

    • 访问局部变量

      可以在 lambda 表达式中访问外部的局部变量

      int number = 10;
      Converter<String,Integer> converter = num->Integer.valueOf(num + number);
      System.out.println(converter.convert("123"));//12310
      

      在匿名内部类中外部的局部变量必须声明为 final。而我们这里不需要。

      但是要注意的是这的 number 不能被后面的代码修改,否则编译不通过,也就是具有隐性的 final 语义。

    • 访问 字段和静态变量

      我们对 lambda 表达式中的实例字段和静态字段变量都有读写访问权限。

      class Lambda4 {
          static int outerStaticNum;
          int outerNum;
      
          void testScopes() {
              Converter<Integer, String> stringConverter1 = (from) -> {
                  outerNum = 23;
                  return String.valueOf(from);
              };
      
              Converter<Integer, String> stringConverter2 = (from) -> {
                  outerStaticNum = 72;
                  return String.valueOf(from);
              };
          }
      }
      
    • 无法在 lambda 表达式中访问默认接口方法。

    函数式接口

    在 Java 中有许多已有的接口都选哦封装成代码块,比如 Runnable 或者 Comparator 。 lambda 表达式与这些接口是像后兼容的。

    对于只包含一个抽象方法的接口,但是可以有多个非抽象方法,(非抽象方法也是 java 8 新特性,我们后面会讲到),我们可以通过 lambda 表达式来创建该接口的对象。这种接口被称为函数式接口。

    Java 8 新增加了一种特殊的注解 @FunctionalInterface,该接口会自动判断你的接口中是否只有一个抽象方法,如果多于一个抽象方法就会报错。

    现在我们来自定义一个函数式接口:

    @FunctionalInterface
    interface Converter<F,T>{
        T convert(F num);
    }
    
    // 将数字形式的字符串转化成整型
    Converter<String,Integer> converter = (num -> Integer.valueOf(num));
    System.out.println(converter.convert("123").getClass());//class java.lang.Integer
    

    现在来解释下该代码,在该代码中我们的函数式接口中定义了一个方法,该方法能够实现传入一个 F 类型的参数,我们可以对这个类型的参数进行各种处理,最后返回一个 T 类型的结果。在这里我只是简单的将传进来的 string 转成了 integer。这里的 F 与 T 都是泛型类型,可以为任何实体类。

    java 8 帮我们实现了很多函数式接口,大部分都不需要我们自己写,这些接口在 java.util.function 包 里,可以自行进行查阅。

    上面的代码可以写的更加简单:

    Converter<String,Integer> converter = Integer::valueOf;
    System.out.println(converter.convert("123").getClass());//class java.lang.Integer
    

    java 8 可以通过 ** : : **来传递方法或者构造函数的引用。上面的演示了如果引用静态方法,引用对象方法也相差不大,只是需要声明一个对象:

    class Demo{
        public Integer demo(String num){
            return Integer.valueOf(num);
        }
    }
    public class Main {
        public static void main(String[] args) {
            Demo demo = new Demo();
            Converter<String,Integer> converter = demo::demo;
            System.out.println(converter.convert("123").getClass());
            //class java.lang.Integer
        }
    }
    

    内置函数式接口

    • Predicates

      Predicate 接口是只有一个参数的返回布尔类型值的 断言型 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)

      @FunctionalInterface
      public interface Predicate<T> {
          // 该方法是接受一个传入类型,返回一个布尔值.此方法应用于判断.
          boolean test(T t);
          ....
      }
      
    • Functions

      Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen)

      @FunctionalInterface
      public interface Function<T, R> {
          
          //将Function对象应用到输入的参数上,然后返回计算结果。
          R apply(T t);
          ...
      }
      
    • Suppliers

      Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。

    • Consumers

      Consumer 接口表示要对单个输入参数执行的操作。

    • Comparators

      Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法

    默认方法

    前面已经有写地方提到了接口的默认方法,这里对其做下介绍。接口的默认方法也是 java 8 新出的功能。能够通过使用 default 关键字向接口添加非抽象方法实现。

    interface Formula{
    
        double calculate(int a);
    
        default double sqrt(int a) {
            return Math.sqrt(a);
        }
    
    }
    

    Formula 接口中除了抽象方法计算接口公式还定义了默认方法 sqrt。 实现该接口的类只需要实现抽象方法 calculate。 默认方法sqrt 可以直接使用。当然你也可以直接通过接口创建对象,然后实现接口中的默认方法就可以了,我们通过代码演示一下这种方式。

    public class Main {
      public static void main(String[] args) {
        // TODO 通过匿名内部类方式访问接口
        Formula formula = new Formula() {
            @Override
            public double calculate(int a) {
                return sqrt(a * 100);
            }
        };
        System.out.println(formula.calculate(100));     // 100.0
        System.out.println(formula.sqrt(16));           // 4.0
      }
    }
    

    formula 是作为匿名对象实现的。该代码非常容易理解,6行代码实现了计算 sqrt(a * 100)

    不管是抽象类还是接口,都可以通过匿名内部类的方式访问。不能通过抽象类或者接口直接创建对象。对于上面通过匿名内部类方式访问接口,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,之后我们让接口的引用来指向这个对象。

    Stream(流)

    Stream 是在 java.util 下的。Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回 Stream 本身,这样我们就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection 的子类:List 或者 Set。Map 不支持。Stream 的操作可以串行执行或者并行执行。

    当我们使用 Stream 时,我们将通过三个阶段来建立一个操作流水线。

    1. 创建一个 Stream。
    2. 在一个或多个步骤中,指定将初始 Stream 转换成为另一个 Stream 的中间操作。
    3. 使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。

    在这之后 stream 就不会再被使用了。

    创建 stream

    通过 Java 8 在 Collection 接口中新提娜佳的 stram 方法,可以将任何集合转化为一个 Stream。如果我们面对的是一个数组,也可以用静态的 Stream.of 方法将其转化为一个 Stream。

    @Test
    public void test1(){
        List<String> stringList = new ArrayList<>();
        stringList.add("ddd2");
        stringList.add("aaa2");
        stringList.add("bbb1");
        stringList.add("aaa1");
        stringList.add("bbb3");
        stringList.add("ccc");
        stringList.add("bbb2");
        stringList.add("ddd1");
        Stream<String> stream = stringList.stream();
        //Stream<String> stringStream = stringList.parallelStream();
    }
    

    我们可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。

    Filter(过滤)

    过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作。(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。

    stringList
        .stream()
        .filter(s->s.startsWith("a"))
        .forEach(System.out::println);
    

    forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。

    Sorted(排序)

    排序是一个中间操作,返回的是排序好的 Stream 。如果我们不指定一个自定义的 Comparator 则会使用默认排序。

    stringList
        .stream()
        .sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
        .forEach(System.out::println);
    

    需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的。

    Map(映射)

    中间操作 map 会将元素根据指定的 Function 接口来依次将元素转成另外的对象。

    map返回的 Stream 类型是根据我们 map 传递进去的函数的返回值决定的。

    stringList
        .stream()
        .map(String::toUpperCase)
        .sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
        .forEach(System.out::println);
    

    Match(匹配)

    Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是 最终操作 ,并返回一个 boolean 类型的值。

    boolean anyStartsWithA =
        stringList
        .stream()
        .anyMatch((s) -> s.startsWith("a"));
    System.out.println(anyStartsWithA);      // true
    
    boolean allStartsWithA =
        stringList
        .stream()
        .allMatch((s) -> s.startsWith("a"));
    System.out.println(allStartsWithA);      // false
    
    boolean noneStartsWithZ =
        stringList
        .stream()
        .noneMatch((s) -> s.startsWith("z"));
    System.out.println(noneStartsWithZ);      // true
    

    Count(计数)

    计数是一个 最终操作,返回Stream中元素的个数,返回值类型是 long

    long count = stringList
        .stream()
        .map(String::toUpperCase)
        .sorted((o1, o2) -> Integer.compare(o1.length(), o2.length()))
        .count();
    System.out.println(count);
    

    Parallel Stream(并行流)

    Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

    下面使用串行流和并行流为一个大容器进行排序,比较两者性能。

    串行排序

    @Test
    public void test1(){
        int max = 1000000;
        List<String> list = new ArrayList<>(max);
        for (int i = 0; i < max; i++) {
            UUID uuid = UUID.randomUUID();
            list.add(uuid.toString());
        }
        long startTime = System.nanoTime();
        long count = list.stream().sorted().count();
        System.out.println(count);
        long endTime = System.nanoTime();
        long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
        System.out.println(millis);
        //1000000
        //877
    }
    

    并行排序

    @Test
    public void test2(){
        int max = 1000000;
        List<String> list = new ArrayList<>(max);
        for (int i = 0; i < max; i++) {
            UUID uuid = UUID.randomUUID();
            list.add(uuid.toString());
        }
        long startTime = System.nanoTime();
        long count = list.parallelStream().sorted().count();
        System.out.println(count);
        long endTime = System.nanoTime();
        long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
        System.out.println(millis);
    }
    //1000000
    //512
    

    可以明显看出在大数据量的情况下并行排序比串行来的快。但是小数据量的话却是串行排序比较快,原因是并行需要涉及到上下文切换。

    Collector 和 Collectors

    Collector 是专门用来作为 Stream 的 collect 方法的参数的。而 Collectors 是作为生产具体 Collector 的工具类。

    • toList():将流构造成 list

      List<String> collect = list.stream().collect(Collectors.toList());
      
    • toSet():将流构造成set

      Set<String> set = list.stream().collect(Collectors.toSet());
      Set<String> treeSet = list.stream().collect(Collectors.toCollection(TreeSet::new));
      
    • joining():拼接流中所有字符串

      String collect = list.stream().collect(Collectors.joining());
      String collect = list.stream().collect(Collectors.joining(";"));
      
    • toMap():将流转成 map

      Map<String, String> collect = list
                      .stream()
                      .collect(Collectors
                      .toMap(e -> "key" + e, e -> "v" + e,(a,b)->b,HashMap::new));
      

      上面的 e -> "key" + e 定义了 map 的 key 的生成规则,e -> "v" + e 定义了 map 的 value 的生成规则,(a,b)->b 表示冲突的解决方案,如果键 a 和 键 b 冲突了则该键键值取 b 的,HashMap::new 定义了生成的 map 为 hashmap。

    Map 新方法

    Map 虽然不支持 Stream 但是我们可以通过 map.keySet().stream(),map.values().stream()map.entrySet().stream() 来通过过去键、值的集合再转换成流进行处理。

    Java 8 中 map 新方法:

    • putIfAbsent(key, value)//有则不加,无则加
      
    • map.forEach((key, value) -> System.out.println(value));//循环打印
      
    • map.computeIfPresent(3, (num, val) -> val + num);//当key 存在则执行后面方法
      
    • map.computeIfAbsent(23, num -> "val" + num);//当key 不存在时执行后面方法
      
    • map.getOrDefault(42, 1);//有则获取,无则置 1
      
    • map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
      //如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。
      

    新的日期与时间 API

    • LocalTime(本地时间)

      LocalTime 定义了一个没有时区信息的时间

      方法 描述
      now,of 这些静态方法可以根据当前时间或指定的年、月、日来创建一个 LocalTime 对象
      getHour,getMinute,getSecond,getNano 获得当前 LocalTime 的小时、分钟、秒钟及微妙值
      isBefore,isAfter 比较两个LocalTime
    • LocalDate(本地日期)

      LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和LocalTime基本一致。

      方法 描述
      now,of 这些静态方法可以根据当前时间或指定的年、月、日来创建一个LocalDate对象
      getDayOfMonth 获取月份天数(在 1~ 31 之间)
      getDayOfYear 获取年份天数(在1~366之间)
      getMonth,getMonthValue 获得月份,或者为一个 Month 枚举的值,或者是 1 ~ 12 之间的一个数字
      getYear 获取年份
      isBefore,isAfter 比较两个LocalDate

    上面这些方法是比较常用的,其余的可以自行查阅。

    参考资料

  • 相关阅读:
    FZU 2098 刻苦的小芳(卡特兰数,动态规划)
    卡特兰数总结
    FZU 1064 教授的测试(卡特兰数,递归)
    HDU 4745 Two Rabbits(区间DP,最长非连续回文子串)
    Java 第十一届 蓝桥杯 省模拟赛 正整数的摆动序列
    Java 第十一届 蓝桥杯 省模拟赛 反倍数
    Java 第十一届 蓝桥杯 省模拟赛 反倍数
    Java 第十一届 蓝桥杯 省模拟赛 反倍数
    Java 第十一届 蓝桥杯 省模拟赛 凯撒密码加密
    Java 第十一届 蓝桥杯 省模拟赛 凯撒密码加密
  • 原文地址:https://www.cnblogs.com/czsy/p/11203931.html
Copyright © 2011-2022 走看看