Go To My HomePage

Java8新特性

一、Map结构优化

​ java8之前HashMap存储结构如下图,将Map中的key进行哈希运算得到hashCode,当出现hashCode相同但equals不同时称此现象为碰撞,发生碰撞时会形成链表结构,取值时会遍历整个链表结构效率较低。

hashMap-old-struct

​ java8采用的HashMap存储结构如下图,当发生碰撞形成的链表上元素个数大于8时,总容量大于64会将链表转换为红黑树。此种情况下除了添加元素慢一些,其余操作(查询,删除)均高于链表结构。

hashMap-new-struct

​ java8之前concurrentHashMap为HashTable的组合达到线程安全的效果,默认并发级别为16即concurrentHashMap由16个HashTable组成。java8采用CAS算法到达线程安全的效果,数据结构为java8的HashMap结构。

注:hashMap起始默认容器大小为16,当容器元素个数到达75%(扩容因子)开始扩容,扩容一倍大小重新计算位置。

二、JVM内存结构的改变

历史:之前很多公司生产的JVM早已没有永久代,只是SUN的JVM还没有淘汰永久代

  • Oracle-SUN Hotspot
  • Oracle HRocket
  • IBM J9 JVM
  • Alibaba Taobao JVM

​ java8将永久代变为元空间(MetaSpace)。之前永久代在JVM中分配,永久代基本不回收占用JVM内存空间。java8废弃永久代改为元空间,元空间在操作系统的内存上进行分配。

  • PremGenSize和MaxPremGenMaxSize被删除
  • MetaSpaceSize和MetaSpcaeMaxSiez被添加

三、Lambda表达式

​ 在函数式语言中,我们只需要给函数分配变量,并将这个函数作为参数传递给其它函数就可实现特定的功能。而java如前言中所述,不能直接将方法当作一个参数传递。同时匿名内部类又存在诸多不便:语法过于冗余,匿名类中的this和变量名容易使人产生误解,类型载入和实例创建语义不够灵活,无法捕获非final的局部变量等。 Lambda 表达式的出现为 Java 添加了缺失的函数式编程特点,使我们能将函数当做一等公民看待。

概述

​ Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为-> ,该操作符被称为Lambda 操作符或箭头操作符。它将Lambda 分为两个部分: ​ 左侧:指定了Lambda 表达式需要的所有参数 ​ 右侧:指定了Lambda 体,即Lambda 表达式要执行的功能


​ Lambda表达式实现的必须是函数式接口。

​ 只包含一个抽象方法的接口,称为函数式接口。可以通过Lambda 表达式来创建该接口的对象。(若Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。我们可以在任意函数式接口上使用@FunctionalInterface注解,这样做可以检查它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。

演示

/**
 * 情景一:无参数,无返回值,一条语句
 */
@Test
public void test() throws Exception {
  Runnable r = () -> System.out.println("Hello Lambda");
  r.run();
}
/**
 * 情景二:有一个参数,并且无返回值,一条语句
 */
@Test
public void test() throws Exception {
  Consumer<String> consumer = (x) -> System.out.println("Hello " + x);
  consumer.accept("Lambda");
}
/**
 * 情景三:有两个以上参数,多条语句
 */
@Test
public void test() throws Exception {
  Comparator<Integer> comparator = (x, y) -> {
    System.out.println("多条语句");
    return Integer.compare(x, y);
  };
}

注:

  • 当参数只有一个时小括号可以不写,但一般情况下不省略。

  • 当只有Lambda体中只有一条语句可以省略{}return,一般情况下省略。

  • Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器可以通过上下文进行类型推断。如果要写需要全部写上类型。

内置核心函数式接口

①消费型接口

@FunctionalInterface
public interface Consumer<T> {

  void accept(T t);

  // 链式调用,之后继续调用消费型接口
  default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
  }
}

②供给型接口

@FunctionalInterface
public interface Supplier<T> {

  T get();
}

③函数型接口

@FunctionalInterface
public interface Function<T, R> {

  R apply(T t);

  // 链式调用,在调用此方法之前调用传入的函数式接口
  default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
  }

  // 链式调用,在调用此方法之后调用传入的函数式接口
  default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
  }

  // 返回输入的参数,Function.identity().apply("AAA")返回AAA
  static <T> Function<T, T> identity() {
    return t -> t;
  }
}

④断言型接口

@FunctionalInterface
public interface Predicate<T> {

  boolean test(T t);

  // 链式调用,需要同时满足条件
  default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
  }

  // 取相反值
  default Predicate<T> negate() {
    return (t) -> !test(t);
  }

  // 链式调用,只需要满足一个条件即可
  default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
  }

  static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
      ? Objects::isNull
        : object -> targetRef.equals(object);
  }
}

四、方法引用与构造器引用

方法引用

​ 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用(实现抽象方法的参数列表和返回类型,必须与方法引用方法的参数列表和返回类型保持一致)

​ 方法引用:使用操作符:: 将方法名和对象或类的名字分隔开来。如下三种主要使用情况:

  • 对象::实例方法

    @Test
    public void test() throws Exception {
      PrintStream ps = System.out;
      Consumer<String> consumer = ps::println;
      consumer.accept("Lambda");
    }
    
  • 类::静态方法

    @Test
    public void test() throws Exception {
        Comparator<Integer> comparator = Integer::compare;
    }
    
  • 类::实例方法,第一个参数是此方法的调用者,第二个参数是此方法的参数时可以使用ClassName::MethodName

    @Test
    public void test() throws Exception {
      BiPredicate<String, String> predicate = String::equals;
    }
    

构造器引用

  • 普通对象的构造器引用
public void test() throws Exception {
  Supplier<Employee> supplier = Employee::new; // 与函数式接口参数列表匹配调用对应的构造方法
  supplier.get();
}
  • 数组对象的构造器引用
@Test
public void test() throws Exception {
  // 传入的参数必须是Integer,返回是一个数组
  Function<Integer, Employee[]> function = Employee[]::new;//new Employee[num]
}

五、Stream

简介

​ Stream 是Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用SQL 执行的数据库查询。也可以使用Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

流是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算”

assets/Stream

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

Stream 的操作三个步骤

一、创建Stream

@Test
public void test() throws Exception {
  // 1、通过Collection系列集合提供的stream()或parallelStream()获取
  List<String> list = new ArrayList<String>();
  Stream<String> stream1 = list.stream();

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

  // 3、通过Stream类中的静态方法of()
  Stream<String> stream3 = Stream.of("AAA","BBB","CCC");

  // 4、迭代创建无限流
  Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 1);

  // 5、生成创建无限流
  Stream<Double> stream5 = Stream.generate(Math::random);
}

二、中间操作

  • 筛选

    /**
     * filter——接受Lambda,从流中排除某些元素
     * limit——截断流,使其元素不超过给定数量
     * skip(n)——跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit互补
     * distinct——去重,通过元素生成的hashCode和equals去重
     */
    @Test
    public void test() throws Exception {
      emps.stream().filter((e) -> e.getSalary() > 5000);
      
      emps.stream().limit(5);
      
      emps.stream().skip(5);
      
      emps.stream().distinct();
      
      // 此操作会发生短路,取出工资大于5000的两个其余不再遍历
      emps.stream().filter((e) -> e.getSalary() > 5000).limit(2);
    }
    
  • 映射

    /**
     * map——接收Lambda,将元素转换为其他形式。
     * flatMap——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
     */
    @Test
    public void test() throws Exception {
      emps.stream().map(Employee::getName);
      
      // 会将【【A】,【B】,【C】】转换为【A,B,C】
      List<List<String>> lls = Arrays.asList(Arrays.asList("A"),Arrays.asList("B"),Arrays.asList("C"));
      lls.stream().flatMap((ls) -> ls.stream());
    }
    
  • 排序

    /**
     * sorted():自然排序
     * sorted(Comparator com):定制排序
     */
    @Test
    public void test() throws Exception {
      List<String> arr = Arrays.asList("AAA","BBB","CCC");
      arr.stream().sorted(); // 自然排序
      emps.stream().sorted((x, y) -> x.getAge().compareTo(y.getAge())); // 定制排序
    }	
    

三、终止操作(终端操作)

  • 查找与匹配

    /**
     * allMatch:检查是否匹配所有元素
     * anyMatch:检查是否至少匹配一个元素
     * noneMatch:检查是否没有匹配所有元素
     * findFirst:返回第一个元素
     * findAny:返回任意一个元素
     * count:返回元素个数
     * max:返回流中最大元素
     * min:返回流中最小元素
     */
    @Test
    public void test() throws Exception {
      // employees是否年龄都大于18
      boolean allMatch = emps.stream().allMatch((e) -> e.getAge() > 18);
      // employees是否存在工资大于6k
      boolean anyMatch = emps.stream().anyMatch((e) -> e.getSalary() > 6000);
      // employees是否没有人叫小明
      boolean noneMatch = emps.stream().noneMatch((e) -> e.getName().equals("小明"));
      
      // 返回第一个元素
      Optional<Employee> findFirst = emps.stream().findFirst(); 
      // 返回任意一个元素,stream()会一直返回第一个
      Optional<Employee> findAny = emps.parallelStream().findAny(); 
      
      // 返回元素个数
      long count = emps.stream().count(); 
      // 返回年龄最大的
      Optional<Employee> max = emps.stream().max((x, y) -> Integer.compare(x.getAge(),y.getAge())); 
      // 返回年龄最小的
      Optional<Employee> min = emps.stream().min((x, y) -> Integer.compare(x.getAge(),y.getAge())); 
    }
    
  • 归约与收集

    /**
     * 归约:reduce(T indetity, BinaryOperator) / reduce(BinaryOperator)
     *     可以将流中的元素反复结合起来得到一个新值,indetity-起始值
     */
    @Test
    public void testReduce() throws Exception {
      emps.stream().map(Employee::getSalary).reduce(0.0, Double::sum);	//计算工资总和
    }
      
    /**
     * 收集:collect - 将流转换为其他形式,接收一个Collector接口实现,用于数据汇总【Collectors工具类】
     */
    @Test
    public void testCollect() throws Exception {
      List<String> names = emps.stream().map(Employee::getName).collect(Collectors.toList());
      // 转换为自定义数据类型
      HashSet<Double> salarys = emps.stream().map(Employee::getSalary).collect(Collectors.toCollection(HashSet::new));
      // 用收集器得到总个数
      long count = emps.stream().collect(Collectors.counting()); 
      // 取平均值
      Double avg = emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
      // 取总和
      Double sum = emps.stream().collect(Collectors.summingDouble(Employee::getSalary));
      // 取最大值
      Optional<Employee> max = emps.stream().collect(Collectors.maxBy(
        (x, y) -> Double.compare(x.getSalary(), y.getSalary())));
      // 拼接字符串
      String joinNames = emps.stream().map(Employee::getName).collect(Collectors.joining(","));
      // 通过summaryStatistics获得值
      DoubleSummaryStatistics summaryStatistics = emps.stream().collect(Collectors.summarizingDouble(Employee::getSalary));
      summaryStatistics.getAverage();
      summaryStatistics.getCount();
      summaryStatistics.getMax();
      summaryStatistics.getMin();
      summaryStatistics.getSum();
      
      // 分组,也可以进行多级分组
      Map<Double, List<Employee>> groupSalary = emps.stream().collect(Collectors.groupingBy(Employee::getSalary)); //以工资分组
      Map<String, List<Employee>> groupAge = emps.stream().collect(Collectors.groupingBy(
        (e) -> e.getAge() > 35 ? "中年" : "青年")); //以年龄分组
      
      // 分区是一种特殊的分组,结果 map至少包含两个不同的分组一个true,一个false
      Map<Boolean, List<Employee>> partitionSalary = emps.stream().collect(Collectors.partitioningBy((e) -> e.getSalary() > 5000));
    }
    
  • 遍历

    @Test
    public void test() throws Exception {
      emps.stream().forEach(System.out::println);
    }
    

并行流与串行流

传统线程池的缺陷

ThreadPool

Fork/Join

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

​ 采用工作窃取模式(work-stealing): ​ 当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了线程的等待时间,提高了性能。

fork-join

Fork/Join的好处

fork-join-good

Fork/Join示例

public class ForkJoinCalculate extends RecursiveTask<Long> {

  private static final long serialVersionUID = 1L;
  private long start;
  private long end;
  private static final long THRESHOLD = 10000;

  public ForkJoinCalculate(long start, long end) {
    this.start = start;
    this.end = end;
  }

  @Override
  protected Long compute() {
    long length = end - start;
    if(length <= THRESHOLD) {
      long sum = 0;
      for (long i = start; i <= end; i++) {
        sum += i;
      }
      return sum;
    }else {
      long middle = (start + end) / 2;
      ForkJoinCalculate left = new ForkJoinCalculate(start,middle);
      left.fork();
      ForkJoinCalculate right = new ForkJoinCalculate(middle + 1,end);
      right.fork();
      return left.join() + right.join();
    }
  }

  public static void main(String[] args) {
    ForkJoinPool pool = new ForkJoinPool();
    ForkJoinCalculate calculate = new ForkJoinCalculate(1, 1000000000L);
    Long result = pool.invoke(calculate);
    System.out.println(result);
  }
}

并行流

​ 底层依旧使用的Fork/Join框架,使用的公共的ForkJoinPool,大大简化了Fork/Join框架的使用难度

@Test
public void test() throws Exception {
  OptionalLong result = LongStream.rangeClosed(1, 10000000L).parallel().reduce(Long::sum);
  System.out.println(result.getAsLong());
}

六、Optional 类

​ Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用null 表示一个值不存在,现在Optional 可以更好的表达这个概念。并且可以避免空指针异常。

常用方法:

  • Optional.of(T t) : 创建一个Optional 实例,t为null会抛出空指针异常
  • Optional.get() : 如果没有值会抛出NoSuchElementException
  • Optional.empty() : 创建一个空的Optional 实例
  • Optional.ofNullable(T t):若t 不为null,创建Optional 实例,否则创建空实例
  • isPresent() : 判断是否包含值
  • orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
  • orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回s 获取的值
  • map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
  • flatMap(Function mapper):与map 类似,要求返回值必须是Optional

七、接口中方法

Java 8中允许接口中包含具有具体实现的方法,该方法称为默认方法,使用default关键字修饰。

Java 8中允许接口中定义和实现静态方法。

接口默认方法的”类优先”原则

​ 若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。

接口冲突

​ 如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突。

interface Foo {
  default String getFoo() { // 默认方法
    return "foo";
  }
  static String showFoo() { // 静态方法
    System.out.println("foo");    
  }
}

八、重复注解和类型注解

可重复注解

  • 在可重复注解上使用@Repeatable标注且提供容器类

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(MyAnnotations.class)
    public @interface MyAnnotation {
      String value();
    }
    
  • 容器类必须提供可重复注解[] value()

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {
      MyAnnotation[] value();
    }
    

类型注解

  • ElementType.TYPE_PARAMETER(Type parameter declaration) 用来标注类型参数

    @Target(ElementType.TYPE_PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TypeParameterAnnotation {
      
    }
      
    // 如下是该注解的使用例子
    public class TypeParameterClass<@TypeParameterAnnotation T> {
      public <@TypeParameterAnnotation U> T foo(T t) {
        return null;
      }    
    }
    
  • ElementType.TYPE_USE(Use of a type) 能标注任何类型名称

    public class TestTypeUse {
      
      @Target(ElementType.TYPE_USE)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface TypeUseAnnotation {
      
      }
      
      public static @TypeUseAnnotation class TypeUseClass<@TypeUseAnnotation T> extends @TypeUseAnnotation Object {
        public void foo(@TypeUseAnnotation T t) throws @TypeUseAnnotation Exception {
      
        }
      }
      
      // 如下注解的使用都是合法的
      @SuppressWarnings({ "rawtypes", "unused", "resource" })
      public static void main(String[] args) throws Exception {
        TypeUseClass<@TypeUseAnnotation String> typeUseClass = new @TypeUseAnnotation TypeUseClass<>();
        typeUseClass.foo("");
        List<@TypeUseAnnotation Comparable> list1 = new ArrayList<>();
        List<? extends Comparable> list2 = new ArrayList<@TypeUseAnnotation Comparable>();
        @TypeUseAnnotation String text = (@TypeUseAnnotation String)new Object();
        java.util. @TypeUseAnnotation Scanner console = new java.util.@TypeUseAnnotation Scanner(System.in);
      }
    }
    

九、新时间与日期

​ java8提供的日期时间均是线程安全的,原始的Data、DateFormat、Calendar等均线程不安全

  • java.time.LocalDateTime 本地日期时间,包括本地时间的操作(增、减、所属星期、月份等)
  • java.time.LocalDate 本地日期,包括本地时间的操作(增、减所属星期、月份等)
  • java.time.LocalTime 本地时间,包括本地时间的操作(增、减所属星期、月份等)
  • java.time.Instant 时间戳,1970.01.01:00:00:00到现在的毫秒,可以增、减、得到所属星期、月份等【instant.toEpochMilli()得到毫秒,纳秒、秒等用getXXX()】
  • java.time.Duration 计算时间间隔
  • java.time.Period 计算日期间隔
  • java.time.temporal.TemporalAdjusters TemporalAdjuster【时间矫正器】的工具类,提供了下星期、明年的某天。。。
  • java.time.format.DateTimeFormatter 格式化日期时间,使用ofPattern(String)自定义格式
  • java.time.ZonedDateTime 指定时区 ,在构造时间的时候会根据时区进行时间偏移,如果在后来设置,只是更改时区并不修改时间,可使用ZoneId获得时区