JDK8新特性知识点总结
一个简洁的博客网站:http://lss-coding.top,欢迎大家来访
学习娱乐导航页:http://miss123.top/
1. Open JDK 和 Oracle JDK
Java 由 Sun 公司发明,Open JDK 是 Sun 在2006年末把 Java 开源而形成的项目。也就是说 Open JDK 是 JavaSE 平台版的开源和免费实现,它由 SUN 和 Java 社区提供支持,2009年 Oracle 收购了 Sun 公司,自此 Java 的维护方之一的 Sun 也变成了 Oracle。
**关系:**大多数 JDK 都是在 Open JDK 的基础上进一步编写完成的,比如 IBM J9,Oracle JDK 和 Azul Zulu,Azul Zing。Oracle JDK 完全由 Oracle 公司开发,Oracle JDK 是基于 Open JDK 源代码的商业版本。此外,它包含闭源组件。Oracle JDK 根据二进制代码许可协议获得许可,在没有商业许可的情况下,在 2019年1月之后发布的 Oracle Java SE8 的公开更新将无法用于商业或生产用途。但是 Open JDK 是完全开源的,可以自由使用。
Open JDK 官网:http://openjdk.java.net/
2. Lambda 表达式
2.1 分析
public static void main(String[] args) {// 创建一个线程,执行一段代码new Thread(new Runnable() {@Overridepublic void run() {System.out.println("子线程执行:" + Thread.currentThread());}}).start();System.out.println("主线程执行:" + Thread.currentThread());
}
代码分析:
- Thread 类需要一个 Runnable 接口作为参数,其中的抽象方法 run 方法是用来执行线程任务的核心
- 为了执行 run() 方法,不得不需要 Runnable 的实现类
- 为省去定义 Runnable 实现类,不得不使用匿名内部类
- 必须覆盖重写抽象的 run 方法,所有的方法名称、参数、返回值不得不重写一遍
- 实际上,只在乎方法体的代码
2.2 使用
Lambda 表达式是一个匿名内部类,可以理解为一段可以传递的代码
new Thread(() -> {System.out.println("Lambda执行线程:" + Thread.currentThread());
}).start();
优点:简化匿名内部类使用,语法更加简单。
匿名内部类语法冗余,Lambda表达式是简化匿名内部类的一种方式。
2.3 语法规则
省去了面向对象的一些约束
(参数类型 参数名称,...) -> {方法体}
2.4 练习1
无参 Lambda 表达式
- 首先创建一个 UserService 方法
public interface UserService {void show();
}
- 调用该方法
public class Demo02 {public static void main(String[] args) {getShow(() -> {System.out.println("调用了UserService接口");});}public static void getShow(UserService userService) {userService.show();}
}
2.5 练习2
有参 Lambda 表达式
- 创建一个 User 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {private String name;private Integer age;
}
- 在主类中创建三个对象加入到 list 集合中
- 不适用 lambda 进行排序
public class Demo03 {public static void main(String[] args) {List<User> list = new ArrayList<>();list.add(new User("张三", 22));list.add(new User("李四", 31));list.add(new User("王五", 11));Collections.sort(list, new Comparator<User>() {@Overridepublic int compare(User o1, User o2) {return o1.getAge() - o2.getAge();}});list.forEach(System.out::println);}
}
- 可以看到上面 sort 方法中的 new Comparator… 是一个匿名内部类,可以通过 lambda 进行转换
Collections.sort(list, (User o1, User o2) -> {return o1.getAge() - o2.getAge();
});
2.6 @FunctionalInterface 注解
在我们的接口中只能有一个方法,否则在调用的时候会报错,使用该注解标识我们的接口为函数时接口,并且该接口中只能有一个抽象方法(方法)
/*** 标志注解,被该注解修饰的接口只能声明一个抽象方法**/
@FunctionalInterface
public interface UserService {void show();}
2.7 原理
- 匿名内部类的本质就是在编译的时候生成一个 Class 文件,类名$1.class
// 该代码执行完成之后
// 创建一个线程,执行一段代码
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("子线程执行:" + Thread.currentThread());}
}).start();
通过反编译可以看到
package com.lss.jdk8NewFeatures.lambda;final class Demo01$1 implements Runnable {Demo01$1() {}public void run() {System.out.println("子线程执行:" + Thread.currentThread());}
}
- Lambda 表达式
写有 Lambda 表达式的 class 文件不能反编译工具 Xjad进行查看,可以通过 JDK 自带的工具:javap 对字节码进行反汇编操作。
javap -c -p 文件名.class-c:表示对代码进行反汇编
-p:显示所有类和成员
在反编译的源码中看到了一个静态方法 lambda$main$0(),在代码体其实就是调用这个方法
匿名内部类在编译的收会产生一个 class 文件
Lambda 表达式在程序运行的时候会形成一个类
- 在类中新增一个方法,这个方法的方法体就是 Lambda 表达式中的代码
- 还会生成一个匿名内部类,实现接口,重写抽象方法
- 在接口中重写方法会调用新生成的方法
2.8 省略写法
- 小括号内的参数类型可以省略
- 如果小括号里面有且只有一个参数,小括号可以省略
- 如果花括号中有且只有一个语句,可以省略花括号,return 关键字以及语句分号。
getShow(() -> System.out.println("调用了UserService接口"));
getStudent( name -> System.out.println(name));
Lambda 使用要求:方法的参数或局部变量必须为接口才能使用 Lambda、接口中有且仅有一个抽象方法(@FunctionalInteface)
Lambda 和 匿名内部类对比
- 类型不一样
- 匿名内部类的类型可以是类、抽象类、接口
- Lambda表达式需要的类型必须是接口
- 方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的
- Lambda 表达式所需的接口中只能有一个抽象方法
- 实现原理不一样
- 匿名内部类是在编译后生成一个 Class文件
- Lambda 表达式是在程序运行的时候动态生成 Class 文件
3. 接口中新增的方法
3.1 接口新增
在 JDK8 中对接口进行了增强,接口中可以有默认方法和静态方法
interface 接口名{静态常量;抽象方法;默认方法;静态方法;
}
3.2 默认方法
3.2.1 为什么要新增默认方法
在 JDK8 以前的接口中只能由抽象方法和静态常量,会出现一下问题:
如果接口中添加了一个抽象方法,那么它所有的实现类都需要重写这个抽象方法,非常地不利于接口的扩展。
3.2.2 使用
接口中默认方法的语法格式:
interface 接口名{修饰符 default 返回值类型 方法名{方法体;}
}
public class Demo01 {public static void main(String[] args) {A b = new B();System.out.println(b.show2());A c = new C();System.out.println(c.show2());}
}interface A {void show();// 定义一个默认方法public default String show2() {return "执行了A接口中的默认方法";}}class B implements A {@Overridepublic void show() {}
}class C implements A {@Overridepublic void show() {}// 重写默认方法@Overridepublic String show2() {return "在C类中的重写A接口的默认方法";}
}
执行了A接口中的默认方法
在C类中的重写A接口的默认方法
接口中的默认方法有两种使用方式:
- 实现类直接调用接口的默认方法
- 实现类重写接口的默认方法
3.3 静态方法
接口中的静态方法就是在接口中定义一个抽象方法通过static 关键字进行修饰,该静态方法不能被实现类重写,只能通过接口名.方法名()调用
public class Demo01 {public static void main(String[] args) {A b = new B();System.out.println(b.show2());A c = new C();System.out.println(c.show2());// 调用接口中的静态方法A.show3();}
}interface A {void show();// 定义一个默认方法public default String show2() {return "执行了A接口中的默认方法";}// 定义一个静态方法public static void show3() {System.out.println("执行了接口中的静态方法");}}
- 默认方法通过实例调用,静态方法通过接口名调用
- 默认方法可以继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
- 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用
4. 函数式接口
4.1 函数式接口由来
使用 Lambda 表达式的前提是需要有函数式接口,而 Lambda 表达式使用时不关心接口名,抽象方法名。只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 表达式更加的方便,在 JDK 中提供了大量常用的函数式接口
public class Demo01 {public static void main(String[] args) {fun1((arr -> {int sum = 0;for (int i : arr) {sum += i;}return sum;}));}public static void fun1(Operator operator) {int[] arr = {1,2,3,4};int sum = operator.getSum(arr);System.out.println("sum = " + sum);}
}
@FunctionalInterface
interface Operator{int getSum(int[] arr);
}
4.2 函数式接口介绍
在 java.util.function 包中定义了一些函数式接口,在之后的使用中不一定需要每次都定义一个接口
4.2.1 Supplier
无参有返回值
@FunctionalInterface
public interface Supplier<T> {/*** Gets a result.** @return a result*/T get();
}
/*** Supplier 函数式接口的使用*/
public class Demo02Supplier {public static void main(String[] args) {fun1(() -> {int arr[] = {11,2,33,5,53,99,2};// 计算出数组中的最大值Arrays.sort(arr);return arr[arr.length - 1];});}public static void fun1(Supplier<Integer> supplier) {Integer integer = supplier.get();System.out.println("max = " + integer);}
}
4.2.2 Consumer
有参无返回值
@FunctionalInterface
public interface Consumer<T> {/*** Performs this operation on the given argument.** @param t the input argument*/void accept(T t);
}
/*** Consumer 函数式接口使用*/
public class Demo03Consumer {public static void main(String[] args) {fun1(s -> {System.out.println(s.toLowerCase());});}public static void fun1(Consumer<String> consumer) {consumer.accept("Hello, World");}}
4.2.3 Function
有参有返回值,根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
@FunctionalInterface
public interface Function<T, R> {/*** Applies this function to the given argument.** @param t the function argument* @return the function result*/R apply(T t);
}
public class Demo04Function {public static void main(String[] args) {test(msg -> {return Integer.parseInt("111111");});}public static void test(Function<String,Integer> function) {Integer apply = function.apply("123");System.out.println("apply = " + apply);}}
在源码中可以看到有一个 andThen 方法,做组合使用
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {Objects.requireNonNull(after);return (T t) -> after.apply(apply(t));
}
public class Demo04Function2 {public static void main(String[] args) {test(msg -> {return Integer.parseInt(msg);}, msg -> {return msg * 10;});}public static void test(Function<String,Integer> f1, Function<Integer, Integer> f2) {// Integer apply = f1.apply("123");
// Integer apply1 = f2.apply(apply);Integer apply = f1.andThen(f2).apply("123");System.out.println("i2: " + apply);}}
4.2.4 Predicate
有参且返回结果为 boolean
@FunctionalInterface
public interface Predicate<T> {/*** Evaluates this predicate on the given argument.** @param t the input argument* @return {@code true} if the input argument matches the predicate,* otherwise {@code false}*/boolean test(T t);
}
/*** 根据输入的值进行判断,返回结果 boolean*/
public class Demo05Predicate {public static void main(String[] args) {test(msg -> {return msg.length() > 0;}, "Hello");}public static void test(Predicate<String> predicate, String msg){boolean b = predicate.test(msg);System.out.println("b: " + b);}
}
在 Predicate 中的默认方法提供了逻辑关系操作,negate、and、or等
public class Demo05Predicate1 {public static void main(String[] args) {test(msg -> {return msg.contains("H");}, msg -> {return msg.contains("W");});}public static void test(Predicate<String> p1, Predicate<String> p2){// p1 包含 H,同时 p2 包含Wboolean r1 = p1.and(p2).test("Hello");// p1 包含 H,或者 P2 包含Wboolean r2 = p1.or(p2).test("Hello");// p1 不包含Hboolean r3 = p1.negate().test("Hello");System.out.println(r1);System.out.println(r2);System.out.println(r3);}
}
5. 方法的引用
5.1 使用
在 Lambda 表达式中要执行的代码和我们另一个方法的代码是一样的,这样就没有必要重新写,可以”引用“重复代码
public class Demo01 {public static void main(String[] args) {// :: 方法引用,JDK8新特性printSum(Demo01::getTotal);}// 求数组元素之和public static void getTotal(int a[]) {int sum = 0;for (int i : a) {sum += i;}System.out.println("数组之和:" + sum);}public static void printSum(Consumer<int[]> consumer) {int[] a = {10, 20, 30, 40, 50, 60};consumer.accept(a);}}
5.2 方法引用格式
符号:::
双冒号为方法引用的运算符,它所在的表达式被称为方法引用
**应用场景:**如果 Lambda 表达式所要实现的方案,已经有其他方法存在相同的方案,那么可以使用方法应用。
常见的引用方式:
- instanceName::methodName 对象名::方法名
- ClassName::staticMethodName 类名::静态方法
- ClassName::methodName 类名::普通方法
- ClassName::new 类名:::new 调用的构造器
- TypeName[]::new String[]::new 调用数组的构造器
5.3 对象名::方法名
最常见用法,如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法
public static void main(String[] args) {Date date = new Date();Supplier<Long> supplier = () -> date.getTime();System.out.println(supplier.get());// 通过方法引用的方式来进行处理Supplier<Long> supplier1 = date::getTime;System.out.println(supplier1.get());
}
注意:被引用的方法参数要和接口中的抽象方法的参数一样;当接口抽象方法有返回值时,被引用的方法也必须有返回值。
5.4 类名::静态方法名
// public static native long currentTimeMillis();
public static void main(String[] args) {Supplier<Long> supplier = () -> {return System.currentTimeMillis();};System.out.println(supplier.get());// 通过方法引用实现Supplier<Long> supplier1 = System::currentTimeMillis;System.out.println(supplier1.get());
}
5.5 类名::引用实例方法
Java 面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
public static void main(String[] args) {Function<String, Integer> function = (s) -> {return s.length();};System.out.println(function.apply("hello"));// 通过方法引用实现Function<String, Integer> function1 = String::length;System.out.println(function1.apply("aaaaaaaaaaaaaaaaaaaa"));BiFunction<String, Integer, String> function2 = String::substring;String msg = function2.apply("HelloWorld", 3);System.out.println(msg);
}
5.6 类名::构造器
由于构造器名称与类名完全一致,所以构造器引用使用 ::new
的格式使用
public static void main(String[] args) {Supplier<User> supplier = () ->{ return new User(); };System.out.println(supplier.get());// 通过方法引用的方式Supplier<User> supplier1 = User::new;System.out.println(supplier1.get());BiFunction<String, Integer, User> function = User::new;System.out.println(function.apply("张三", 21));
}
5.7 数组::构造器
public static void main(String[] args) {Function<Integer, String[]> fun1 = (len) -> new String[len];String[] a1 = fun1.apply(3);System.out.println("数组长度为:" + a1.length);// 方法引用方式调用数组的构造器Function<Integer, String[]> fun2 = String[]::new;String[] a2 = fun2.apply(8);System.out.println("数组长度为:" + a2.length);
}
方法引用是对 Lambda 表达式符合特定情况下的一种缩写方式,它使得我们的 Lambda 表达式更加的简洁,也可以理解为 Lambda 表达式的缩写形式,不过需要注意的是方法引用只能引用已经存在的方法。
6. Stream API
6.1 集合处理的弊端
需要对一个集合进行查询的处理,则需要写好几个循环才能实现
- 不适用 Stream
public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("李四");list.add("李晓小");list.add("张三");// 1. 查询集合中姓名以 李 开头的List<String> list1 = new ArrayList<>(); // 记录开头为李的元素for (String s : list) {if (s.startsWith("李")) {list1.add(s);}}// 2. 在开头为李的元素中查找长度为3的List<String> list2 = new ArrayList<>(); // 记录长度为3的元素列表for (String s : list1) {if (s.length() == 3) {list2.add(s);}}// 3. 输出结果查看for (String s : list2) {System.out.println(s);}
}
- 使用Stream
public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("李四");list.add("李晓小");list.add("张三");list.stream() // 获取流.filter(s -> s.startsWith("李")) // 通过 Lambda 过滤掉开头不是李的元素.filter(s -> s.length() == 3) // 通过 Lambda 过滤掉长度不为3 的元素.forEach(System.out::println); // 遍历输出
}
6.2 Stream 流式思想概述
注意:Stream 和 IO 流是没有任何关系的
Stream 流式思想类似于工厂车间的”生产流水线“,Stream 流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream 可以看作是流水线的一个工序。在流水线上,通过多个工序让一个原料加工成一个商品。
Stream API 能够让我们快速完成许多复杂的操作:筛选、切片、映射、查找、去重、统计、匹配和归约等。
6.3 Stream 获取方式
6.3.1 根据 Collection
java.util.Collection 接口中加入了 default 方法 stream, 也就是说 Collection 接口下的所有的实现都可以通过 stream 方法来获取Stream流。
public static void main(String[] args) {List<String> list = new ArrayList<>();list.stream();Set<String> set = new HashSet<>();set.stream();Vector vector = new Vector();vector.stream();
}
注意:Map 接口没有实现 Collection 接口,可以通过 Map 的 key vlaue 集合获取
Map<String, Object> map = new HashMap<>();
Stream<String> stream = map.keySet().stream(); // key
Stream<Object> stream1 = map.values().stream(); // value
Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); // entry
6.3.2 根据 Stream 的 of 方法
开发中不可避免会需要进行对数据中数据的操作,由于数组对象不可能添加默认方法,所有 Stream 接口中提供了静态方法 of
public static void main(String[] args) {Stream<String> stringStream = Stream.of("1", "2", "3");int[] ints = {11,22,33};Stream<int[]> ints1 = Stream.of(ints);stringStream.forEach(System.out::println);// 基本数据类型的数组是不行的,输出的数组的地址 [I@5b6f7412ints1.forEach(System.out::println);
}
6.4 Stream 常用方法
常用 API,分为两种:终结、函数拼接
方法名 | 方法作用 | 返回值类型 | 方法种类 |
---|---|---|---|
count | 统计个数 | long | 终结 |
forEach | 逐一处理 | void | 终结 |
filter | 过滤 | Stream | 函数拼接 |
limit | 取用前几个 | Stream | 函数拼接 |
skip | 跳过前几个 | Stream | 函数拼接 |
map | 映射 | Stream | 函数拼接 |
concat | 组合 | Stream | 函数拼接 |
**终结方法:**返回值类型不再是 Stream 类型的方法,不再支持链式调用。
**非终结方法:**返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法)
注意:
- Stream 只能操作一次。
- Stream 方法返回的是新的流。
- Stream 不调用终结方法,中间步骤不会执行。
6.4.1 forEach
遍历流中的数据
void forEach(Consumer<? super T> action);
// 该方法接收一个 Consumer 接口,会将每一个流元素交给函数处理
public static void main(String[] args) {Stream<String> stringStream = Stream.of("1", "2", "3");stringStream.forEach(System.out::println);
}
6.4.2 count
统计流中元素的个数
long count();
public static void main(String[] args) {Stream<String> stringStream = Stream.of("1", "2", "3");long count = stringStream.count();System.out.println(count);
}
6.4.3 filter
过滤数据,返回符合条件的数据。从下图可以看出,将一个流转成成另一个子集流
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate 函数式接口参数,作为筛选条件。
public static void main(String[] args) {Stream<String> stringStream = Stream.of("a1", "a2", "a3");stringStream.filter(s -> s.startsWith("a")).forEach(System.out::println);
}
6.4.4 limit
可以对流进行截取处理,取前 n 个数据
Stream<T> limit(long maxSize);
public static void main(String[] args) {Stream<String> stringStream = Stream.of("a1", "a2", "a3","a4", "a5", "a6");Stream<String> limit = stringStream.limit(2);limit.forEach(System.out::println);
}
6.4.5 skip
如果希望跳过前面的几个元素,可以使用 skip 获取截取之后的新流。
public static void main(String[] args) {Stream<String> stringStream = Stream.of("a1", "a2", "a3","a4", "a5", "a6");stringStream.skip(3).forEach(System.out::println);
}
6.4.6 map
如果需要将流中的元素映射到另一个流中,可以使用 map 方法
<R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper)
该接口需要 Function 函数式接口参数,可以将当前的 T 类型数据转换成另一种类型数据。
public static void main(String[] args) {Stream.of("1", "2", "3","4", "5", "6")// .map(msg -> Integer.parseInt(msg)).map(Integer::parseInt).forEach(System.out::println);
}
6.4.7 sorted
如果需要将数据排序,可以使用该方法
Stream<T> sorted();
public static void main(String[] args) {Stream.of("5", "8", "2","4", "1", "0").map(Integer::parseInt)// .sorted() // 根据自然顺序排序.sorted((n1, n2 ) -> n2 - n1) // 通过比较器指定对应的排序规则.forEach(System.out::println);
}
6.4.8 distinct
如果要去掉流元素中的重复元素,可以使用该方法
Stream<T> distinct();
public static void main(String[] args) {Stream.of("5", "8", "2","4","0", "1", "0").distinct().forEach(System.out::println);
}
注意:Stream 流中的 distinct 方法对于基本数据类型是可以直接去重的,但是对于自定义类型,需要重写 hasCode 和 equals 方法来判断。
6.4.9 match
如果需要判断数据是否匹配指定的条件,可以使用该方法 match 是终结方法
// 元素是否有一个满足条件
boolean anyMatch(Predicate<? super T> predicate);// 元素是否都满足条件
boolean allMatch(Predicate<? super T> predicate);// 元素是否都不满足条件
boolean noneMatch(Predicate<? super T> predicate);
public static void main(String[] args) {boolean b = Stream.of("5", "8", "2", "4", "0", "1", "0").map(Integer::parseInt).allMatch(s -> s > 0); // 判断上面集合中的各个数字是否都大于 0 System.out.println(b);
}
6.4.10 find
如果需要找到某些元素或者数据,可以使用该方法实现
Optional<T> findFirst();Optional<T> findAny();
public static void main(String[] args) {Optional<Integer> first = Stream.of("5", "8", "2", "4", "1", "1", "1").map(Integer::parseInt).findFirst();System.out.println(first.get());
}
6.4.11 max 和 min
如果想要获取最大值和最小值则可以使用该方法
Optional<T> max(Comparator<? super T> comparator);Optional<T> min(Comparator<? super T> comparator);
public static void main(String[] args) {Optional<Integer> max = Stream.of("5", "8", "2", "4", "1", "1", "1").map(Integer::parseInt).max((o1, o2) -> o1 - o2);Optional<Integer> min = Stream.of("5", "8", "2", "4", "1", "1", "1").map(Integer::parseInt).min((o1, o2) -> o1 - o2);System.out.println("最大值:" + max.get());System.out.println("最小值:" + min.get());
}
6.4.12 reduce 方法
如果需要将所有数据归纳得到一个数据,可以使用该方法
T reduce(T identity, BinaryOperator<T> accumulator);
public static void main(String[] args) {Stream<Integer> integerStream = Stream.of(2, 4, 6, 1, 9, 4);// 第一次的时候会将默认值复制给 x// 之后每次会将上一次的操作结果复制给 x, y就是每次从数据中获取的元素Integer reduce = integerStream.reduce(0, (x, y) -> {System.out.println("x=" + x + ",y=" + y);return x + y;});System.out.println(reduce);
}
6.4.13 map 和 reduce 的组合
实际使用的时候都是 map 和 reduct 一块进行使用
public static void main(String[] args) {// 1. 求出列表中所有用户年龄的总和List<User> list = new ArrayList<>();list.add(new User("张三", 20));list.add(new User("李四", 20));list.add(new User("王五", 20));list.add(new User("高成", 20));Integer sum = list.stream().map(User::getAge) // 实现类型数据的转换.reduce(0,Integer::sum);System.out.println("所有年龄的总和为:" + sum);// 2. 求出所有年龄中的最大值List<User> list1 = new ArrayList<>();list.add(new User("张三", 20));list.add(new User("李四", 20));list.add(new User("王五", 20));list.add(new User("高成", 22));Integer sum1 = list.stream().map(User::getAge) // 实现类型数据的转换.reduce(0,Integer::max);System.out.println("所有年龄最大的为:" + sum1);// 3. 统计字符 a 出现的次数Integer reduce = Stream.of("a", "b", "c", "a", "b", "e", "a").map(ch -> "a".equals(ch) ? 1 : 0).reduce(0, Integer::sum);System.out.println("字母a出现的次数:" + reduce);
}
6.4.14 mapToInt
Integer 类型占用的内存会比 int 多得多,在 Stream 流操作中会自动进行装箱和拆箱操作, 为了提高代码效率可以先将流中的Integer转换成 int 在操作。
IntStream mapToInt(ToIntFunction<? super T> mapper);
public static void main(String[] args) {Integer arr[] = {1,2,3,4,5,6};Stream.of(arr).filter(i -> i > 4).forEach(System.out::println);System.out.println("-----------------------");Stream.of(arr).mapToInt(Integer::intValue).forEach(System.out::println);
}
6.4.15 concat
如果两个流希望合并称为一个流,那么可以使用 该方法
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {Objects.requireNonNull(a);Objects.requireNonNull(b);@SuppressWarnings("unchecked")Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>((Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());return stream.onClose(Streams.composedClose(a, b));
}
public static void main(String[] args) {Stream<String> stream1 = Stream.of("a","b","c");Stream<String> stream2 = Stream.of("x","y","z");Stream.concat(stream1, stream2).forEach(System.out::println);
}
6.4.16 案例练习
需求:
- 第一个队伍只保留姓名长度为 3 的成员
- 第一个筛选之后只要前 3 个
- 第二个队伍只要姓张的成员
- 第二个队伍筛选之后不要前两个人
- 将两个队伍合并为一个队伍
- 根据姓名创建 User 对象
- 打印整个队伍的User信息
public class Demo18 {public static void main(String[] args) {List<String> list1 = Arrays.asList("邓紫棋", "许巍", "郭富城", "周杰伦", "汪峰", "刘德华", "黄百鸣");List<String> list2 = Arrays.asList("赵磊", "王府井", "张云雷", "郭德纲", "张九南", "张艺兴", "张国荣");// Stream<String> stream1 = list1
// .stream()
// .filter(s -> s.length() == 3)
// .limit(3);
// System.out.println("----------------------------------");
// Stream<String> stream2 = list2
// .stream()
// .filter(s -> s.startsWith("张"))
// .skip(2);
// Stream.concat(stream1, stream2).map(n -> new User(n)).forEach(System.out::println);Stream.concat(list1.stream().filter(s -> s.length() == 3).limit(3), list2.stream().filter(s -> s.startsWith("张")).skip(2)).map(User::new).forEach(System.out::println);}}
6.5 Stream 结果收集
6.5.1 结果收集到集合中
@Test
public void test01() {// 将数据收集到 list中List<String> list = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toList());System.out.println(list);// 将数据收集到 set 中Set<String> set = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toSet());System.out.println(set);// 如果需要获取的类型为具体的实现,比如:ArrayList HashSetArrayList<String> arrayList = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toCollection(ArrayList::new));System.out.println(arrayList);// 收集到具体实现 HashSetHashSet<String> hashSet = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toCollection(HashSet::new));System.out.println(hashSet);
}
6.5.2 结果收集到数组中
Stream 中提供了 toArray 方法来将结果放到一个数组中,返回值类型是 Object[],如果我们要指定返回的类型,那么可以使用另一个重载的 toArray(IntFunction f) 方法。
@Test
public void test02() {Object[] objects = Stream.of("aa", "bb", "cc", "aa").toArray(); // 返回的数组中元素是 Object 类型System.out.println(Arrays.toString(objects));// 如果需要指定返回的类型String[] strings = Stream.of("aa", "bb", "cc", "aa").toArray(String[]::new);System.out.println(Arrays.toString(strings));
}
6.5.3 对流中的数据做聚合计算
当我们使用 Stream 流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如获得最大值、最小值、求和、平均值或者统计数量。
@Test
public void test03() {// 获取年龄的最大值Optional<User> maxAge = Stream.of(new User("张三", 12),new User("王五", 14),new User("张三", 12)).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));System.out.println("最大年龄:" + maxAge.get());// 获取年龄的最小值Optional<User> minAge = Stream.of(new User("张三", 12),new User("王五", 14),new User("张三", 12)).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));System.out.println("最小年龄:" + minAge.get());// 求所有年龄的总和Integer sumAge = Stream.of(new User("张三", 12),new User("王五", 14),new User("张三", 12)).collect(Collectors.summingInt(User::getAge));System.out.println("年龄总和:" + sumAge);// 求年龄平均值Double aveAge = Stream.of(new User("张三", 12),new User("王五", 14),new User("张三", 12)).collect(Collectors.averagingDouble(User::getAge));System.out.println("平均年龄为:" + aveAge);// 统计个数Long count = Stream.of(new User("张三", 12),new User("王五", 14),new User("张三", 12)).filter(u -> u.getAge() > 10).collect(Collectors.counting());System.out.println("个数:" + count);
}
6.5.4 对流中的数据做分组操作
当使用 Stream 处理数据后,可以根据某个属性将数据分组
@Test
public void test04() {// 根据姓名进行分组Map<String, List<User>> collect = Stream.of(new User("张三", 12),new User("李四", 14),new User("张三", 33),new User("张三", 12),new User("李四", 88),new User("张三", 12),new User("李四", 19)).collect(Collectors.groupingBy(User::getName));System.out.println("根据姓名分组");collect.forEach((k,v) -> System.out.println("k=" + k + ",v=" +v ));System.out.println("根据年龄分组");Map<String, List<User>> collect1 = Stream.of(new User("张三", 12),new User("李四", 14),new User("张三", 33),new User("张三", 12),new User("李四", 88),new User("张三", 12),new User("李四", 19)).collect(Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年"));collect1.forEach((k,v) -> System.out.println("k=" + k + ",v=" +v ));
}
- 多级分组,先根据 name 分组,再根据年龄分组
@Test
public void test05() {// 根据姓名进行分组Map<String, Map<String, List<User>>> collect = Stream.of(new User("张三", 12),new User("李四", 14),new User("张三", 33),new User("张三", 12),new User("李四", 88),new User("张三", 12),new User("李四", 19)).collect(Collectors.groupingBy(User::getName,Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年")));collect.forEach((k,v) -> {System.out.println(k);v.forEach((k1,v1) -> {System.out.println("k1=" + k1 + ",v1=" + v1);});});
}
6.6.5 对流中的数据做分区操作
Collectors.partitioningBy 会根据值是否为 true, 把集合中的数据分割为两个列表, 一个 true 列表,一个 false 列表
@Test
public void test06() {// 根据姓名进行分组Map<Boolean, List<User>> collect = Stream.of(new User("张三", 12),new User("李四", 14),new User("张三", 33),new User("张三", 12),new User("李四", 88),new User("张三", 12),new User("李四", 19)).collect(Collectors.partitioningBy(p -> p.getAge() > 18));collect.forEach((k,v) -> System.out.println("k=" + k + ",v=" + v));
}
6.6.6 对流中的数据进行拼接
Collectors.joining 会根据指定的连接符,将所有的元素连接成一个字符串。
@Test
public void test07() {// 根据姓名进行分组String collect = Stream.of(new User("张三", 12),new User("李四", 14),new User("张三", 33),new User("张三", 12),new User("李四", 88),new User("张三", 12),new User("李四", 19)).map(User::getName).collect(Collectors.joining("_", "###", "$$$"));System.out.println(collect);
}
6.6 并行 Stream 流
上面使用的 Stream 流都是串行的,在一个线程上执行。
// 串行流
@Test
public void test01() {Stream.of(3,2,5,2,1).filter( s -> {System.out.println(Thread.currentThread() + "--" + s);return s > 3;}).count();
}
并行流
parallelStream 其实就是一个并行执行的流,它通过默认的 ForkJoinpool, 可以提高读线程的执行速度。
// 获取并行流的两种方式
@Test
public void test02() {List<Integer> list = new ArrayList<>();// 通过 list 接口,直接获取并行流Stream<Integer> integerStream = list.parallelStream();// 将已有的串行流转换为并行流Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
}
@Test
public void test03() {Stream.of(1, 2, 3,6,2,3,6,9).parallel().filter(s -> {System.out.println(Thread.currentThread() + "s=" + s);return s > 2;}).count();
}
测试
Long times = 5000000000L;
private long start;
@Before
public void test05() {start = System.currentTimeMillis();
}
@After
public void test06() {long end = System.currentTimeMillis();System.out.println("共执行:" + (end - start) + "ms");
}
@Test
public void test07() {System.out.println("普通for循环");// 共执行:1999mslong sum = 0;for (long i = 0; i < times; i++) {sum += i;}
}
@Test
public void test08() {System.out.println("串行Stream流");// 共执行:2523msLongStream.range(0, times).reduce(0, Long::sum);
}
@Test
public void test09(){System.out.println("并行Stream流");// 共执行:1128msLongStream.range(0, times).parallel().reduce(0, Long::sum);
}
通过上面的测试,可以看到 parallelStream 的效率是最高的
Stream 并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作。
6.7 线程安全问题
// 并行流中的线程安全问题
@Test
public void test10(){List<Integer> list = new ArrayList<>();for (int i = 0; i < 1000; i++) {list.add(i);}System.out.println(list.size());List<Integer> listNew = new ArrayList<>();list.parallelStream().forEach(listNew::add);System.out.println(listNew.size());
}
要么执行成功,结果不对,要么会抛出异常。
针对问题解决方案:
- 加同步锁
list.parallelStream().forEach(s -> {synchronized (obj) {listNew.add(s);}
});
- 使用线程安全的容器
@Test
public void test12() {Vector v = new Vector();Object obj = new Object();IntStream.rangeClosed(1 , 1000).parallel().forEach(i -> {v.add(i);});System.out.println(v.size());
}
- 将线程不安全的容器转换成线程安全的容器
@Test
public void test13() {List<Integer> listNew = new ArrayList<>();// 将线程不安全的容器包装成线程安全的容器List<Integer> synchronizedList = Collections.synchronizedList(listNew);Object obj = new Object();IntStream.rangeClosed(1 , 1000).parallel().forEach(i -> {synchronizedList.add(i);});System.out.println(synchronizedList.size());
}
- 通过 Stream 中的 toArray 方法或者 collect 方法来操作,满足线程安全的要求
@Test
public void test14() {List<Integer> listNew = new ArrayList<>();List<Integer> collect = IntStream.rangeClosed(1, 1000).parallel().boxed().collect(Collectors.toList());System.out.println(collect.size());
}
6.8 Fork/Join 框架
parallelStream 使用的是 Fork/Join 框架。Fork/Join 框架自 JDK7 引入。Fork/Join 框架可以将一个大任务拆分为很多小任务来异步执行。Fork/Join 框架主要包含三个模块:
- 线程池:ForkJoinPool
- 任务对象:ForkJoinTask
- 执行任务的线程:ForkJoinWorkerThread
6.8.1 工作原理-分治法
分治法,ForkJoinPool 主要使用分治法来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量任务。比如要对 1000万个数据进行排序,那么会将这个任务分割成两个 500万的排序任务和一个针对这两组 500万数据的合并任务。以此类推,对于 500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于 10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概 20000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
6.8.2 工作原理-工作窃取法
Fork/Join 最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的 CPU,那么如何利用好这个空闲的 CPU 就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个 Fork/Join 框架的核心理念,Fork/Join工作窃取算法是指某个线程从其他队列里窃取任务来执行。
假如我们需要做一个比较大的任务,我们可以把这个人物分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如 A 线程负责处理 A 队列里的任务。但是有线程会先把自己队列里的任务干完,而其他线程对队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
上文中提到 Java8 引入了自动并行化的概念。它能够让一部分 Java 代码自动地以并行的方式执行,也就是我们使用了 ForkJoinPool 的 ParallelStream。
对于 ForkJoinPool 通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N(N为线程数量),来调整 ForkJoinPool 的线程数量,可以尝试调整成不同的参数来观察每次的输出结果。
7. Option类
主要解决空指针的问题
Option 是一个没有子类的工具类,Option 是一个可以为 null 的容器对象,它的主要作用就是为了避免 Null检查,防止 NullpointException
public final class Optional<T> {}
7.1 创建 Option 对象
public static void main(String[] args) {// 1. 通过 of 方法,of 方法是不支持 null的Optional<String> op1 = Optional.of("张三");Optional<String> op2 = Optional.of(null);// 报错// 2. 通过 ofNullable 方法,支持 nullOptional<String> op3 = Optional.ofNullable("张三");Optional<String> op4 = Optional.ofNullable(null);// 3. 通过 empty 方法直接创建一个空的 Optional 对象Optional<String> op5 = Optional.empty();
}
7.2 常用方法
/*** 常用方法* get():如果 Optional 有值则返回,否则抛出 NoSuchElementException 异常* get() 通常和 isPresent 方法一块使用* isPresent(): 判断是否包含值,包含则返回 true,否则返回 false* orElse(T t):如果调用对象包含值,返回该值,否则返回 t* orElseGet(Supplier s):如果调用对象包含值就返回该值,否则返回 Lambda 表达式的返回值*/
@Test
public void test() {Optional<String> op1 = Optional.of("张三");Optional<String> op2 = Optional.empty();// 获取 Optional 中的值if (op1.isPresent()) {String s1 = op1.get();System.out.println("用户名称:" + s1);}if (op2.isPresent()) {System.out.println(op2.get());} else {System.out.println("op2 是一个空的Optional对象");}String s3 = op1.orElse("李四");System.out.println(s3);String s4 = op2.orElse("王五");System.out.println(s4);String s5 = op2.orElseGet(() -> "没有值");System.out.println(s5);
}
@Test
public void test02() {Optional<String> op1 = Optional.of("张三");Optional<String> op2 = Optional.empty();// 如果存在值就做一些什么操作op1.ifPresent(s -> System.out.println("有值:" + s));
}
/*自定义方法,将 User 对象中的 name 转换为大写并返回*/
@Test
public void test03() {User user = new User("zhangsan", 88);Optional<User> op = Optional.of(user);String nameForObject = getNameForObject(op);System.out.println(nameForObject);
}
/*根据 User 对象,将 name 转换为大写并返回通过 Optional 方式实现*/
public String getNameForObject(Optional<User> op) {if (op.isPresent()) {String res = op.map(User::getName).map(String::toUpperCase).orElse("空值");return res;}return null;
}
// 原始写法
/*** 根据 User 对象,将name转换成大写并且返回*/
public String nameToUpper(User user) {if (user != null) {String name = user.getName();if (name != null) {return name;} else return "空值";} else {return "空值";}
}
8. 新时间日期 API
8.1 旧版问题
- 设计不合理,在 java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间的,而 java.sql.Date 仅仅包含日期,此外用于格式化和解析的类在 java.text 包下。
- 非线程安全,java.util.Date 是非线程安全的,所有的日期类都是可变的
- 时区处理麻烦,日期类并不提供国际化,没有时区支持
8.2 新版日期 API 介绍
JDK8 中增加了一套全新的日期时间 API,设计合理,线程安全。新的日期及时间API 位于 java.time 包中,下面是一些关键类
- LocalDate:表示日期,包含年月日,格式为 2019-10-16
- LocalTime:表示时间,包含时分秒格式为 16.38.54. 158549300
- LocalDateTime:表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
- DateTimeFormatter:日期时间格式化类
- Instant:时间戳,表示一个特定的时间瞬间
- Duration:用于计算 2 个时间(LocalTime,时分秒)的距离
- Period:用于计算 2 个日期(LocalDate,年月日)的距离
- ZonedDateTime:包含时区的时间
Java 中使用的历法是 ISO 8601 日历系统,它是世界民用历法,也就是我们常说的公历。平年有 365天,闰年有 366天。此外 Java8 还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中国民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
8.3 日期时间常用操作
- 对日期的操作
public static void main(String[] args) {// 创建指定日期LocalDate date1 = LocalDate.of(2022,4,1);System.out.println(date1);// 获得当前日期LocalDate now = LocalDate.now();System.out.println(now);// 获得日期中的相关信息System.out.println("年份:" + now.getYear());System.out.println("月份:" + now.getMonth().getValue());System.out.println("日:" + now.getDayOfMonth());System.out.println("星期:" + now.getDayOfWeek().getValue());
}
- 对时间的操作
@Test
public void test1() {// 获得指定的时间LocalTime time = LocalTime.of(6,30,00,00);System.out.println(time);// 获得当前的时间LocalTime time1 = LocalTime.now();System.out.println(time1);// 获得当前时间的相关信息System.out.println(time1.getHour());System.out.println(time1.getMinute());System.out.println(time1.getSecond());System.out.println(time1.getNano());
}
- 日期时间
@Test
public void test2() {// 获得指定的日期时间LocalDateTime dateTime = LocalDateTime.of(LocalDate.now(), LocalTime.now());System.out.println(dateTime);// 获得当前的日期时间LocalDateTime dateTime1 = LocalDateTime.now();System.out.println(dateTime1);
}
8.4 日期时间的修改
@Test
public void test3() {LocalDateTime dateTime = LocalDateTime.now();// 修改日期时间,对日期时间的修改,对已存在的 LocalDate 对象,创建了它的模板,并不会修改原来的信息LocalDateTime updDateTime = dateTime.withYear(1998);System.out.println("now: " + dateTime);System.out.println("upd: " + updDateTime);// 在当前日期的基础上,加上或者减去指定时间System.out.println("两天后:" + dateTime.plusDays(5));// 减去10年System.out.println("减去10年:" + dateTime.minusYears(10));
}// 日期时间的比较
@Test
public void test04() {LocalDateTime now = LocalDateTime.now();LocalDateTime date = LocalDateTime.of(2022,1,3,1,3,4);// 在 JDK8 中要实现日期的比较 isAfter、isBefore、isEqual 通过这几个方法来直接比较System.out.println(now.isAfter(date));System.out.println(now.isEqual(date));
}
注意:在进行日期时间的修改的时候,原来的 LocalDate 对象是不会被修改的,每次操作都是返回一个新的 LocalDate 对象,所以在多线程场景下是线程安全的。
8.5 格式化
@Test
public void test05() {LocalDateTime now = LocalDateTime.now();// 指定格式 使用系统默认的格式DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;// 将日期转换成字符String format = now.format(isoLocalDateTime);System.out.println("系统默认转换 = " + format);// 通过 ofPattern 方法指定特定的格式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String format1 = formatter.format(now);System.out.println("自定义格式 = " + format1);// 解析日期LocalDateTime parse = LocalDateTime.parse("1999-01-01 22:22:22", formatter);System.out.println("parse = " + parse);
}
8.6 计算日期时间差
JDK8 中提供了两个工具类 Duration/Period:计算日期时间差
- Duration:用来计算两个时间差(LocalTime)
- Period:用来计算两个日期差(LocalDate)
public static void main(String[] args) {// 计算时间差LocalTime now = LocalTime.now();LocalTime time = LocalTime.of(22, 40, 12);System.out.println("now = " + now);System.out.println("time = " + time);// 通过 Duration 计算时间差Duration duration = Duration.between(now, time);System.out.println(duration.toDays());System.out.println(duration.toHours());System.out.println(duration.toMinutes());System.out.println(duration.toMillis());
}
@Test
public void test01() {// 计算日期差LocalDate nowDate = LocalDate.now();LocalDate date = LocalDate.of(2001,03,20);Period period = Period.between(date, nowDate);System.out.println(period.getYears());System.out.println(period.getMonths());System.out.println(period.getDays());
}
8.7 时间校正器
有时候我们可能需要如下调整:将日期调整到 下个月的第一天 等操作。这是我们可以通过时间校正器获得更好的效果。
- TemporalAdjuster 时间校正器
- TemporalAdjusters 通过该类的静态方法提供了大量TemporalAdjuster的常用操作,简化操作
@Test
public void test02() {LocalDateTime now = LocalDateTime.now();// 将当前的日期调整到下个月的一号TemporalAdjuster adjuster = (temporal -> {LocalDateTime dateTime = (LocalDateTime) temporal;LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);System.out.println("nextMonth = " + nextMonth);return nextMonth;});// LocalDateTime nextMonth = now.with(adjuster);LocalDateTime nextMonth = now.with(TemporalAdjusters.firstDayOfNextMonth());System.out.println(nextMonth);
}
8.8 日期时间的时区
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime 是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID, ID 的格式为 “区域/城市”,例如:Asia/Shanghai 等。
ZonedId:该类中包含了所有的时区信息。
public static void main(String[] args) {// 获得所有时区idZoneId.getAvailableZoneIds().forEach(System.out::println);// 获得当前时间,中国时区,东八区时区,比标准时间早8小时LocalDateTime now = LocalDateTime.now();System.out.println("now = " + now);// 获得标准时间ZonedDateTime bzsj = ZonedDateTime.now(Clock.systemUTC());System.out.println("bzsj = " + bzsj);// 使用计算机默认时区,创建日期时间ZonedDateTime now1 = ZonedDateTime.now();System.out.println("now1 = " + now1);// 使用指定的时区创建日期时间ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));System.out.println("now2 = " + now2);
}
新日期API 优势
- 新版日期时间 API 中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例
- 提供不同的两种方式,有效的区分了人和机器的操作
- TemporalAdjuster 可以更精确的操作日期,还可以自定义日期调整器
- 线程安全
**学习参考视频:**https://www.bilibili.com/video/BV1HV411W78K?p=1
JDK8新特性知识点总结相关推荐
- JDK8新特性(五):JDK8时间日期API
本文目录: 前言 1.旧版日期时间API存在的问题 2.新日期时间 API 介绍 3.用法介绍 1.JDK8 日期和时间类 2.JDK8 日期时间格式化与解析 3.JDK8 Instant 类 4.J ...
- JDK8新特性之函数式接口
转载自 JDK8新特性之函数式接口 什么是函数式接口 先来看看传统的创建线程是怎么写的 Thread t1 = new Thread(new Runnable() {@Overridepublic v ...
- JDK8新特性之重复注解
转载自 JDK8新特性之重复注解 什么是重复注解 下面是JDK8中的重复注解( java.lang.annotation.Repeatable)定义的源码. @Documented @Retentio ...
- JDK8新特性之Optional
转载自 JDK8新特性之Optional Optional是什么 java.util.Optional Jdk8提供 Optional,一个可以包含null值的容器对象,可以用来代替xx != nul ...
- JDK8新特性之方法引用
转载自 JDK8新特性之方法引用 什么是方法引用 方法引用是只需要使用方法的名字,而具体调用交给函数式接口,需要和Lambda表达式配合使用. 如: List<String> list = ...
- JDK8新特性之Lambda表达式
转载自 JDK8新特性之Lambda表达式 什么是Lambda表达式 Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁.当开发者在编写Lambda表达式时,也会随之被编译成 ...
- Java番外篇2——jdk8新特性
Java番外篇2--jdk8新特性 1.Lambda 1.1.无参无返回值 public class Test {interface Print{void print();}public static ...
- 接口与抽象类区别和接口jdk8新特性
jdk8之前特点对比抽象类 1 接口用interface表示,和类是并列的,定义接口就是定义接口的成员 2 接口只能定义公共抽象方法(public abstract)和全局静态最终变量(public ...
- jdk8新特性之出现This inspection finds all usages of methods that have @since tag in their documentation.
今天在做jdk8新特性测试的时候出现了如上图的bug,提供两种解决办法. 解决思路一: 检查自己项目的JDK是不是JDK8,若不是请选择JDK8. 解决思路二: 打开自己的IDE ...
最新文章
- 大话卷积神经网络CNN,小白也能看懂的深度学习算法教程,全程干货建议收藏!...
- Android将联系人读取到LISTVIEW中遇到的问题!
- eureka服务注册yml配置
- Flask中的 url_for() 函数
- 假如王撕葱是程序员。。。
- 6-2-JSP基本语法
- 记一次海洋cms任意代码执行漏洞拿shell(url一句话)
- 16进制转base64_《蹲坑学K8S》之19-5:二进制部署Calico网络
- 2020美赛M奖感想
- 【有限差分法】(三)一维和二维抛物方程CN格式以及长时间稳定性分析(附算例与Python代码)
- 预约小程序开发:小程序开发的费用都包含了哪些?
- 关于QRCODE二维码使用彩色进行扩容的思考
- 在Java中为JFrame添加背景音乐
- 出版印刷纸张大小尺寸一览表
- JPBC库实现基于身份的签名体制
- 学校食堂简易点餐管理系统(含用户登录且密码隐藏)C++
- 一起来看流星雨剧情简介/剧情介绍/剧情分集介绍第二十一集
- Retrofit 通过刷新头部Token解决token过期
- 浅谈电信运营商BMO融合
- 几张产生视觉错觉的图片