jdk1.8 新特性

本文摘抄自网络,作为个人学习笔记

 

知识点:

  • jdk1.8 对 hashmap 的优化
  • Lambda 表达式
  • 函数式接口
  • 方法引用和构造器调用
  • Stream API
  • 接口中的默认方法和静态方法
  • 新时间日期 API

 

 

map 新原理 

 

  在 jdk1.8 中对 hashMap 等 map 集合的数据结构进行了优化。原来的 hashMap 采用的数据结构是哈希表(数组+链表),hashMap 默认大小是 16,一个 0-15 索引的数组。怎么往里面存储元素呢?首先调用元素的 hashcode 方法,计算出 哈希码值,经过 哈希算法算成数组的索引值,如果对应的索引处没有元素,直接存放,如果有对象在,那么比较他们的 equals 方法,如果内容一样,后一个 value 会将前一个 value 的值覆盖,如果不一样,在 1.7 的时候,后加的放在前面形成一个链表,这就造成了“碰撞”,在某些情况下,如果链表无限下去,,那么效率极低,碰撞是避免不了的。

  加载因子:0.75,数组扩容,达到总容量的 75%,就进行扩容,但是无法避免碰撞的情况发生。

在 1.8 之后,在 数组 + 链表 + 红黑树 来实现 hashmap,当碰撞的元素大于 8 & 总容量大于 64,会有红黑树的引入。除了添加之外,效率都比链表高,1.8 之后链表新进元素加到末尾。

  ConcurrentHashMap(锁分段机制),concurrentLevel ,jdk1.8 采用 CAS 算法(无锁算法,不再使用锁分段)。

  数组 + 链表 中也引入了红黑树的使用。

 

 

Lambda 表达式

 

  Lambda 表达式本质上是一段匿名内部类,也可以是一段传递的代码

Comparartor<Integer> cpt = (x, y) -> Integer.compare(x, y);
TreeSet<Integer> set = new TreeSet<>(cpt);

 

 Lambda 表达式的语法总结:() -> ();

 

注:当一个接口中存在多个抽象方法时,如果使用 lambda 表达式,并不能智能地匹配对应的抽象方法,因此引入了函数式接口的概念

 

 

函数式接口

   函数式接口的提出是为了给 Lambda 表达式的使用提供更好的支持。

 

什么是函数式接口?

  简单来说就是只定义了一个抽象方法的接口(Object 类的 public 方法除外),就是函数式接口,并且还提供了注解:@FunctionalInterface

 

常见的四大函数式接口:

  • Consumer<T>:消费型接口,有参无返回值

    // 消费型

    public static void changeStr(String str, Consumer<String> con){
      con.accept(str);
    }

    // 调用

    changeStr("hello", (str) -> System.out.println(str));

    说明:这里方法的参数是字符串和 Consumer,Consumer 是接口,调用时需要实现这个接口,即实现了消费这个操作。

  • Supplier<T>:供给型接口,无参有返回值
    // 供给型
    public
    static String getValue(Supplier<String> sup){ return sup.get(); }

    // 调用
    System.out.println(getValue(() -> "1111111"));

    说明:这里同上,返回的是个字符串,按理有个 return,但是 lambda 表达式说,按照我的规范,可以省略这个 return。

  • Function<T, R>:函数式接口,有参数有返回值
    // 函数式接口
    public
    static Long changeNum(Long num, Function<Long, Long> function){ return function.apply(num); }
    // 调用

    Long result = changeNum(100L, (x) -> x+200L);

     

  • Predicate<T>:断言型接口,有参有返回值,返回值是 boolean 类型
    // 断言型
    public static boolean changeBoolean(String str, Predicate<String> pre){
        return pre.test(str);
    }
    // 调用
    boolean reboolean = changeBoolean("hello", (str)->str.length() > 5);

     

   总结:函数式接口的提出是为了让我们更加方便的使用 lambda 表达式,不需要自己再手动创建一个函数式接口,直接拿来用就好了。

 

 

方法引用

  若 lambda 体中的内容有方法已经实现了,那么可以使用 “方法引用”。也可以理解为方法引用是 lambda 表达式的另外一种表现形式,并且其语法比 lambda 表达式更加简单。

 

1)方法引用

  三种表现形式:

  1. 对象 :: 实例方法名

  2. 类 :: 静态方法名

  3. 类 :: 实例方法名(lambda 参数列表中第一个参数是实例方法的调用者,第二个参数是实例方法的参数时可用)

// 方法引用
/**
 * 1. lambda 体中调用方法的参数列表与返回值类型,
 *      要与函数式接口中抽象方法的参数列表和返回值保持一致
 * 2. 若 lambda 参数列表中的第一个参数是实例方法的调用者,
 *      而第二个参数是实例方法的参数时,可以使用 ClassName::method
 */

Consumer<Integer> consumer = (x) -> System.out.println("x");
consumer.accept(100);

// 方法引用 - 类名 :: 静态方法名
BiFunction<Integer, Integer, Integer> biFun = (x, y) -> Integer.compare(x, y);
BiFunction<Integer, Integer, Integer> biFun2 = Integer::compare;
Integer resBi = biFun2.apply(1200, 200);
System.out.println("resBi = " + resBi);

// 方法引用 - 类名 :: 实例方法名
BiFunction<String, String, Boolean> fun1 = (str1, str2) -> str1.equals(str2);
BiFunction<String, String, Boolean> fun2 = String::equals;
Boolean resB2 = fun2.apply("hello", "hello");
System.out.println("resB2 = " + resB2);

 

 2)构造器引用

  格式:ClassName :: new 

    Supplier<Product> sup = () -> new Product();
    System.out.println("sup.get() = " + sup.get());

    Supplier<Product> sup2 = Product::new;
    System.out.println("sup2.get() = " + sup2.get());

  // 一个参数的情况 Function
<String, Product> bifun2 = Product::new; BiFunction<Integer, String, Product> bifun = (x, y) -> new Product(y, x);

 

 3)数组引用

  格式:Type[] :: new 

    // 数组引用
    Function<Integer, String[]> func = (x) -> new String[x];
    Function<Integer, String[]> func2 = String[] :: new ;

    String[] strArray = func2.apply(10);
    Arrays.stream(strArray).forEach(System.out::println);

 

 

 Strean API

   Stream 操作的三个步骤

  • 创建 stream
  • 中间操作(过滤、map)
  • 终止操作

  stream 的创建:

    // Stream 操作
    // 1. 通过 Collection 系列集合提供的 stream() 或者 paralleStream()
    List<String> slist = new ArrayList<>();
    Stream<String> stream1 = slist.parallelStream();
    Stream<String> stream11 = slist.stream();

    // 2. 通过 Arrays 的静态方法 stream() 获取数组流
    String[] strArr = new String[10];
    Stream<String> stream2 = Arrays.stream(strArr);

    // 3. 通过 Stream 类的静态方法 of
    Stream<String> stream3 = Stream.of("aa", "nn", "cc");

    // 4. 创建无限流
    // 迭代
    Stream<Integer> stream4 = Stream.iterate(0, (x)->x+2);
    // 生成
    Stream.generate(()->Math.random());

 

  stream 的中间操作(Intermediate):一个流可以后面跟随 0 个或者多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射 / 过滤,然后返回一个新的流,交给下一个操作者使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

    // 筛选、过滤、去重
    list.stream()
            .filter(e -> e.getPrice()>20)
            .limit(3)
            .skip(1)
            .forEach(System.out::println);

    System.out.println("---------------------");

    // 通过 map 映射,生成新的流
    list.stream()
            .map(e -> {if(e.getPrice()>20){return e;}else{return null;}})
            .forEach(System.out::println);

    System.out.println("++++++++++++++++++++++");

    // 做排序
    list.stream()
            .sorted((p111, p222) -> p111.getPrice().compareTo(p222.getPrice()) )
            .forEach(System.out::println);

 

  stream 的终止操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被用光了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正的开始流的遍历,并且会生成一个结果。

    /**
     * 查找和匹配
     * allMatch 检查是否匹配所有元素
     * anyMatch 检查是否至少匹配一个元素
     * noneMatch 检查是否没有匹配所有的元素
     * findFirst 返回第一个元素
     * findAny 返回当前流中的任意元素
     * count 返回流中元素的总个数
     * max 返回流中最大值
     * min 返回流中最小值
     */
    boolean allMatchRes = list.stream().allMatch((e) -> e.getPrice()>30);
    System.out.println("allMatchRes = " + allMatchRes);

    boolean anyMatchRes = list.stream().anyMatch(e -> e.getPrice()>30);
    System.out.println("anyMatchRes = " + anyMatchRes);

    boolean noneMatchRes = list.stream().noneMatch(e ->e.getPrice() > 50);
    System.out.println("noneMatchRes = " + noneMatchRes);

    Optional<Product> findFirstRes = list.stream().findFirst();
    System.out.println("findFirstRes = " + findFirstRes.get());

 

   还有两个强大的终止操作 reduce 和 collect 。

   reduce 操作是将流中元素反复结合起来,得到一个值

    /**
     * reduce : 规约操作
     */
    List<Integer> numList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
    Integer count = numList.stream().reduce(0, (x, y) -> x+y);
    System.out.println("count = " + count);

    Optional<Integer> reduce = list.stream()
                    .map(Product::getPrice)
                    .reduce(Integer::sum); System.out.println(
"reduce = " + reduce);

 

   collect 操作:collect 操作将流转换成其他形式,接收一个 Collection 接口的实现,用于给 Stream 中元素做汇总的方法

    /**
     *  collect 收集操作
     */
    List priceList = list.stream()
                        .map(Product :: getPrice)
                        .collect(Collectors.toList());
    priceList.stream().forEach(System.out :: println);

 

  在对于一个 Stream 进行多次转换操作(Intermediate操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样的时间复杂度就是 N 个 for 循环里把所有操作都做完的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样理解:Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。so easy ! 

 

 

 并行流和串行流

   在 jdk1.8 新的 stream 包中,针对集合的操作提供了并行操作流和串行操作流。并行流就是把内容切割成多个数据块,并且使用多个线程分别处理每个数据块的内容。Stream api 中声明可以通过 parallel() 与 sequential() 方法在并行流和串行流之间进行切换。

  jdk1.8 并行流使用的是 fork / join 框架进行并行操作。

 

ForkJoin 框架

  Fork / Join 框架:就是在必要的情况下,将一个大的任务,进行拆分(fork)成若干小任务(拆分到不能拆分时),再将一个个的小任务运算结果进行 join 汇总。

  关键字:递归分合,分而治之

  减少了线程的等待时间,提高了性能。

 

 

Optional 容器

  使用 Optional 容器可以快速的定位 NPE,并且在一定程度上可以减少对参数非空检验的代码量。

 

 

接口中可以定义默认实现方法和静态方法

  在接口中可以使用 default 和 static 关键字来修饰接口中的普通方法。

  在 jdk1.8 中很多接口会新增方法,为了保证 1.8 向下兼容, 1.7 版本中的接口实现类不用每个都重新实现新添加的接口方法,引入了 default 默认实现,static 的用法是直接用接口调用方法即可。当一个类继承父类又实现接口时,若后两者方法名相同,则优先继承父类中的同名方法,即“类优先”,如果是实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法。

 

 

新的日期 API     LocalDate | LocalTime | LocalDateTime

  新的日期 API 都是不可变的,更适用于多线程的使用环境中

    LocalDateTime dateTime = LocalDateTime.now();
    System.out.println("dateTime = " + dateTime);
    System.out.println(dateTime.getYear());
    System.out.println(dateTime.getMonthValue());
    System.out.println(dateTime.getDayOfMonth());
    System.out.println(dateTime.getHour());
    System.out.println(dateTime.getMinute());
    System.out.println(dateTime.getSecond());
    System.out.println(dateTime.getNano()); // 纳秒

    LocalDateTime dateTime2 = LocalDateTime.of(2020, 3, 31, 20, 9);
    System.out.println("dateTime2 = " + dateTime2);

    LocalDateTime dateTime3 = dateTime2.plusHours(3);
    System.out.println("dateTime3 = " + dateTime3);

    Instant ins = Instant.now();
    System.out.println("ins = " + ins);

... 未完不续了 ...

  表示日期的 LocalDate

  表示时间的 LocalTime

  表示日期时间的 LocalDateTime

 

新的日期 API 的几个优点:

之前使用的 java.util.Date 月份从 0 开始,我们一般会 加1 使用,很不方便,java.time.LocalDate 月份和星期都改成 enum

java.util.Date 和 SimpleDateFormate 都不是线程安全的,而 LocalDate 和 LocalTime 和最基本的 String 一样,是不变类型,不但线程安全,而且不能修改

java.util.Date 是一个“万能接口”,它包含日期、时间、毫秒数,新接口更好用的原因是考虑到了日期的操作,经常发生往前或往后推几天的情况,用 java.util.Date 配合Calendar 代码较繁琐。

 

 

 https://blog.csdn.net/qq_29411737/article/details/80835658

 

posted @ 2020-03-31 20:25  停不下的时光  阅读(285)  评论(0编辑  收藏  举报