文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

JDK 新特性篇:JDK 8 新特性详解

2023-09-12 14:42

关注

Java8新特性简介

Java 8 (又称为 JDK 1.8) 是 Java 语言开发的一个主要版本。Java 8 是 Oracle 公司于 2014 年 3 月发布,可以看成是自 Java 5 以来最具革命性的版本。Java 8 为 Java 语言、编译器、类库、开发工具与 JVM 带来了大量新特性。

......

其中最为核心的是 Lambda 表达式和 Stream API。

哈希结构

Java8 优化了哈希算法,JDK7 之前是数组 + 链表,当两个不同对象经过哈希算法算出的下标相同时,则该数组下标处形成链表,旧的在链表最后面。但是这样产生很长的链表时,恰巧我们需要的对象在最后一个,则需要从头遍历到尾,效率低。

image-20220209144032901

JDK8 变成了数组 + 链表 + 红黑树,当链表的长度超出 8 时,并且整个数组的长度超出 64 时,则链表转为红黑树,即一种二叉树,这样减少了从头遍历到尾的效率低问题,只需要在二叉树的一方遍历,找出对象即可,不需要像全部遍历到最后。

image-20220209144503523

JVM方法区

永久代改为元空间,即最大内存变成电脑的最大内存。

因为永久代的垃圾回收条件苛刻,所以容易导致内存不足,而转为元空间,使用本地内存,极大减少了 OOM 异常,毕竟,现在电脑的内存足以支持 Java 的允许。

首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java 虚拟机规范》管束,并不要求统一。

Hotspot 中方法区的变化:

| 版本 | 变化 | | ---------- | ----------------------------------------------- | | JDK1.6 及以前 | 有永久代,静态变量存储在永久代上 | | JDK1.7 | 有永久代,但已经逐步「去永久代」,字符串常量池,静态变量移除,保存在堆中 | | JDK1.8 | 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中 |

为什么永久代要被元空间替代?

随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:

在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。比如某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。

Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space

而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

因此,默认情况下,元空间的大小仅受本地内存限制。

有些人认为方法区(如 HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)。

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。

Lambda表达式

为什么使用Lambda表达式

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

从匿名类到 Lambda 的转换

// 原来的匿名内部类 public void test1(){    Comparator com = new Comparator(){        @Override        public int compare(String o1, String o2) {            return Integer.compare(o1.length(), o2.length());       }   };    TreeSet ts = new TreeSet<>(com); } ​ // 现在的 Lambda 表达式 public void test2(){    Comparator com = (x, y) -> Integer.compare(x.length(), y.length());    TreeSet ts = new TreeSet<>(com); }

由此我们可以知道,Lambada 表达式替换了匿名内部类,自动实现匿名内部类的唯一接口。

逐步优化代码示例(顺便了解 Stream API)

首先是没有优化的代码,也就是初学者经常使用的代码,获取年龄小于 35 的员工信息、获取公司中工资大于 5000 的员工信息

``` public class TestLambda {    List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) );    // 需求:获取公司中年龄小于 35 的员工信息 public List filterEmployeeAge(List emps){ List list = new ArrayList<>();

for (Employee emp : emps) {  if(emp.getAge() <= 35){    list.add(emp);  }}return list;

}    // 需求:获取公司中工资大于 5000 的员工信息 public List filterEmployeeSalary(List emps){ List list = new ArrayList<>();

for (Employee emp : emps) {  if(emp.getSalary() >= 5000){    list.add(emp);  }}return list;

}

} ```

优化方式一:策略设计模式 + 匿名内部类

首先创建策略设计模式接口

public interface MyPredicate { public boolean test(T t); }

然后利用匿名内部类实现

``` public class TestLambda {    List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) );    // 需求:获取公司中年龄小于 35 的员工信息 public void filterEmployeeAge(){ List list = filterEmployee(emps, new MyPredicate () { @Override public boolean test(Employee t) { return t.getId() <= 103; } });

for (Employee employee : list) {  System.out.println(employee);}

}    // 需求:获取公司中工资大于 5000 的员工信息    public void filterEmployeeSalary(){ List list = filterEmployee(emps, new MyPredicate () { @Override public boolean test(Employee t) { return t.getSalary() >= 5000; } });

for (Employee employee : list) {  System.out.println(employee);}

} } ```

优化方式三:策略设计模式 + Lambda 表达式

``` public class TestLambda {    List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) );    // 需求:获取公司中年龄小于 35 的员工信息 public void test6(){ List list = filterEmployee(emps, (e) -> e.getAge() <= 35); list.forEach(System.out::println);

System.out.println("------------------------------------------");List list2 = filterEmployee(emps, (e) -> e.getSalary() >= 5000);list2.forEach(System.out::println);

} } ```

优化方式四:Stream API

注意:不需要策略设计模式

``` public class TestLambda {    List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) );    public void test7(){ emps.stream() .filter((e) -> e.getAge() <= 35) .forEach(System.out::println);

System.out.println("----------------------------------------------");emps.stream()  .map(Employee::getName)  .limit(3)  .sorted()  .forEach(System.out::println);

} } ```

Lambda表达式语法

Lambda 表达式在 Java 语言中引入了一个新的语法元素和操作符。这个操作符为 -> , 该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:

语法格式一:无参,无返回值,Lambda 体只需一条语句

public void test1(){    Runnable r1 = () -> System.out.println("Hello Lambda!"); }

语法格式二:Lambda 需要一个参数,并且无返回值

public void test2(){    Consumer con = (x) -> System.out.println(x);    con.accept("Lambda 表达式"); }

语法格式三:Lambda 只需要一个参数时,参数的小括号可以省略

public void test2(){    Consumer con = x -> System.out.println(x);    con.accept("Lambda 表达式"); }

语法格式四:Lambda 需要两个参数,并且有返回值

public void test3(){    Comparator com = (x, y) -> {        System.out.println("函数式接口");        return Integer.compare(x, y);   }; }

语法格式五:当 Lambda 体只有一条语句时,return 与大括号可以省略

public void test4(){    Comparator com = (x, y) -> Integer.compare(x, y); }

语法格式六:数据类型可以省略,因为可由编译器推断得出,称为「类型推断」

public void test4(){    // 类型不省略    Comparator com = (Integer x, Integer y) -> Integer.compare(x, y);    // 数据类型省略    Comparator com = (x, y) -> Integer.compare(x, y); }

类型推断

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的「类型推断」。

函数式接口

什么是函数式接口

只包含一个抽象方法的接口,称为函数式接口。

你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。

我们可以在任意函数式接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。

自定义函数式接口

使用 @FunctionalInterface 注解

@FunctionalInterface public interface MyFunction {    public String getValue(String str); }

函数式接口中使用泛型

@FunctionalInterface public interface MyFunction2 { public R getValue(T t1, T t2); }

作为参数传递Lambda表达式

这里面使用了自定义函数式接口的第一个例子。

public String toUpperString(MyFunction mf, String str) {    return mf.getValue(str) }

作为参数传递 Lambda 表达式:

String newStr = toUpperString( (str) -> str.toUpperCase(), "abcdef"); System.out.println(newStr);

作为参数传递 Lambda 表达式:为了将 Lambda 表达式作为参数传递,接收 Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。

四大核心函数式接口

这是 Java 为我们提供的四个函数式接口,所以简单的函数式接口不用自己写。

| 函数式接口 | 参数类型 | 返回类型 | 用途 | | --------------------- | ---- | ------- | ---------------------------------------------------------- | | Consumer 消费型接口 | T | void | 对类型为 T 的对象应用操作,包含方法 void accept(T t); | | Supplier 供给型接口 | 无 | T | 返回类型为 T 的对象,包含方法 T get(); | | Function函数型接口 | T | R | 对类型为 T 的对象应用操作,并返回结果。结果是 R 类型的对象。包含方法 R apply(T t); | | Predicate 断言型接口 | T | boolean | 确定类型为 T 的对象是否满足某约束,并返回 boolean 值。包含方法 boolean test(T t); |

消费型接口 Consumer

接口的方法

void accept(T t);

没有返回值,参数一去不复返

public void happy(double money, Consumer con){    con.accept(money); } ​ public void test1(){    happy(10000, (m) -> System.out.println("消费:" + m + "元")); }

供给型接口 Supplier

接口的方法

T get();

没有参数,返回值是泛型 T,也就是参数进入接口后,回来的还是自己(值可能发生改变)

// 产生指定个数的整数,并放入集合中 public List getNumList(int num, Supplier sup){    List list = new ArrayList<>(); ​    for (int i = 0; i < num; i++) {        Integer n = sup.get();        list.add(n);   } ​    return list; } ​ public void test2(){    List numList = getNumList(10, () -> (int)(Math.random() * 100)); ​    for (Integer num : numList) {        System.out.println(num);   } }

函数型接口 Function

接口的方法

R apply(T t);

参数是 T,返回值是 R,也就是 T 参数进入接口后,处理成 R 的类型并返回

// 用于处理字符串 public String strHandler(String str, Function fun){    return fun.apply(str); } ​ public void test3(){    String newStr = strHandler("\t\t\t 四大核心函数式接口   ", (str) -> str.trim());    System.out.println(newStr); ​    String subStr = strHandler("四大核心函数式接口", (str) -> str.substring(2, 5));    System.out.println(subStr); }

断言型接口 Predicate

接口的方法

boolean test(T t);

对 T 参数进行处理后,返回类型是 boolean 值

// 将满足条件的字符串,放入集合中 public List filterStr(List list, Predicate pre){    List strList = new ArrayList<>(); ​    for (String str : list) {        if(pre.test(str)){            strList.add(str);       }   } ​    return strList; } ​ public void test4(){    List list = Arrays.asList("Hello", "kele", "Lambda", "www", "ok");    List strList = filterStr(list, (s) -> s.length() > 3); ​    for (String str : strList) {        System.out.println(str);   } }

其他接口

| 函数式接口 | 参数类型 | 返回类型 | 用途 | | ----------------------------------- | ------ | ------ | --------------------------------------------------------- | | BiFunction | T,U | R | 对类型为 T,U 参数应用操作,返回 R 类型的 结果。包含方法为 R apply(T t, U u); | | UnaryOperator(Function 子接口) | T | T | 对类型为 T 的对象进行一元运算, 并返回 T 类型的结果。包含方法为 T apply(T t); | | BinaryOperator(BiFunction 子接口) | T,T | T | 对类型为 T 的对象进行二元运算,并返回 T 类型的结果。包含方法为 T apply(T t1, T t2); | | BiConsumer | T,U | void | 对类型为 T,U 参数应用操作。包含方法为 void accept(T t, U u) | | ToIntFunction | T | int | 计算 int 值的函数 | | ToLongFunction | T | long | 计算 long 值的函数 | | ToDoubleFunction | T | double | 计算 double 值的函数 | | IntFunction | int | R | 参数为 int 类型的函数 | | LongFunction | long | R | 参数为 long 类型的函数 | | DoubleFunction | double | R | 参数为 double 类型的函数 |

三大引用

方法引用

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

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

例 1(类::静态方法):

(x) -> System.out.println(x); // 等价于 System.out::println; ​ public void test1(){    Consumer con = (str) -> System.out.println(str);    con.accept("Hello World!"); // 等价于    Consumer con2 = System.out::println;    con2.accept("Hello Java8!"); } public void test4(){    Comparator com = (x, y) -> Integer.compare(x, y); // 等价于    Comparator com2 = Integer::compare; }

自动读取参数列表,放到 :: 后面的方法里。

例 2(对象::实例方法):

Binaryoperator bo = (x, y) -> Math.pow(x, y); // 等价于 Binaryoperator bo = Math::pow; ​ public void test2(){    Employee emp = new Employee(101, "张三", 18, 9999.99); ​    Supplier sup = () -> emp.getName();    System.out.println(sup.get()); // 等价于    Supplier sup2 = emp::getName;    System.out.println(sup2.get()); }

自动读取参数列表,放到 :: 后面的方法里。

例 3(类::实例方法):

compare((x, y) -> x.equals(y), "abcdef", "abcdef" ); // 等价于 compare(String::equals, "abcdef", "abcdef" ); ​ public void test5(){    BiPredicate bp = (x, y) -> x.equals(y);    System.out.println(bp.test("abcde", "abcde"));    // 等价于    BiPredicate bp2 = String::equals;    System.out.println(bp2.test("abcde", "abcde")); ​    System.out.println("-----------------------------------------"); ​    Function fun = (e) -> e.show();    System.out.println(fun.apply(new Employee())); // 等价于    Function fun2 = Employee::show;    System.out.println(fun2.apply(new Employee())); ​ }

:: 前面是类型,后面是实例方法,则自动读取参数列表,第一个参数放到 :: 的前面,其他参数放到 :: 后面的方法里。

注意:

构造器引用

格式:ClassName::new

与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致。

public void test7(){    Function fun = (n) -> new Employee(n);    // 等价于    Function fun = Employee :: new; }

数组引用

格式:ClassName[]::new

public void test8(){    Function fun = (args) -> new String[args];    String[] strs = fun.apply(10);    System.out.println(strs.length);    // 等价于    Function fun2 = Employee[] :: new;    Employee[] emps = fun2.apply(20);    System.out.println(emps.length); }

额外 - 遍历集合

利用引用来快速遍历 List、Set、Map 等集合的数据。

public void testCollection(){    //List:    List list = Arrays.asList(1, 2, 3);    list.forEach(System.out::println);        // Set:    Set set = new HashSet<>(Arrays.asList(1, 2, 3));    set.forEach(System.out::println); ​    // Map:    Map map = new HashMap<>();    map.put("Tom", 78);    map.put("Jerry", 88);    map.put("Tim", 68);        map.forEach((k,v) -> System.out.println(k + ":" + v)); }

Stream API

了解Stream

Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API(java.util.stream.*)

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。

使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

什么是Stream

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

注意:

操作三个步骤

image-20220209172921727

Stream创建

由集合创建流

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

public void test1(){    // Collection 提供了两个方法 stream() 与 parallelStream()    List list = new ArrayList<>();    Stream stream = list.stream(); // 获取一个顺序流    Stream parallelStream = list.parallelStream(); // 获取一个并行流 }

由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

重载形式,能够处理对应基本类型的数组:

public void test1(){   // 通过 Arrays 中的 stream() 获取一个数组流   Integer[] nums = new Integer[10];   Stream stream1 = Arrays.stream(nums);   }

由值创建流

可以使用静态方法 Stream.of(),通过显示值 创建一个流。它可以接收任意数量的参数

public void test1(){   // 通过 Stream 类中静态方法 of()   Stream stream2 = Stream.of(1,2,3,4,5,6); }

由函数创建流:创建无限流

可以使用静态方法 Stream.iterate()Stream.generate(),创建无限流

public void test1(){     // 创建无限流   // 迭代   Stream stream3 = Stream.iterate(0, (x) -> x + 2).limit(10); // 限制 10 个,否则无限创建   stream3.forEach(System.out::println);     // 生成   Stream stream4 = Stream.generate(Math::random).limit(2); // 限制 2 个   stream4.forEach(System.out::println); }

Stream中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为「惰性求值」。

筛选与切片

| 方法 | 描述 | | ------------------- | ------------------------------------------------------- | | filter(Predicate p) | 接收 Lambda,从流中排除某些元素 | | distinct() | 筛选,通过流所生成元素的 hashCode()equals() 去除重复元素 | | limit(long maxSize) | 截断流,使其元素不超过给定数量 | | skip(long n) | 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 |

代码示例:

public class TestStreamaAPI {    List emps = Arrays.asList(        new Employee(102, "李四", 59, 6666.66),        new Employee(101, "张三", 18, 9999.99),        new Employee(103, "王五", 28, 3333.33),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(105, "田七", 38, 5555.55)   ); // 测试 filter    public void testFilter(){        // 所有的中间操作不会做任何的处理        Stream stream = emps.stream()           .filter((e) -> {                System.out.println("测试中间操作");                return e.getAge() <= 35;           });        // 只有当做终止操作时,所有的中间操作会一次性的全部执行,称为「惰性求值」        stream.forEach(System.out::println);   } // 测试 limit    public void testLimit(){        emps.stream()           .filter((e) -> {                System.out.println("短路"); // && ||                return e.getSalary() >= 5000;           }).limit(3)           .forEach(System.out::println);   } // 测试 skip    public void testSkip(){        emps.parallelStream() // 并行流           .filter((e) -> e.getSalary() >= 5000)           .skip(2)           .forEach(System.out::println);   } // 测试 distinct    public void testDistinct(){        emps.stream()           .distinct()           .forEach(System.out::println);   } }

映射

| 方法 | 描述 | | ------------------------------- | -------------------------------------------- | | map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素 | | mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream | | mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream | | mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream | | flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |

代码示例:

public class TestStreamAPI1 { ​    List emps = Arrays.asList(        new Employee(102, "李四", 59, 6666.66),        new Employee(101, "张三", 18, 9999.99),        new Employee(103, "王五", 28, 3333.33),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(105, "田七", 38, 5555.55)   );    @Test    public void test1(){        Stream str = emps.stream()           .map((e) -> e.getName()); ​        System.out.println("-------------------------------------------"); ​        List strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee"); // 字母转大写        Stream stream = strList.stream()           .map(String::toUpperCase); ​        stream.forEach(System.out::println); ​        Stream> stream2 = strList.stream()           .map(TestStreamAPI1::filterCharacter); ​        stream2.forEach((sm) -> {            sm.forEach(System.out::println);       }); ​        System.out.println("---------------------------------------------"); ​        Stream stream3 = strList.stream()           .flatMap(TestStreamAPI1::filterCharacter); ​        stream3.forEach(System.out::println);   } ​    public static Stream filterCharacter(String str){        List list = new ArrayList<>(); ​        for (Character ch : str.toCharArray()) {            list.add(ch);       } ​        return list.stream();   } }

如何区分 mapflatMap?利用去重 distinct 来讲解

public void testMap(){    String[] words = new String[]{"Hello", "World"};    List a = Arrays.stream(words)       .map(word -> word.split(""))       .distinct()       .collect(Collectors.toList());    a.forEach(System.out::print); // 输出:[Ljava.lang.String;@5e9f23b4[Ljava.lang.String;@4783da3f } public void testFlatMap(){    String[] words = new String[]{"Hello", "World"};    List a = Arrays.stream(words)       .map(word -> word.split(""))       .flatMap(Arrays::stream)       .distinct()       .collect(Collectors.toList());    a.forEach(System.out::print); // 输出:HeloWrd }

数组使用 distinct 去重是比较数组不同下标的元素,如 Hello 和 World 比较,而不是 Hello 和 World 自己内部比较去重。

flatMap 是把 Hello 和 World 分成一个字符串,即一个字符串就是一个流,然后去重,是可以把 l 去掉,而 map 是不同下标元素比较。

使用 flatMap 方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用 map(Array::stream) 时生成的单个流被合并起来,即扁平化为一个流。

两个方法效果如图:

img

img

举例说明

有二箱鸡蛋,每箱 5 个,现在要把鸡蛋加工成煎蛋,然后分给学生。

map 做的事情:把二箱鸡蛋分别加工成煎蛋,还是放成原来的两箱,分给 2 组学生;

flatMap 做的事情:把二箱鸡蛋分别加工成煎蛋,然后放到一起【10 个煎蛋】,分给 10 个学生。

总结

数组去重复时,如果去重复的是不同下标的整体元素,则使用 map,如果去重复的是每个元素自己的内容,则使用 flatMap

排序

| 方法 | 描述 | | ----------------------- | ---------------------- | | sorted() | 产生一个新流,其中按自然顺序排序(从小到大) | | sorted(Comparator comp) | 产生一个新流,其中按比较器顺序排序 |

代码示例:

``` public class TestStreamAPI1 { ​    List emps = Arrays.asList(        new Employee(102, "李四", 59, 6666.66),        new Employee(101, "张三", 18, 9999.99),        new Employee(103, "王五", 28, 3333.33),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(104, "赵六", 8, 7777.77),        new Employee(105, "田七", 38, 5555.55)   );    @Test    public void test2(){ emps.stream() .map(Employee::getName) .sorted() .forEach(System.out::println);

System.out.println("------------------------------------");emps.stream()  .sorted((x, y) -> {    if(x.getAge() == y.getAge()){      return x.getName().compareTo(y.getName());    }else{      return Integer.compare(x.getAge(), y.getAge());    }  }).forEach(System.out::println);

} } ```

增量

| 方法 | 描述 | | ------------------------------------------------- | ------------------------------------------------------------------------- | | range(int startInclusive, int endExclusive) | 以 1 的增量步从 startInclusive 到 endExclusive 返回顺序的有序 IntStream,不包括endExclusive | | rangeClosed(int startInclusive, int endExclusive) | 以 1 的增量步从 startInclusive 到 endExclusive 返回顺序的有序 IntStream,包括endExclusive |

代码示例:

public class TestStreamAPI1 {    @Test    public void test3(){        IntStream.range(0, 10).forEach( i -> System.out.println(i)); ​   }    @Test    public void test4(){        IntStream.rangeClosed(0, 10).forEach( i -> System.out.println(i));   } }

test3 方法会输出 0 到 9,而 test4 方法会输出 0 到 10。

Stream终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void。

查找与匹配

| 方法 | 描述 | | ---------------------- | -------------------------------------------------------------------- | | allMatch(Predicate p) | 检查是否有匹配所有条件的元素 | | anyMatch(Predicate p) | 检查是否有至少匹配一个条件的元素 | | noneMatch(Predicate p) | 检查是否没有匹配所有条件的元素 | | findFirst() | 返回第一个元素 | | findAny() | 返回当前流中的任意元素 | | count() | 返回流中元素总数 | | max(Comparator c) | 返回流中最大值 | | min(Comparator c) | 返回流中最小值 | | forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代,即它帮你把迭代做了) |

代码示例:

``` public class TestStreamAPI2 {    // BUSY:忙碌,FREE:空闲,VOCATION:工作    List emps = Arrays.asList(        new Employee(102, "李四", 59, 6666.66, Status.BUSY),        new Employee(101, "张三", 18, 9999.99, Status.FREE),        new Employee(103, "王五", 28, 3333.33, Status.VOCATION),        new Employee(104, "赵六", 8, 7777.77, Status.BUSY),        new Employee(104, "赵六", 8, 7777.77, Status.FREE),        new Employee(104, "赵六", 8, 7777.77, Status.FREE),        new Employee(105, "田七", 38, 5555.55, Status.BUSY)   );    // 测试检查 match    @Test    public void test1(){        boolean bl = emps.stream()           .allMatch((e) -> e.getStatus().equals(Status.BUSY)); ​        System.out.println(bl); ​        boolean bl1 = emps.stream()           .anyMatch((e) -> e.getStatus().equals(Status.BUSY)); ​        System.out.println(bl1); ​        boolean bl2 = emps.stream()           .noneMatch((e) -> e.getStatus().equals(Status.BUSY)); ​        System.out.println(bl2);   }    // 测试 findFirst() 和 findAny()    @Test public void test2(){ Optional op = emps.stream() .sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())) .findFirst();

System.out.println(op.get());System.out.println("--------------------------------");Optional op2 = emps.parallelStream()  .filter((e) -> e.getStatus().equals(Status.FREE))  .findAny();System.out.println(op2.get());

}    // 测试 max,min    @Test    public void test3(){ long count = emps.stream() .filter((e) -> e.getStatus().equals(Status.FREE)) .count();

System.out.println(count);Optional op = emps.stream()  .map(Employee::getSalary)  .max(Double::compare);System.out.println(op.get());Optional op2 = emps.stream()  .min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));System.out.println(op2.get());

} } ```

注意:流进行了终止操作后,则该流不能再次使用。

如下的 max 返回无效,因为 count 已经终止操作。

public void test4(){    Stream stream = emps.stream()       .filter((e) -> e.getStatus().equals(Status.FREE)); ​    long count = stream.count(); ​    stream.map(Employee::getSalary)       .max(Double::compare); }

归约

| 方法 | 描述 | | -------------------------------- | ------------------------------------- | | reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T,初始值是 iden | | reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。 返回 Optional |

注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

代码示例:

``` public class TestStreamAPI3 {    List emps = Arrays.asList( new Employee(102, "李四", 79, 6666.66, Status.BUSY), new Employee(101, "张三", 18, 9999.99, Status.FREE), new Employee(103, "王五", 28, 3333.33, Status.VOCATION), new Employee(104, "赵六", 8, 7777.77, Status.BUSY), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(105, "田七", 38, 5555.55, Status.BUSY) );    @Test public void test1(){ List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Integer sum = list.stream()  .reduce(0, (x, y) -> x + y); // 0 是初始值

​ System.out.println(sum);

System.out.println("----------------------------------------");Optional op = emps.stream()  .map(Employee::getSalary)  .reduce(Double::sum);

​ System.out.println(op.get()); }     } ```

收集

| 方法 | 描述 | | -------------------- | --------------------------------------------------- | | collect(Collector c) | 将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法 |

Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

| 方法 | 返回类型 | 作用 | | ----------------- | --------------------- | --------------------------------------------------- | | toList | List | 把流中元素收集到 List | | toSet | Set | 把流中元素收集到 Set | | toCollection | Collection | 把流中元素收集到创建的集合 | | counting | Long | 计算流中元素的个数 | | summingInt | Integer | 对流中元素的整数属性求和 | | averagingInt | Double | 计算流中元素 Integer 属性的平均值 | | summarizingInt | IntSummaryStatistics | 收集流中 Integer 属性的统计值。如:平均值 | | joining | String | 连接流中每个字符串 | | maxBy | Optional | 根据比较器选择最大值 | | minBy | Optional | 根据比较器选择最小值 | | reducing | 归约产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中元素逐个结合,从而归约成单个值 | | collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果转换函数 | | groupingBy | Map > | 根据某属性值对流分组,属性为 K,结果为 T | | partitioningBy | Map > | 根据 true 或 false 进行分区 |

代码示例:

public class TestStreamAPI3 {    List emps = Arrays.asList(        new Employee(102, "李四", 79, 6666.66, Status.BUSY),        new Employee(101, "张三", 18, 9999.99, Status.FREE),        new Employee(103, "王五", 28, 3333.33, Status.VOCATION),        new Employee(104, "赵六", 8, 7777.77, Status.BUSY),        new Employee(104, "赵六", 8, 7777.77, Status.FREE),        new Employee(104, "赵六", 8, 7777.77, Status.FREE),        new Employee(105, "田七", 38, 5555.55, Status.BUSY)   );    @Test    public void test3(){        List list = emps.stream()           .map(Employee::getName)           .collect(Collectors.toList()); ​        list.forEach(System.out::println); ​        System.out.println("----------------------------------"); ​        Set set = emps.stream()           .map(Employee::getName)           .collect(Collectors.toSet()); ​        set.forEach(System.out::println); ​        System.out.println("----------------------------------"); ​        HashSet hs = emps.stream()           .map(Employee::getName)           .collect(Collectors.toCollection(HashSet::new)); ​        hs.forEach(System.out::println);   }    @Test    public void test4(){        Optional max = emps.stream()           .map(Employee::getSalary)           .collect(Collectors.maxBy(Double::compare)); ​        System.out.println(max.get()); ​        Optional op = emps.stream()           .collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))); ​        System.out.println(op.get()); ​        Double sum = emps.stream()           .collect(Collectors.summingDouble(Employee::getSalary)); ​        System.out.println(sum); ​        Double avg = emps.stream()           .collect(Collectors.averagingDouble(Employee::getSalary)); ​        System.out.println(avg); ​        Long count = emps.stream()           .collect(Collectors.counting()); ​        System.out.println(count); ​        System.out.println("--------------------------------------------"); ​        DoubleSummaryStatistics dss = emps.stream()           .collect(Collectors.summarizingDouble(Employee::getSalary)); ​        System.out.println(dss.getMax());   } ​    // 分组    @Test    public void test5(){        Map> map = emps.stream()           .collect(Collectors.groupingBy(Employee::getStatus)); ​        System.out.println(map);   } ​    // 多级分组    @Test    public void test6(){        Map>> map = emps.stream()           .collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e) -> {                if(e.getAge() >= 60)                    return "老年";                else if(e.getAge() >= 35)                    return "中年";                else                    return "成年";           }))); ​        System.out.println(map);   } ​    // 分区    @Test    public void test7(){        Map> map = emps.stream()           .collect(Collectors.partitioningBy((e) -> e.getSalary() >= 5000)); ​        System.out.println(map);   } ​    @Test    public void test8(){        String str = emps.stream()           .map(Employee::getName)           .collect(Collectors.joining("," , "----", "----")); ​        System.out.println(str);   } ​    @Test    public void test9(){        Optional sum = emps.stream()           .map(Employee::getSalary)           .collect(Collectors.reducing(Double::sum)); ​        System.out.println(sum.get());   } }

并行流与串行流

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让他们都忙起来。整个过程无需程序员显示实现优化。

Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel()sequential() 在并行流与顺序流之间进行切换。

了解Fork/Join框架

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

image-20220209212215740

Fork/Join框架与传统线程池的区别

采用 工作窃取模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从另外一个随机线程的队列中偷一个并把它放在自己的队列中。即自己空闲时,就去偷取别线程的任务来执行。

相对于一般的线程池实现,Fork/Join 框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在 Fork/Join 框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了线程的等待时间,提高了性能。

代码示例

Java8 已经优化了 Fork/Join 框架,因为 Java7 之前使用该框架代码会比较繁琐。

如下 Java7 前的 Fork/Join 框架简易代码,计算累加值:

public class ForkJoinCalculate extends RecursiveTask{ ​   private static final long serialVersionUID = 13475679780L;     private long start;   private long end;     private static final long THRESHOLD = 10000L; // 临界值     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(); // 累加成功后,再把子任务的结果合并在一起     }         } ​ }

计算出累加 10 亿的花费时间:

public class TestForkJoin {   // Fork/Join 框架花费时间   @Test   public void test1(){      long start = System.currentTimeMillis();            ForkJoinPool pool = new ForkJoinPool();      ForkJoinTask task = new ForkJoinCalculate(0L, 10000000000L);            long sum = pool.invoke(task);      System.out.println(sum);            long end = System.currentTimeMillis();            System.out.println("耗费的时间为: " + (end - start)); // 花费时间:113808   }   // 传统 for 循环花费时间   @Test   public void test2(){      long start = System.currentTimeMillis();            long sum = 0L;            for (long i = 0L; i <= 10000000000L; i++) {         sum += i;     }            System.out.println(sum);            long end = System.currentTimeMillis();            System.out.println("耗费的时间为: " + (end - start)); // 花费时间:31583   }     ​ }

而 Java8 之后,Fork/Join 框架的代码优化更加简洁,通过 parallel()sequential() 在并行流与顺序流之间进行切换。如下:

public class TestForkJoin {    @Test    public void test3(){        long start = System.currentTimeMillis(); ​        Long sum = LongStream.rangeClosed(0L, 10000000000L)           .parallel()  // 并行流           .sum(); ​        System.out.println(sum); ​        long end = System.currentTimeMillis(); ​        System.out.println("耗费的时间为: " + (end - start)); // 花费时间:18926   } }

Optional API

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

常用方法:

| 方法 | 作用 | | ------------------------ | ----------------------------------------------- | | Optional.of(T t) | 创建一个 Optional 实例 | | 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 |

代码示例

public class TestOptional { ​    @Test    public void test1(){        Optional op = Optional.of(new Employee());        Employee emp = op.get();        System.out.println(emp);   } ​    @Test    public void test2(){        Optional op = Optional.ofNullable(null);        System.out.println(op.get()); // 空指针 ​        Optional op = Optional.empty();        System.out.println(op.get()); // 空指针   } ​    @Test    public void test3(){        Optional op = Optional.ofNullable(new Employee()); ​        if(op.isPresent()){            System.out.println(op.get());       } ​        Employee emp = op.orElse(new Employee("张三"));        System.out.println(emp); ​        Employee emp2 = op.orElseGet(() -> new Employee());        System.out.println(emp2);   } ​    @Test    public void test4(){        Optional op = Optional.of(new Employee(101, "张三", 18, 9999.99)); ​        Optional op2 = op.map(Employee::getName);        System.out.println(op2.get()); ​        Optional op3 = op.flatMap((e) -> Optional.of(e.getName()));        System.out.println(op3.get());   } ​    @Test    public void test5(){        Man man = new Man(); ​        String name = getGodnessName(man);        System.out.println(name);   } ​    //需求:获取一个男人心中女神的名字    public String getGodnessName(Man man){        if(man != null){            Godness g = man.getGod(); ​            if(g != null){                return g.getName();           }       } ​        return "苍老师";   } ​    // 运用 Optional 的实体类    @Test    public void test6(){        Optional godness = Optional.ofNullable(new Godness("林志玲")); ​        Optional op = Optional.ofNullable(new NewMan(godness));        String name = getGodnessName2(op);        System.out.println(name);   } ​    public String getGodnessName2(Optional man){        return man.orElse(new NewMan())           .getGodness()           .orElse(new Godness("苍老师"))           .getName();   } }

模拟实际场景

每个男生心里都可能有一个女神,也可能没有,可以利用 Optional 来解决空指针异常。

首先创建没有利用 Optional,而是 if 判断 null 的男生实体类:

public class Man { ​    private Godness god; ​    public Man() {   } ​    public Man(Godness god) {        this.god = god;   } ​    public Godness getGod() {        return god;   } ​    public void setGod(Godness god) {        this.god = god;   } ​    @Override    public String toString() {        return "Man [god=" + god + "]";   } }

女神实体类(有名字):

public class Godness { ​    private String name; ​    public Godness() {   } ​    public Godness(String name) {        this.name = name;   } ​    public String getName() {        return name;   } ​    public void setName(String name) {        this.name = name;   } ​    @Override    public String toString() {        return "Godness [name=" + name + "]";   } }

模拟获取男生心里的女神名字:

public class TestOptional { ​    @Test    public void test5(){        Man man = new Man(); ​        String name = getGodnessName(man);        System.out.println(name);   } ​    // 需求:获取一个男人心中女神的名字    public String getGodnessName(Man man){        if(man != null){            Godness g = man.getGod();            if(g != null){                return g.getName();           }       }        return "冰糖"; // 默认值   } }

上面代码是我们习惯使用的多层 if 来解决 null 问题,那么如何利用 Optional 解决呢?

创建 Optional 的男生实体类:

``` // 注意:Optional 不能被序列化 public class NewMan { ​ private Optional godness = Optional.empty(); // 必须给个值,否则默认为 null

private Godness god;

public Optional getGod(){ return Optional.of(god); } ​ public NewMan() { } ​ public NewMan(Optional godness) { this.godness = godness; } ​ public Optional getGodness() { return godness; } ​ public void setGodness(Optional godness) { this.godness = godness; } ​ @Override public String toString() { return "NewMan [godness=" + godness + "]"; } } ```

模拟获取男生心里的女神名字:

public class TestOptional {    // 运用 Optional 的实体类    @Test    public void test6(){        Optional godness = Optional.ofNullable(new Godness("雪梨")); ​        Optional op = Optional.ofNullable(new NewMan(godness));        String name = getGodnessName2(op);        System.out.println(name);   } ​    public String getGodnessName2(Optional man){        return man.orElse(new NewMan())           .getGodness()           .orElse(new Godness("冰糖"))  // 默认值           .getName();   } }

接口中的默认方法与静态方法

接口中的默认方法

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

例如:

public interface MyFun { default String getName(){ return "哈哈哈"; } }

如果有一个类也定义了相同的方法,如:

public class MyClass {    public String getName(){ return "嘿嘿嘿"; } }

再来一个类同时继承和实现 MyClassMyFun,该使用谁呢?使用类 MyClass

public class SubClass extends MyClass implements MyFun {     } class Test {    public static void main(String[] args) { SubClass sc = new SubClass();        System.out.println(sc.getName()); // 输出:嘿嘿嘿 } }

或者再多出一个有相同方法的接口

``` public interface MyInterface {

default String getName(){ return "呵呵呵"; } } ```

SubClass 类同时实现两个接口,那么该使用哪个的方法呢?需要重写方法来决定使用哪个接口的方法

public class SubClass implements MyFun, MyInterface {    @Override public String getName() { return MyInterface.super.getName(); } }

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

若一个接口中定义了一个默认方法,而另外一个父类或接口中 又定义了一个同名的方法时

接口中的静态方法

Java8 中,接口中允许添加静态方法。

例如:

``` public interface MyInterface {

public static void show(){ System.out.println("接口中的静态方法"); } } ```

通过 MyInterface.show 调用即可。

新时间日期API

新时间日期API出现原因

为什么 Java 8 要出现新的时间日期 API,以前的 API 有什么问题吗?

当然有,那就是线程不安全问题,当多个线程同时调用一个日期 API 时,会报错。

举个例子:

public class TestSimpleDateFormat {     public static void main(String[] args) throws Exception {            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");            Callable task = new Callable() { ​         @Override         public Date call() throws Exception {            return sdf.parse("20161121");         }             }; ​      ExecutorService pool = Executors.newFixedThreadPool(10); // 创建 10 个线程            List> results = new ArrayList<>();            for (int i = 0; i < 10; i++) {         results.add(pool.submit(task));  // 执行线程,并将结果添加到 List 集合     }            for (Future future : results) {         System.out.println(future.get()); // 报错,因为传统时间 API 有线程不安全问题     }            pool.shutdown();   } }

上面代码创建 10 个线程,循环 10 次获取某个时间点,最终瞬间执行 100 次获取时间的操作,导致报错,因为传统时间 API 有线程不安全问题,那么 Java 8 以前如何解决呢?

利用 ThreadLocal 来加锁即可,如下:

public class TestSimpleDateFormat { ​    public static void main(String[] args) throws Exception { ​        // 解决多线程安全问题        Callable task = new Callable() { ​            @Override            public Date call() throws Exception {                return DateFormatThreadLocal.convert("20161121");           } ​       }; ​        ExecutorService pool = Executors.newFixedThreadPool(10); ​        List> results = new ArrayList<>(); ​        for (int i = 0; i < 10; i++) {            results.add(pool.submit(task));       } ​        for (Future future : results) {            System.out.println(future.get());       } ​        pool.shutdown();   } } // 加锁 class DateFormatThreadLocal { ​    private static final ThreadLocal df = new ThreadLocal(){ ​        protected DateFormat initialValue(){            return new SimpleDateFormat("yyyyMMdd");       } ​   }; ​    public static final Date convert(String source) throws ParseException{        return df.get().parse(source);   } ​ }

Java 8 之后如何解决这个问题呢?

利用三大核心类的 LocalDate 可以更加简洁的替换 ThreadLocal 来解决日期线程不安全问题。如下:

``` public class TestSimpleDateFormat {

public static void main(String[] args) throws Exception {

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");Callable task = new Callable() {

​ @Override public LocalDate call() throws Exception { LocalDate ld = LocalDate.parse("20161121", dtf); return ld; }

};

​ ExecutorService pool = Executors.newFixedThreadPool(10);

List> results = new ArrayList<>();for (int i = 0; i < 10; i++) {  results.add(pool.submit(task));}for (Future future : results) {  System.out.println(future.get());}pool.shutdown();

} ​ } ```

三大核心类

三大类 LocalDateLocalTimeLocalDateTime 类的实例是 不可变的对象(线程安全),分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。

| 方法 | 描述 | | ----------------------------------------------- | ----------------------------------------- | | now() | 静态方法,根据当前时间创建对象 | | of() | 静态方法,根据指定日期/时间创建对象 | | plusDays, plusWeeks, plusMonths, plusYears | 向当前 LocalDate 对象添加几天、几周、几个月、几年 | | minusDays、minusWeeks、minusMonths、minusYears | 从当前 LocalDate 对象减去几天、几周、几个月、几年 | | plus、minus | 添加或减少一个 Duration 或 Period | | withDayOfMonth、withDayOfYear、withMonth、withYear | 将月份天数、年份天数、月份、年份修改为指定的值并返回新的 LocalDate 对象 | | getDayOfMonth | 获得月份天数(1-31) | | getDayOfYear | 获得年份天数(1-366) | | getDayOfWeek | 获得星期几(返回一个 DayOfWeek 枚举值) | | getMonth | 获得月份,返回一个Month 枚举值 | | getMonthValue | 获得月份(1-12) | | getYear | 获得年份 | | until | 获得两个日期之间的 Period 对象,或者指定ChronoUnits 的数字 | | isBefore、isAfter | 比较两个 LocalDate | | isLeapYear | 判断是否是闰年 |

now() 示例:

LocalDate localDate = LocalDate.now(); LocalDate localDate = LocalTime.now(); LocalDateTime localDateTime= LocalDateTime.now();

of() 示例:

LocalDate localDate = LocalDate.of(2016, 10, 26); LocalTime localTime = LocalTime.of(02, 22, 56); LocalDateTime localDateTime = LocalDateTime.of(2016, 10,  26, 12, 10, 55);

演示三大类的常用 API

public class TestLocalDateTime { ​    // LocalDateTime:相当于 LocalDate + LocalTime    @Test    public void test1() {        LocalDateTime ldt = LocalDateTime.now();        System.out.println(ldt); ​        LocalDateTime ld2 = LocalDateTime.of(2016, 11, 21, 10, 10, 10); // 直接指定年月日,时分秒        System.out.println(ld2); ​        LocalDateTime ldt3 = ld2.plusYears(20); // 加 20 年        System.out.println(ldt3); ​        LocalDateTime ldt4 = ld2.minusMonths(2); // 减 2 个月        System.out.println(ldt4); ​        System.out.println(ldt.getYear()); // 获取年        System.out.println(ldt.getMonthValue()); // 获取月        System.out.println(ldt.getDayOfMonth()); // 获取日        System.out.println(ldt.getHour()); // 获取时        System.out.println(ldt.getMinute()); // 获取分        System.out.println(ldt.getSecond()); // 获取秒   } }

Instant时间戳

用于「时间戳」的运算。它是以 Unix 元年(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的描述进行运算。

代码示例:

public class TestLocalDateTime { ​    // Instant : 时间戳。(使用 Unix 元年 1970 年 1 月 1 日 00:00:00 所经历的毫秒值)    @Test    public void test2() {        Instant ins = Instant.now();  // 默认使用 UTC 时区,所以比中国时间少 8 小时        System.out.println(ins); ​        OffsetDateTime odt = ins.atOffset(ZoneOffset.ofHours(8)); // 加 8 小时:中国时区        System.out.println(odt); ​        System.out.println(ins.getNano()); // 获取纳秒 ​        Instant ins2 = Instant.ofEpochSecond(5); // 获取秒        System.out.println(ins2);   } }

Duration和Period

public class TestLocalDateTime { ​    // Duration : 用于计算两个「时间」间隔    // Period : 用于计算两个「日期」间隔    @Test    public void test3(){        Instant ins1 = Instant.now(); ​        try {            Thread.sleep(1000);       } catch (InterruptedException e) {       } ​        Instant ins2 = Instant.now(); ​        System.out.println("所耗费时间为:" + Duration.between(ins1, ins2)); // 计算时间 ​        System.out.println("----------------------------------"); ​        LocalDate ld1 = LocalDate.now();        LocalDate ld2 = LocalDate.of(2011, 1, 1); ​        Period pe = Period.between(ld2, ld1); // 计算日期        System.out.println(pe.getYears()); // 获取年        System.out.println(pe.getMonths()); // 获取月        System.out.println(pe.getDays()); // 获取日   } }

日期的操纵

代码示例:

public class TestLocalDateTime { ​    // TemporalAdjuster : 时间校正器    @Test    public void test4(){        LocalDateTime ldt = LocalDateTime.now();        System.out.println(ldt); ​        LocalDateTime ldt2 = ldt.withDayOfMonth(10); // 直接修改月份为 10        System.out.println(ldt2); ​        LocalDateTime ldt3 = ldt.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)); // 下周日        System.out.println(ldt3); ​        // 自定义:下一个工作日        LocalDateTime ldt5 = ldt.with((l) -> {            LocalDateTime ldt4 = (LocalDateTime) l; ​            DayOfWeek dow = ldt4.getDayOfWeek(); // 获取日对象 ​            if(dow.equals(DayOfWeek.FRIDAY)){ // 如果是周五                return ldt4.plusDays(3); // 加 3 天就是周一           }else if(dow.equals(DayOfWeek.SATURDAY)){   // 如果是周六                return ldt4.plusDays(2); // 加 2 天就是周一           }else{ // 如果是周日                return ldt4.plusDays(1); // 加 1 天就是周一           }       });        System.out.println(ldt5);   } }

解析与格式化

java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:

public class TestLocalDateTime { ​    // DateTimeFormatter : 解析和格式化日期或时间    @Test    public void test5(){        // DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE; // 默认标准格式 ​        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E"); // 自定义格式 ​        LocalDateTime ldt = LocalDateTime.now();        String strDate = ldt.format(dtf); ​        System.out.println(strDate); ​        LocalDateTime newLdt = ldt.parse(strDate, dtf); // 转换为 LocalDateTime 标准格式        System.out.println(newLdt);   } }

时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDateZonedTimeZonedDateTime

其中每个时区都对应着 ID,地区 ID 都为「{区域}/{城市}」的格式 例如:Asia/Shanghai 等

代码示例:

public class TestLocalDateTime { ​    // ZonedDate、ZonedTime、ZonedDateTime :带时区的时间或日期    @Test    public void test6(){        Set set = ZoneId.getAvailableZoneIds(); // 获取所有时区信息        set.forEach(System.out::println);   } ​    @Test    public void test7(){        LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai")); // 指定时区        System.out.println(ldt); ​        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("US/Pacific")); // 指定时区        System.out.println(zdt);   } }

与传统日期处理的转换

| 类 | 传统方法 | 转换方法 | | ------------------------------------------------------------- | -------------------------------------- | --------------------------- | | java.time.Instant VS java.util.Date | Date.from(instant) | date.toInstant() | | java.time.Instant VS java.sql.Timestamp | Timestamp.from(instant) | timestamp.toInstant() | | java.time.ZonedDateTime VS java.util.GregorianCalendar | GregorianCalendar.from(zonedDateTim e) | cal.toZonedDateTime() | | java.time.LocalDate VS java.sql.Time | Date.valueOf(localDate) | date.toLocalDate() | | java.time.LocalTime VS java.sql.Time | Date.valueOf(localDate) | date.toLocalTime() | | java.time.LocalDateTime VS java.sql.Timestamp | Timestamp.valueOf(localDateTime) | timestamp.toLocalDateTime() | | java.time.ZoneId VS java.util.TimeZone | Timezone.getTimeZone(id) | timeZone.toZoneId() | | java.time.format.DateTimeFormatterVS java.text.DateFormat | formatter.toFormat() | 无 |

Try升级

Java 8 之前,我们写的 try-catch 结尾都要写 finally 来关闭资源,则 Java 8 升级了 try-catch 的用法,直接在 try 加个括号,里面填写对象的实例化即可自动关闭资源,对比代码如下:

// Java 8 之前 public void testTry1(){    InputStreamReader reader = null;    try{        reader  = new InputStreamReader(System.in);        //读取数据的过程:略        reader.read();   }catch (IOException e){        e.printStackTrace();   }finally{        //资源的关闭操作        if(reader != null){            try {                reader.close();           } catch (IOException e) {                e.printStackTrace();           }       }   } ​ } // Java 8 public void testTry2(){    try(InputStreamReader reader = new InputStreamReader(System.in)){        // 读取数据的过程:略        reader.read();   }catch(IOException e){        e.printStackTrace();   } }

注意:java 8 中要求资源对象的实例化,必须放在 try 的一对 () 内完成。

Java 9 又进行了升级,对象的实例化可以放在外面,try 的一对 () 传入对象即可,如下:

public void testTry3(){    InputStreamReader reader = new InputStreamReader(System.in);    try(reader){        //读取数据的过程:略        reader.read();   }catch(IOException e){        e.printStackTrace();   } }

重复注解与类型注解

Java 8 对注解处理提供了两点改进:

代码示例

创建一个数组注解,用于开启可重复的注解:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.RUNTIME) publie @interface MyAnnotations {    MyAnnotation[] value(); }

创建可重复的注解类:

@Repeatabie(MyAnnotations.class) // 开启使用重复注解 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation {    String value(); }

测试使用重复注解:

public class TestAnnotations {    @MyAnnotation("Hello")    @MyAnnotation("World")    public void show() {   } }

可用于类型的注解,即在参数或者属性上可以加入注解,类似于 Spring Boot 的 @RequestParam()

public class TestAnnotations {    @MyAnnotation("Hello")    @MyAnnotation("World")    public void show(@MyAnnotation("kele") String str) {   } }

来源地址:https://blog.csdn.net/zhiqi_l163991102/article/details/131414769

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯