目录
- 传送门
- 一、Lambda表达式
- 1、概念
- 2、语法
- 基本语法:
- Lambda简写:
- Lambda 表达式与匿名内部类区别:
- 3、案例
- 二、接口中新增方法
- 1、概念
- 2、默认方法
- 3、静态方法
- 三、函数式接口
- 1、概念
- 2、函数式接口的由来
- 3、常见的函数式接口
- 四、方法引用
- 1、概念
- Lambda表达式冗余案例
- 解决方案:方法引用案例
- 2、语法
- 3、案例
- 五、Steam API
- 1、概念
- 2、为什么使用Stream流
- 3、Stream流的原理
- 4、Stream流的获取方式
- 4.1、根据Collection获取
- 4.2、根据Stream获取
- 4.3、根据Arrays获取
- 5、Stream常见API
- 5.1、forEach
- 5.2、find/match
- 5.3、reduce
- 5.4、max/min/count
- 5.5、collect
- 5.6、filter/distinct
- 5.7、limit/skip
- 5.8、map
- 5.9、sorted
- 5.10、concat
- 6、Stream的串行和并行
- 6.1、Stream的串行
- 6.2、Stream的并行
- 6.3、串行流和并行流的对比
- 6.4、并行流线程安全问题
- 7、Fork/Join框架
- 7.1、Fork/Join原理-分治法
- 7.2、Fork/Join原理-工作窃取算法
- 7.3、Fork/Join案例
- 六、Optional类
- 1、概念
- 2、Optional的使用
- 2.1、Optional对象的创建
- 2.2、Optional常用方法
- 七、新时间日期API
- 1、旧版日期时间问题
- 2、新日期时间API
- 2.1、日期时间的常见操作
- 2.2、日期时间的修改和比较
- 2.3、日期时间的格式化和解析
- 2.4、Instant类
- 2.5、计算日期时间差
- 2.6、时间校正器
- 2.7、日期时间的时区
- 八、新增注解
- 1、重复注解
- 1.1、定义一个重复注解的容器
- 1.2、定义一个可以重复的注解
- 1.3、配置重复注解使用案例
- 2、类型注解
传送门
JDK8新特性
JDK9新特性
JDK10新特性
JDK11新特性
JDK12新特性
JDK13新特性
JDK14新特性
JDK15新特性
JDK16新特性
JDK17新特性
JDK18新特性
JDK19新特性
JDK20新特性
JDK21新特性
一、Lambda表达式
1、概念
Lambda表达式是一个匿名函数,很多语言在用,容许把函数作为参数传入方法中。
2、语法
基本语法:
<函数式接口> <变量名> = (参数列表) -> {
// 方法体
};
备注:
尖括号<>表示必选参数
大括号{}表示必选参数
方括号[]表示可选参数
竖线 | 用于分隔多个参数,含义为“或”
函数式接口:接口中只有一个抽象方法
(参数列表):抽象方法的参数
->:分隔符
{}:表示抽象方法的实现
Lambda简写:
可以根据上下文推断出参数类型,省略参数类型
如果形参列表为空,只需保留()
如果参数只有一个,小括号也可以省略
如果Lambda表达式体只有一句代码可以省略return和大括号{}
Lambda 表达式与匿名内部类区别:
匿名内部类可以为任意接口创建实例,不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法(代码块内调用接口其他默认方法不行的)。
匿名内部类会生成一个单独的class文件,Lambda 表达式不会。
3、案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
public class Jdk8NewFeatures {
public static void main(String[] args) {
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程:" + Thread.currentThread().getName());
}
}).start();
System.out.println("main线程:" + Thread.currentThread().getName());
// Lambda表达式
Runnable runnable = () -> {
System.out.println("子线程:" + Thread.currentThread().getName());
};
new Thread(runnable).start();
System.out.println("main线程:" + Thread.currentThread().getName());
// Lambda表达式
new Thread(() -> {
System.out.println("子线程:" + Thread.currentThread().getName());
}).start();
System.out.println("main线程:" + Thread.currentThread().getName());
}
}
二、接口中新增方法
1、概念
JDK8之前
interface 接口名{
静态常量;
抽象方法;
}
JDK8之后对接口做了语法增强,可以有默认方法和静态方法
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
2、默认方法
JDK8之前,接口新增抽象方法,那么所有实现类都必须要覆写这个抽象方法,非常不利于接口的扩展
interface 接口名{
[修饰符] default 返回值类型 方法名{
// 方法体;
}
}
接口实现类对象可以直接调用接口的默认方法
接口实现类也可以重写接口的默认方法
类和抽象类没有默认方法
默认方法可以被接口实现类继承
package java.util;
import java.util.function.UnaryOperator;
public interface List<E> extends Collection<E> {
// 默认方法
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
// 覆写父类默认方法
@Override
default Spliterator<E> spliterator() {
if (this instanceof RandomAccess) {
return new AbstractList.RandomAccessSpliterator<>(this);
} else {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
}
3、静态方法
JDK8之后新增的静态方法也是为了接口的扩展
interface 接口名{
[修饰符] static 返回值类型 方法名{
// 方法体;
}
}
接口实现类对象不能直接调用接口的静态方法,只能接口名.静态方法()方式调用
接口实现类不能重写接口的静态方法
类和抽象类可以有静态方法
静态方法不能被接口实现类继承
package java.util;
import java.util.function.UnaryOperator;
public interface List<E> extends Collection<E> {
// 静态方法
static <E> List<E> of() {
return (List<E>) ImmutableCollections.EMPTY_LIST;
}
}
三、函数式接口
1、概念
如果一个接口只有一个抽象方法,则该接口称之为函数式接口,函数式接口可以使用Lambda表达式,Lambda表达式会被匹配到这个抽象方法上。@FunctionalInterface注解检查接口是否符合函数式接口。
2、函数式接口的由来
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Operater o = arr -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
};
Integer sum = fun(o);
}
public static Integer fun(Operater operater) {// 普通方法
int[] arr = {1, 2, 3,4, 5, 6, 7,8};
return operater.getSum(arr);
}
@FunctionalInterface // 函数式接口
interface Operater {
// 求数组的和
public abstract Integer getSum(int[] arr);
}
}
分析:
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
3、常见的函数式接口
在JDK中帮我们提供的有大量常用函数式接口,主要是在java.util.function包中
最常用的主要有下面4个
结合常见的函数式接口,我们案例代码可以优化
第一次优化
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.function.Function;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Function<int[],Integer> function = arr -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
};
Integer sum = fun(function);
}
public static Integer fun(Function<int[],Integer> function) {// 普通方法
int[] arr = {1, 2, 3,4, 5, 6, 7,8};
return function.apply(arr);
}
//@FunctionalInterface // 函数式接口
//interface Operater {
// // 求数组的和
// public abstract Integer getSum(int[] arr);// 对应函数型接口Function<T,R>
//}
}
第二次优化
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.function.Function;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Integer sumNew = fun(arr -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
});
}
public static Integer fun(Function<int[],Integer> function) {// 普通方法
int[] arr = {1, 2, 3,4, 5, 6, 7,8};
return function.apply(arr);
}
}
四、方法引用
1、概念
方法引用是Lambda表达式的一种简写形式,如果Lambda表达式方法体中只是调用一个特定的已经存在的方法,则可以使用方法引用。
Lambda表达式冗余案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.function.Function;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Integer sumNew = fun(arr -> {// Lambda表达式与普通方法getSum功能冗余了
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
});
}
public static Integer fun(Function<int[],Integer> function) {// 普通方法
int[] arr = {1, 2, 3,4, 5, 6, 7,8};
return function.apply(arr);
}
public static Integer getSum(int[] arr){// 普通方法getSum
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
}
}
分析:
如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引 用”过去就好了:—方法引用。
解决方案:方法引用案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.function.Function;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Integer sumNew = fun(Jdk8NewFeatures::getSum);// 36
}
public static Integer fun(Function<int[],Integer> function) {// 普通方法
int[] arr = {1, 2, 3,4, 5, 6, 7,8};
return function.apply(arr);
}
public static Integer getSum(int[] arr){// 普通方法getSum
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println("数组的和为:" + sum);// 36
return sum;
}
}
2、语法
:: 方法引用是JDK8中新的语法
符号表示: ::
符号说明:双冒号为方法引用运算符,而它所在的表达式称为方法引用
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用
常见方式:
1. 对象::实例方法 2. 类::静态方法 3. 类::实例方法 4. 类::new
3、案例
对象::实例方法 案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.Date;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Date now = new Date();
Supplier<Long> supplier1 = ()->{return now.getTime();};// Lambda表达式所要实现的getTime()方案,Date类中已经存在getTime()的方案,可以使用方法引用
System.out.println(supplier1.get());// 1693993605985
// 对象::实例方法
Supplier<Long> supplier2 = now::getTime;// 被引用的方法,参数和返回值类型等要和上面supplier1方法体中的实现方法的一样
System.out.println(supplier2.get());// 1693993605985
}
}
类::静态方法 案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.Date;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
public static void main(String[] args) {
Supplier<Long> supplier1 = ()->{return System.currentTimeMillis();};
System.out.println(supplier1.get());// 1693994527150
// 类::静态方法
Supplier<Long> supplier2 = System::currentTimeMillis;
System.out.println(supplier2.get());// 1693994527151
}
}
类::实例方法 案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.Date;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
// java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者
public static void main(String[] args) {
Function<String,Integer> function1 = (s)->{
return s.length();
};
System.out.println(function1.apply("hello"));// 5
// 类::实例方法
Function<String,Integer> function2 = String::length;
System.out.println(function2.apply("hello"));// 5
// 类::实例方法
BiFunction<String,Integer,String> function3 = String::substring;
System.out.println(function3.apply("helloworld",5));// world
}
}
类::new 案例
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.Date;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
// 由于构造器的名称和类名完全一致,所以构造器引用使用::new的格式使用
public static void main(String[] args) {
Supplier<Date> supplier1 = ()->{return new Date();};
System.out.println(supplier1.get());// Wed Sep 06 18:21:15 CST 2023
// 类::new
Supplier<Date> supplier2 = Date::new;
System.out.println(supplier2.get());// Wed Sep 06 18:21:15 CST 2023
}
}
五、Steam API
1、概念
Stream 是Java8中处理集合的关键抽象概念,它可以对集合进行非常复杂的查找、过滤、筛选等操作。对集合的操作语法简洁,性能比传统快。
2、为什么使用Stream流
当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。我们来体验集合操作数据的弊端,需求如下:
一个List集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰,何线程
需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
传统写法
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
public static void main(String[] args) {
// 一个List集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰
// 需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// 1.拿到所有姓张的
ArrayList<String> zhangList = new ArrayList<>(); // {"张无忌", "张强", "张三丰"}
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
// 2.拿到名字长度为3个字的
ArrayList<String> threeList = new ArrayList<>(); // {"张无忌", "张三丰"}
for (String name : zhangList) {
if (name.length() == 3) {
threeList.add(name);
}
}
// 3.打印这些数据
for (String name : threeList) {
System.out.println(name);
}
}
}
分析:
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使用另一个循环从头开始。
那Stream能给我们带来怎样更加优雅的写法呢?
package com.ours.www.demo.utils;
import io.micrometer.observation.Observation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class Jdk8NewFeatures {
public static void main(String[] args) {
// 一个List集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰
// 需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// 1.拿到所有姓张的
// 2.拿到名字长度为3个字的
// 3.打印这些数据
list.stream()
.filter(s->s.startsWith("张"))
.filter(s->s.length() == 3)
.forEach(s-> System.out.println(s));
System.out.println("--------------------");
list.stream()
.filter(s->s.startsWith("张"))
.filter(s->s.length() == 3)
.forEach(System.out::println);// 方法引用
}
}
3、Stream流的原理
注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品
4、Stream流的获取方式
4.1、根据Collection获取
java.util.Collection 接口中加入了default方法stream,Collection接口下的所有实现都可以通过stream方法来获取Stream流。
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
注意:Map接口没有实现Collection接口,我们可以根据Map获取对应的key或者value的集合,再去获取Stream流。
4.2、根据Stream获取
Steam接口本身提供了of、iterate等方法来获取Stream流。
public interface Stream<T> extends BaseStream<T, Stream<T>> {
public static<T> Stream<T> of(T t) {
return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
}
4.3、根据Arrays获取
java.util.Arrays工具类中提供了stream方法来获取Stream流。
public class Arrays {
public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
}
}
5、Stream常见API
中间操作api: 一个操作的中间链,对数据源的数据进行操作。而这种操作的返回类型还是一个Stream对象。
终止操作api: 一个终止操作,执行中间操作链,并产生结果,返回类型不再是Stream流对象。
注意:Stream只能操作一次;Stream方法返回的是新的流;Stream不调用终止操作api,中间的操作不会执行;
5.1、forEach
void forEach(Consumer<? super T> action);
forEach方法用来遍历流中的数据,接受一个Consumer消费型接口参数,会将每一个流元素交给函数处理。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
list.stream().forEach(System.out::println);// 张无忌 周芷若 赵敏 张强 张三丰
5.2、find/match
Optional<T> findFirst();// 找到第一个元素
Optional<T> findAny();// 找到任意一个元素
boolean anyMatch(Predicate<? super T> predicate);// 元素是否有任意一个满足条件
boolean allMatch(Predicate<? super T> predicate);// 元素是否都满足条件
boolean noneMatch(Predicate<? super T> predicate);// 元素是否都不满足条件
find相关方法用来找到某些数据。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Optional<String> first = list.stream().findFirst();// 找到第一个元素
System.out.println(first.get());// 张无忌
Optional<String> any = list.stream().findAny();// 找到任意一个元素
System.out.println(any.get());// 张无忌
match相关方法用来判断数据是否匹配指定的条件。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
boolean anyMatch = list.stream().anyMatch(e -> e.startsWith("赵"));// 元素是否有任意一个满足条件
boolean allMatch = list.stream().allMatch(e -> e.startsWith("赵"));// 元素是否都满足条件
boolean noneMatch = list.stream().noneMatch(e -> e.startsWith("赵"));// 元素是否都不满足条件
System.out.println(anyMatch);// true
System.out.println(allMatch);// false
System.out.println(noneMatch);// false
5.3、reduce
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
reduce方法将所有数据归纳得到一个数据,主要用于集合求和、乘积、最值等操作。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
String reduce = list.stream().reduce("初始值",(x,y) ->{// identity是默认值,是x最先取的值;y是循环从list中取元素;x是每次接受return结果。
System.out.println("x="+x + ",y="+y);
//x=初始值,y=张无忌
//x=初始值-张无忌,y=周芷若
//x=初始值-张无忌-周芷若,y=赵敏
//x=初始值-张无忌-周芷若-赵敏,y=张强
//x=初始值-张无忌-周芷若-赵敏-张强,y=张三丰
return x + "-" +y;
});
System.out.println(reduce);// 初始值-张无忌-周芷若-赵敏-张强-张三丰
5.4、max/min/count
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
long count();
max方法用来找出流中最大的元素。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Optional<String> max = list.stream()
//.max(((o1, o2) -> {
// return o1.length() - o2.length();
//}));
.max(Comparator.comparing(String::length));
System.out.println(max.get());// 张无忌
min方法用来找出流中最小的元素。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Optional<String> min = list.stream()
//.min(((o1, o2) -> {
// return o1.length() - o2.length();
//}));
.min(Comparator.comparing(String::length));
System.out.println(min.get());// 赵敏
count方法用来统计流中的元素个数,返回一个long值。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
long count = list.stream().count();
System.out.println(count); // 5
5.5、collect
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
collect方法可以将数据收集到集合中,可以对数据进行聚合计算,可以对数据做分组操作,可以对数据做分区操作,可以对数据做拼接操作。
Collector<T, A, R> 顶层接口。
Collectors 顶层类只继承Object,和Collector接口没有直接关系,但是类中很多方法的返回是Collector接口。
collect将数据收集到集合中
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// collect将数据收集到集合中
List<String> collectList = list.stream().collect(Collectors.toList());// 转为list,和原集合顺序保持一致
System.out.println(collectList);// [张无忌, 周芷若, 赵敏, 张强, 张三丰]
Set<String> collectSet = list.stream().collect(Collectors.toSet());// 转为set,会去重,并且集合顺序会被打乱
System.out.println(collectSet);// [张强, 张三丰, 周芷若, 赵敏, 张无忌]
//ArrayList<String> collectArrayList = list.stream().collect(Collectors.toCollection(() -> new ArrayList<>()));// 转为具体的ArrayList
ArrayList<String> collectArrayList = list.stream().collect(Collectors.toCollection(ArrayList::new));// 转为具体的ArrayList
System.out.println(collectArrayList);// [张无忌, 周芷若, 赵敏, 张强, 张三丰]
HashSet<String> collectHashSet = list.stream().collect(Collectors.toCollection(HashSet::new));// 转为具体的HashSet
System.out.println(collectHashSet);// [张强, 张三丰, 周芷若, 赵敏, 张无忌]
collect对数据进行聚合计算
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// collect对数据进行聚合计算
Optional<String> collectMaxBy = list.stream().collect(Collectors.maxBy(Comparator.comparing(String::length)));// 获取长度最长的数据,类似max方法
System.out.println(collectMaxBy.get());// 张无忌
Optional<String> collectMinBy = list.stream().collect(Collectors.minBy(Comparator.comparing(String::length)));// 获取长度最短的数据,类似min方法
System.out.println(collectMinBy.get());// 赵敏
Long collectCounting = list.stream().collect(Collectors.counting());// 获取数据总数量,类似count方法
System.out.println(collectCounting);// 5
Integer collectSummingInt = list.stream().collect(Collectors.summingInt(String::length));// 获取数据总长度,求和
System.out.println(collectSummingInt);// 13
Double collectAveragingInt = list.stream().collect(Collectors.averagingInt(String::length));// 获取数据平均长度,求平均值
System.out.println(collectAveragingInt);// 2.6
collect对数据做分组操作
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// collect对数据做分组操作
Map<Integer,List<String>> collectGroupingBy = list.stream().collect(Collectors.groupingBy(String::length));// 按照长度将数据分组,分组
System.out.println(collectGroupingBy);// {2=[赵敏, 张强], 3=[张无忌, 周芷若, 张三丰]}
Map<String, Map<Integer, List<String>>> collectGroupingBy2 = list.stream().collect(Collectors.groupingBy(s -> s.startsWith("张")?"姓张":"不姓张", Collectors.groupingBy(String::length)));// 先按照姓张分组,再按照长度分组,多级分组
System.out.println(collectGroupingBy2);// {姓张={2=[张强], 3=[张无忌, 张三丰]}, 不姓张={2=[赵敏], 3=[周芷若]}}
collect对数据做分区操作
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// collect对数据做分区操作
Map<Boolean, List<String>> collectPartitioningBy = list.stream().collect(Collectors.partitioningBy(s -> s.startsWith("张")));// 按照姓张将数据分区,分区,分区是将原列表分割为了两个列表,一个ture列表,一个false列表;类似分组,只是Map的key是固定的ture和false
System.out.println(collectPartitioningBy);// {false=[周芷若, 赵敏], true=[张无忌, 张强, 张三丰]}
collect对数据做拼接操作
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
// collect对数据做拼接操作
String collectJoining = list.stream().collect(Collectors.joining("-"));// 将所有数据拼接,拼接,类似reduce
System.out.println(collectJoining);// 张无忌-周芷若-赵敏-张强-张三丰
String collectJoining2 = list.stream().collect(Collectors.joining("-", "start", "end"));// 将所有数据拼接,拼接,类似reduce
System.out.println(collectJoining2);// start张无忌-周芷若-赵敏-张强-张三丰end
5.6、filter/distinct
Stream<T> filter(Predicate<? super T> predicate);
Stream<T> distinct();
filter方法的作用是用来过滤数据的,返回符合条件的数据,返回的流是原来流的子集,接受一个Predicate判断型接口的参数作为筛选条件。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Stream<String> filterStream = list.stream().filter(e -> e.startsWith("赵"));
filterStream.forEach(System.out::println);// 赵敏
distinct方法的作用是去掉重复数据。
List<String> list = Arrays.asList("张无忌","张无忌", "周芷若", "赵敏", "张强", "张三丰", "张三丰");
Stream<String> distinctStream = list.stream().distinct();
distinctStream.forEach(System.out::println);// 张无忌 周芷若 赵敏 张强 张三丰
备注:有些list数据存放的自定义对象,自定义对象判断重复的标准就是equals和hashCode。
例如:List list,存放 user1{“张三”,18},user2{“张三”,18}
user1和user2这两个对象业务开发中我们会认为是重复数据,如果User类不重写equals和hashCode这两个方法,默认都是对比对象引用地址了,user1和user2的引用地址一般都是不相等的,这样就无法成功去重,必须重写后有新规则,才可以成功去重。
5.7、limit/skip
Stream<T> limit(long maxSize);
Stream<T> skip(long n);
limit方法可以对流进行截取处理,只取前n个数据,参数传负数会报错,传0取不到数据但是返回结果不为null。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Stream<String> limitStream = list.stream().limit(1);// 参数为0时,limitStream不为null,不用担心空指针异常
limitStream.forEach(System.out::println);// 张无忌
skip方法是跳过前面n个数据,从后面开始取数据到最后一个,参数传负数会报错,传大于总数据数量取不到数据但是返回结果不为null。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Stream<String> skipStream = list.stream().skip(4);// 参数为>=5时,skipStream不为null,不用担心空指针异常
skipStream.forEach(System.out::println);// 张三丰
5.8、map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
map方法可以将流中的元素映射到另一个流中,接受一个Function函数型接口参数,可以将当前流中的T类型数据转化为另一种R类型的数据。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
Stream<String> mapStream = list.stream().map(t -> t.substring(1));
mapStream.forEach(System.out::println);// 无忌 芷若 敏 强 三丰
map和reduce的组合
// map和reduce的组合 张无忌出现次数
Integer mapReduceCount = list.stream()
.map(t -> "张无忌".equals(t) ? 1 : 0)// 出现一次记录1
//.reduce(0, (x, y) -> {
// return x + y;// reduce规约,从x=0开始,y循环map映射后的新集合的每个元素,x累积接受retrun结果
//});
.reduce(0, Integer::sum);
System.out.println(mapReduceCount);// 1
5.9、sorted
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
sorted方法可以将数据进行排序,不传参数会根据数据进行自然规则排序(数字是升序),也可以接受一个Comparator函数式接口的参数来自定义排序规则。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
//Stream<String> sortedStream = list.stream().sorted();// 自然规则排序(数字是升序)
//sortedStream.forEach(System.out::println);// 周芷若 张三丰 张强 张无忌 赵敏
Stream<String> sortedStream = list.stream().sorted((o1, o2) -> o1.length() - o2.length());// Comparator是函数式接口,o1和o2对比,小于0,o1排序到前面
//Stream<String> sortedStream = list.stream().sorted(Comparator.comparing(String::length));// 方法引用优化前面代码
sortedStream.forEach(System.out::println);// 赵敏 张强 张无忌 周芷若 张三丰
5.10、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));
}
concat方法可以将两个流合并成为一个流。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
List<String> list2 = Arrays.asList("a", "b");
Stream<String> stream1 = list.stream();
Stream<String> stream2 = list2.stream();
Stream<String> concatStream = Stream.concat(stream1, stream2);
concatStream.forEach(System.out::println);// 张无忌 周芷若 赵敏 张强 张三丰 a b
6、Stream的串行和并行
6.1、Stream的串行
上面使用的list.stream()流都是串行,也就是在一个线程上面执行。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
list.stream().forEach(e -> {
System.out.println(Thread.currentThread().getName() +"-" + e );
// main-张无忌 main-周芷若 main-赵敏 main-张强 main-张三丰
});
6.2、Stream的并行
list.parallelStream()或者list.stream().parallel()是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度。
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
list.parallelStream().forEach(e -> {
System.out.println(Thread.currentThread().getName() +"-" + e );// 每次结果不一样
// main-赵敏
// main-张三丰
// ForkJoinPool.commonPool-worker-2-张强
// ForkJoinPool.commonPool-worker-1-周芷若
// main-张无忌
});
// 或者
list.stream().parallel().forEach(e -> {// 每次结果不一样
System.out.println(Thread.currentThread().getName() +"-" + e );
// main-赵敏
// ForkJoinPool.commonPool-worker-3-周芷若
// ForkJoinPool.commonPool-worker-2-张三丰
// main-张无忌
// ForkJoinPool.commonPool-worker-1-张强
});
6.3、串行流和并行流的对比
案例:for循环对5亿个数字求和,查看消耗时间
// 串行流和并行流的对比
long num = 500000000L;// 5亿
// 普通for循环
long start1 = System.currentTimeMillis();// 开始时间
long sum1 = 0;// 总和
for (long i = 0; i < num; i++) {
sum1 += i;
}
long end1 = System.currentTimeMillis();// 结束时间
System.out.println("消耗时间1:" + (end1 - start1));// 393
System.out.println("总和1:" + sum1);// 124999999750000000
// 串行流
long start2 = System.currentTimeMillis();// 开始时间
LongStream longStream = LongStream.rangeClosed(0, num);// 以增量为1生成从0(包括)到num(包括)的数值流。例如 0,1,2,3....5亿 组成一个LongStream。 range(0, num)是包括0不包括num。生成这个流比较费时。
long sum2 = longStream.reduce(0, Long::sum);// 总和
long end2 = System.currentTimeMillis();// 结束时间
System.out.println("消耗时间2:" + (end2 - start2));// 2067
System.out.println("总和2:" + sum2);// 125000000250000000
// 并行流
long start3 = System.currentTimeMillis();// 开始时间
long sum3 = LongStream.rangeClosed(0, num).parallel().reduce(0, Long::sum);// 总和
long end3 = System.currentTimeMillis();// 结束时间
System.out.println("消耗时间3:" + (end3 - start3));// 62
System.out.println("总和3:" + sum3);// 125000000250000000
通过案例可以看出parallelStream效率非常高。
6.4、并行流线程安全问题
多线程处理下,肯定会出现数据安全问题。
// 并行流线程安全问题
List<Integer> listOld = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
listOld.add(i);
}
//IntStream listOldStream = IntStream.rangeClosed(1, 1000);// 也可以完成1000个元素的初始化
List<Integer> listNew = new ArrayList<>();
//listOld.stream().parallel().forEach(e -> listNew.add(e));// listOld一共1000个,listNew每次结果不一样
listOld.stream().parallel().forEach(listNew::add);
// listOld一共1000个,listNew每次结果不一样,可能少于1000,也可能多余1000报错ArrayIndexOutOfBoundsException
System.out.println(listNew.size());// 698
解决方案一:加同步锁
List<Integer> listOld = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
listOld.add(i);
}
// 解决方案一:加同步锁
List<Integer> listNew1 = new ArrayList<>();
Object object = new Object();
listOld.stream().parallel().forEach(e -> {
synchronized (object){
listNew1.add(e);
}
});
System.out.println(listNew1.size());// 1000
解决方案二:使用线程安全的容器
List<Integer> listOld = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
listOld.add(i);
}
// 解决方案二:使用线程安全的容器
CopyOnWriteArrayList listNew2 = new CopyOnWriteArrayList();// 线程安全的容器(写时复制技术,独立写,并发读)
listOld.stream().parallel().forEach(listNew2::add);
System.out.println(listNew2.size());// 1000
解决方案三:使用collect方法
List<Integer> listOld = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
listOld.add(i);
}
// 解决方案三:使用collect方法
List<Integer> listNew3 = listOld.stream().parallel().collect(Collectors.toList());
System.out.println(listNew3.size());// 1000
7、Fork/Join框架
parallelStream使用的是Fork/Join框架,该框架从JDK7引入,Fork/Join框架可以将一个大任务拆分为很多小任务来异步执行。主要包含三个模块:
1、线程池:ForkJoinPool
2、任务对象:ForkJoinTask
3、执行任务的线程:ForkJoinWorkerThread
7.1、Fork/Join原理-分治法
ForkJoinPool主要用来使用分治法来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000玩个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对他们进行排序。那么到最后,所有的任务加起来会有大概200万+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
从图中可以看出,Fork主要是分割任务,Join主要是汇聚小 任务结果。
7.2、Fork/Join原理-工作窃取算法
Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高新能的关键,而这里面我们要提到的工作窃取算法就是整个Fork/Join框架的核心理念。工作窃取算法是指某个线程从其他队列里窃取任务来执行。
那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等待,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,既运行时计算机的处理器数量。可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N(N为线程数量),来调整ForkJoinPool的线程数量,可以尝试调整不同的参数开观察每次的输出结果。
7.3、Fork/Join案例
需求:使用Fork/Join计算1-1万的和,当一个任务的计算数量大于3000的时候拆分任务,小于3000就计算。
public class Jdk8NewFeatures {
public static void main(String[] args) {
// 需求:使用Fork/Join计算1-1万的和,当一个任务的计算数量大于3000的时候拆分任务,小于3000就计算。
long start = System.currentTimeMillis();// 开始时间
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1L, 10000L);
Long result = pool.invoke(task);
long end = System.currentTimeMillis();// 结束时间
System.out.println("求和:" + result);
System.out.println("总耗时:" + (end - start));
}
static class SumRecursiveTask extends RecursiveTask<Long> {// 任务对象:ForkJoinTask; RecursiveTask extends ForkJoinTask
// 定义一个拆分的临界值
private static final long THRESHOLD = 3000L;
private final long start;
private final long end;
public SumRecursiveTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length <= THRESHOLD) {// 小于临界值3000,任务不用拆分,可以计算。
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
System.out.println("计算:" + start + "-->" + end + ",的结果为:" + sum);
return sum;
} else {// 任务拆分
long middle = (end + start) / 2;
System.out.println("拆分:左边" + start + "-->" + middle + ",右边" + (middle + 1) + "-->" + end);
SumRecursiveTask left = new SumRecursiveTask(start, middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
}
输出结果:
拆分:左边1-->5000,右边5001-->10000
拆分:左边1-->2500,右边2501-->5000
拆分:左边5001-->7500,右边7501-->10000
计算:7501-->10000,的结果为:21876250
计算:2501-->5000,的结果为:9376250
计算:5001-->7500,的结果为:15626250
计算:1-->2500,的结果为:3126250
求和:50005000
总耗时:60
六、Optional类
1、概念
final修饰的类,Optional是没有子类的工具类。Opinion是一个可以为null的容器对象,它的主要作用是为了避免null检查,防止空指针问题。
以前对null的处理
String username = null;
if (StringUtil.isNotEmpty(username)) {
System.out.println("字符串长度:" + username.length());
} else {
System.out.println("字符串为空");// 字符串为空
}
2、Optional的使用
2.1、Optional对象的创建
// Optional对象的创建
// 方式一:通过of方法,不支持null
Optional<String> optional1 = Optional.of("张无忌");
Optional<String> optional2 = Optional.of(null);// 会报错,of方法不支持null
// 方式二:通过ofNullable方法,支持null
Optional<String> optional3 = Optional.ofNullable("张无忌");
Optional<String> optional4 = Optional.ofNullable(null);// 不报错
// 方式三:通过empty方法,直接创建的是空的Optional对象
Optional<Object> optional5 = Optional.empty();
2.2、Optional常用方法
// 获取Optional中的值
// get()方法,如果Optional有值则返回值,否则抛出NoSuchElementException异常,通常和isPresent方法一块使用
// isPresent()方法,如果Optional有值则返回ture,否则返回false
// orElse(T t)方法,如果Optional有值则返回值,否则返回t
// orElseGet(Supplier s)方法,如果Optional有值则返回值,否则返回Supplier供给型函数中Lambda表达式的返回值提供的值
// orElseThrow()方法,如果Optional有值则返回值,否则抛出NoSuchElementException异常
// orElseThrow(Supplier s)方法,如果Optional有值则返回值,否则抛出Supplier供给型函数中Lambda表达式的返回值提供的异常
// ifPresent(Consumer c)方法,如果Optional有值则执行Consumer消费型函数中的代码,否则不做任何处理
// map(Function f)方法,和Stream流的map类似,是映射关系,将Optional的值映射成新的值,新的值构成了新的Optional对象
Optional<String> optional6 = Optional.of("张无忌");
Optional<String> optional7 = Optional.empty();
Optional<String> optional8 = Optional.of("abc");
if (optional6.isPresent()) {
String s6 = optional6.get();
System.out.println(s6);// 张无忌
}
if (optional7.isPresent()){
System.out.println(optional7.get());
} else {
System.out.println("optional7是个空Optional对象");// optional7是个空Optional对象
}
System.out.println(optional7.get());// 报错 java.util.NoSuchElementException: No value present
String s6orElse = optional6.orElse("赵敏");
System.out.println(s6orElse);// 张无忌
String s7orElse = optional7.orElse("赵敏");
System.out.println(s7orElse);// 赵敏
String s7orElseGet = optional7.orElseGet(() -> {
return "赵敏";
});
System.out.println(s7orElseGet);// 赵敏
String sorElseThrow = optional7.orElseThrow(() -> {
return new Exception("自定义异常");
});// 抛出异常 ava.lang.Exception: 自定义异常
optional6.ifPresent(s6ifPresent-> System.out.println(s6ifPresent));// 张无忌
// optional7无值下面操作不执行,不打印任何东西
optional7.ifPresent(s7ifPresent-> System.out.println(s7ifPresent));
Optional<String> s8map = optional8
.map(s -> s.substring(0, 1))
//.map(s -> s.toUpperCase());
.map(String::toUpperCase);
System.out.println(s8map.get());// A
Optional<Object> s6map = optional6.map(s -> null);// 不会报错
System.out.println(s6map.get());// 报错,java.util.NoSuchElementException: No value present
七、新时间日期API
1、旧版日期时间问题
在旧版本中jdk对于日期和时间这块的设计不合理,Date日期在java.util和java.sql都有,而且它的时间格式转换类在java.text包,并且线程也不安全。时区处理麻烦,日期类并不提供国际化,没有时区支持。
// 旧版日期时间设计问题
// 1、设计不合理
Date date = new Date(2021,05,05);// 导入包的时候有util和sql两个包,导包的时候需要选择util包的Date
System.out.println(date);// Sun Jun 05 00:00:00 CST 3921 备注:3921年不是我们期望的年分,是2021+1900=3921了;Jun也不是我们期望的月份,是6月份了
// 2、时间格式化是线程不安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(sdf.format(date));// 3921-06-05
System.out.println(sdf.parse("2021-06-05"));// Sun Jun 05 00:00:00 CST 2021
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
System.out.println(sdf.parse("2021-06-05"));// 线程安全问题,打印的不一样,而且会报错
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}
JDK8新日期和时间API的优势:
- 新版日期时间API中,日期和时间对象是不可变的,操作日期不会影响原来的值,而是生成一个新的实例
- 提供不同的两种方式,有效的区分了人和机器的操作
- TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整
- 线程安全
2、新日期时间API
JDK8中新增了一套全新的日期时间API,这套API设计合理,是线程安全的。位于java.time包中,下面是一些关键类。
● LocalDate:表示日期,包含年月日,格式为 2021-05-05
● LocalTime:表示时间,包含时分秒,格式为 10:39:59.158549300
● LocalDateTime:表示日期时间,包含年月日时分秒,格式为 2021-05-05T10:39:59.750
● DateTimeFormatter:日期时间格式化类
● Instant:时间戳,表示一个特定的时间瞬间
● Duration:用于计算2个时间LocalTime时分秒的距离
● Period:用于计算2个时间LocalDate年月日的距离
● ZonedDateTime:包含时区的时间
java中使用的历法是ISO 8601日历系统,他是世界民用历法,也就是我们所说的功力。平年有365天,闰年是366天。此外java8还提供了4套其他历法,分别是:
● ThaiBuddhistDate:泰国佛教历
● MinguoDate:中华民国历
● JapaneseDate:日本历
● HijrahDate:伊斯兰历
2.1、日期时间的常见操作
LocalDate、LocalTime、LocalDateTime的操作。
// 新版日期时间的常见操作
// 1、创建指定的日期
LocalDate date1 = LocalDate.of(2024, 05, 05);
System.out.println("date1=" + date1);// date1=2024-05-05
// 2、得到当前的日期
LocalDate date2 = LocalDate.now();
System.out.println("date2=" + date2);// date2=2023-09-26
// 3、获取对应的日期信息
System.out.println("年:" + date2.getYear());// 年:2023
System.out.println("月:" + date2.getMonth());// 月:SEPTEMBER 备注:可以date2.getMonth().getValue()获取枚举值 例如:9
System.out.println("日:" + date2.getDayOfMonth());// 日:26
System.out.println("星期:" + date2.getDayOfWeek());// 星期:TUESDAY 备注:可以date2.getDayOfWeek().getValue()获取枚举值 例如:4
// 新版时间操作
// 1、创建指定的时间
LocalTime time1 = LocalTime.of(23, 59, 59,1234);// 1234是指纳秒 1秒=1000毫秒 1毫秒=1000微妙 1微妙=1000纳秒
System.out.println(time1);// 23:59:59.000001234
// 2、得到当前的时间
LocalTime time2 = LocalTime.now();
System.out.println(time2);// 11:18:20.886005700
// 3、获取对应的时间信息
System.out.println("时:" + time2.getHour());// 时:11
System.out.println("分:" + time2.getMinute());// 分:18
System.out.println("秒:" + time2.getSecond());// 秒:20
System.out.println("纳秒:" + time2.getNano());// 纳秒:886005700
// 新版日期时间操作
// 1、创建指定的日期时间
LocalDateTime dt1 = LocalDateTime.of(2024,5,5,23,59,59,1234);
System.out.println(dt1);// 2024-05-05T23:59:59.000001234
// 2、得当当前的日期时间
LocalDateTime dt2 = LocalDateTime.now();
System.out.println(dt2);// 2023-09-26T11:21:31.763786700
// 3、获取对应的日期时间信息
System.out.println("年:" + dt2.getYear());// 年:2023
System.out.println("月:" + dt2.getMonth());// 月:SEPTEMBER 备注:可以date2.getMonth().getValue()获取枚举值 例如:9
System.out.println("日:" + dt2.getDayOfMonth());// 日:26
System.out.println("星期:" + dt2.getDayOfWeek());// 星期:TUESDAY 备注:可以date2.getDayOfWeek().getValue()获取枚举值 例如:4
System.out.println("时:" + dt2.getHour());// 时:11
System.out.println("分:" + dt2.getMinute());// 分:18
System.out.println("秒:" + dt2.getSecond());// 秒:20
System.out.println("纳秒:" + dt2.getNano());// 纳秒:886005700
2.2、日期时间的修改和比较
// 日期时间的修改
// 日期时间的修改,不会修改原来的信息,而是创建了它的模板形成了新的日期时间对象。with开头的方法来修改对应的时间信息。
LocalDateTime dt3 = LocalDateTime.now();
System.out.println(dt3);// 2023-09-26T14:25:04.944031100
LocalDateTime dt4 = dt3.withYear(1998);// 修改为1998年
System.out.println(dt3);// 2023-09-26T14:25:04.944031100
System.out.println(dt4);// 1998-09-26T14:25:04.944031100
// 日期时间的加减法,也是不会修改原来的信息,而且创建了它的模板形成了新的日期时间对象。plus开头的方法来增加对应的时间信息,minus开头的方法来减少对应的时间信息。
LocalDateTime dt5 = dt3.plusYears(7);// 7年后
System.out.println(dt3);// 2023-09-26T14:25:04.944031100
System.out.println(dt5);// 2030-09-26T14:25:04.944031100
LocalDateTime dt6 = dt3.minusYears(3);// 3年前
System.out.println(dt3);// 2023-09-26T14:25:04.944031100
System.out.println(dt6);// 2020-09-26T14:25:04.944031100
// 日期时间的比较 通过isAfter isBefore equals几个方法直接比较
System.out.println(dt3.isAfter(dt6));// true 备注:dt3在dt6之后,也就是dt3时间大于dt6
System.out.println(dt3.isBefore(dt5));// true
System.out.println(dt3.equals(dt4));// false 备注:dt3等于dt4
注意:在进行日期时间修改的时候,原来的LocalDate等对象是不会被修改的,每次操作都是返回了一个新的LocalDate对象,所以在多线程场景下是数据安全的。
2.3、日期时间的格式化和解析
// 日期时间的格式化和解析
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME;// 系统提供的一种默认格式
// 将日期时间转化为字符串
String format1 = dt3.format(dateTimeFormatter1);
System.out.println(format1);// 2023-09-26T14:25:04.944031100
DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");// 自定义时间格式
String format2 = dt3.format(dateTimeFormatter2);
System.out.println(format2);// 2023-09-26 14:25:04
// 将字符串解析为日期时间
LocalDateTime dt7 = LocalDateTime.parse("2023-09-26 14:25:04",dateTimeFormatter2);
System.out.println(dt7);// 2023-09-26T14:25:04
2.4、Instant类
内部保持了从1970年1月1日00:00:00以来的秒和纳秒,是时间戳和时间线。
// Instant类
// 时间戳,可以用来统计时间消耗
Instant instant1 = Instant.now();
System.out.println(instant1);// 2023-09-28T05:57:33.237103Z
System.out.println(instant1.getNano());// 237103000
Thread.sleep(5);
Instant instant2 = Instant.now();
System.out.println(instant2.getNano());// 244099500
System.out.println("消耗:" + (instant2.getNano() - instant1.getNano()));// 消耗:6996500
2.5、计算日期时间差
两个工具类Duriation/Period可以计算日期时间差
● Duriation用来计算两个时间差(LocalTime)
● Period用来计算两个日期差(LocalDate)
// 计算日期时间差
// 通用Duration计算时间差
LocalTime time3 = LocalTime.now();
System.out.println(time3);// 14:07:47.396087900
LocalTime time4 = LocalTime.of(23, 59, 59);
Duration between1 = Duration.between(time3, time4);
System.out.println("天数:" + between1.toDays());// 天数:0
System.out.println("小时:" + between1.toHours());// 小时:9
System.out.println("分钟:" + between1.toMinutes());// 分钟:592
System.out.println("毫秒:" + between1.toMillis());// 毫秒:35531603
// 通过计算日期差
LocalDate date3 = LocalDate.now();
System.out.println(date3);// 2023-09-28
LocalDate date4 = LocalDate.of(2023, 10, 1);
System.out.println(date4);// 2023-10-01
Period between2 = Period.between(date3, date4);
System.out.println("年份:" + between2.getYears());// 年份:0
System.out.println("月份:" + between2.getMonths());// 月份:0
System.out.println("天数:" + between2.getDays());// 天数:3
2.6、时间校正器
有时候我们可能需要如下调整:将日期调整到下个月第一天等操作,通过时间校正器效果可能更好。
TemporalAdjuster:时间校正器
TemporalAdjusters:工具类提供了大量的常用TemporalAdjuster的实现
// 时间校正器
LocalDateTime dt8 = LocalDateTime.now();
System.out.println(dt8);// 2023-09-28T14:28:30.158591400
// 将当前日期调整到下个月一号
TemporalAdjuster adJuster = (temporal) -> {
LocalDateTime dt9 = (LocalDateTime) temporal;
LocalDateTime dt10 = dt9.plusMonths(1).withDayOfMonth(1);
return dt10;
};// 此方式比较灵活
LocalDateTime dt11 = dt8.with(adJuster);
System.out.println(dt11);// 2023-10-01T14:28:30.158591400
// TemporalAdjusters工具类则提供了常用的一些方法
LocalDateTime dt12 = dt8.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println(dt12);// 2023-10-01T14:28:30.158591400
2.7、日期时间的时区
LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的对应类分别是:
ZonedDate、ZonedTime、ZonedDateTime
其中每个时区都对应着ID,ID的格式为“区域/城市”,例如:Asia/Shanghai 等
ZoneId:该类中包含了所有的时区信息
// 时区
// 获取所有时区id(非常多)
ZoneId.getAvailableZoneIds().forEach(System.out::println);// Asia/Aden America/Cuiaba ... Europe/Monaco
// 获取当地时间 中国使用的 东八区的时区 比标准时间早8小时
LocalDateTime dt13 = LocalDateTime.now();
System.out.println(dt13);// 2023-09-28T14:47:31.419862300
// 使用计算机默认的时区、创建日期时间
ZonedDateTime zdt1 = ZonedDateTime.now();
System.out.println(zdt1);// 2023-09-28T14:47:31.419862300+08:00[Asia/Shanghai]
// 获取标准时间
ZonedDateTime zdt2 = ZonedDateTime.now(Clock.systemUTC());
System.out.println(zdt2);// 2023-09-28T06:47:31.420860800Z
// 使用指定的时区创建日期时间
ZonedDateTime zdt3 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
System.out.println(zdt3);// 2023-09-28T02:47:31.420860800-04:00[America/Marigot]
八、新增注解
1、重复注解
主从java5引入注解以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。JDK8引入了重复注解的概念,容许在同一个地方多次使用同一个注解。在JDK8中使用@Repeatable注解定义重复注解。
1.1、定义一个重复注解的容器
package com.ours.www.demo.utils;
import java.lang.annotation.*;
/**
* 重复注解MyAnnotation的容器
* @author zangtie
* @since 2023/9/28 16:11
*/
/**
* JDK8新特性 六大元注解、重复注解(jkd8新增)、类型注解(jkd8新增)
* 重复注解MyAnnotation的容器
* 注意:容器注解MyAnnotations的几个元注解需要和MyAnnotation相同
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.TYPE_PARAMETER,ElementType.TYPE_USE})
@Documented
public @interface MyAnnotations {
MyAnnotation[] value();
}
1.2、定义一个可以重复的注解
package com.ours.www.demo.utils;
import java.lang.annotation.*;
/**
* 重复注解
* @author zangtie
* @since 2023/9/28 16:11
*/
/**
* JDK8新特性 六大元注解、重复注解(jkd8新增)、类型注解(jkd8新增)
* 元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。
* Java 5 定义了 4 个注解,分别是 @Documented、@Target、@Retention 和 @Inherited。
* Java 8 又增加了 @Repeatable 和 @Native 两个注解。
* 这些注解都可以在 java.lang.annotation 包中找到。
*/
/**
* @Repeatable 是一个重复注解,用来指定一个注解可以在相同程序元素上重复使用。
* 小括号里面的值MyAnnotations.class中,MyAnnotations是容器注解,也是存储注解,MyAnnotation可以重复使用,其实是作为容器注解的value成员的数组元素处理。
* 注意:容器注解MyAnnotations的几个元注解需要和MyAnnotation相同
* 例如:Class A类; @interface MyAnnotation 注解(@MyAnnotation用@Repeatable元注解定义过的注解)
* @MyAnnotation("role1")
* @MyAnnotation("role2")
* public Class A {
*
* }
*/
@Repeatable(MyAnnotations.class)
/**
* @Inherited 是一个标记注解,用来指定一个注解可以被继承。
* 例如:Class A类; Class B extend A; @interface MyAnnotation 注解(@MyAnnotation用@Inherited元注解定义过的注解)
* 如果A类用了@MyAnnotation这个注解标记,那么A类的子类B类即使不写这个注解,也是会自动继承父类A的@IMyAnnotation注解,相当于B类也有了@MyAnnotation注解,子子孙孙无穷匮也。
*/
@Inherited
/**
* @Retention 描述一个注解的生命周期(该注解被保留的时间长短)。
* RetentionPolicy是枚举,有三个枚举常量:
* SOURCE:在源文件中有效(即源文件保留),只是做一些检查性的操作,范围最小,比如 @Override 和 @SuppressWarnings
* CLASS:在 class 文件中有效(即 class 保留),生成一些辅助代码,比如 @ButterKnife
* RUNTIME:在运行时有效(即运行时保留),需要在运行时去动态获取注解信息,那只能用 RUNTIME,范围最大
* 三枚举常量生命周期大小排序为 SOURCE< CLASS< RUNTIME,前者能使用的地方后者一定也能使用。
*/
@Retention(RetentionPolicy.RUNTIME)
/**
* @Target 指定一个注解的使用范围。
* ElementType是枚举,有很多枚举常量:
* METHOD只能用于方法
* TYPE可以用于类、接口(包括注解类型)或 enum 声明
*/
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.TYPE_PARAMETER,ElementType.TYPE_USE})
/**
* @Documented 是一个标记注解,没有成员变量。
* 用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。
* 默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所有注解类型信息就会被包括在生成的帮助文档中。
*/
@Documented
public @interface MyAnnotation {
String value();
}
1.3、配置重复注解使用案例
package com.ours.www.demo.utils;
public class Jdk8NewFeatures {
public static void main(String[] args) throws Exception {
// 反射获取test方法上的注解和注解值
Class<Jdk8NewFeatures> jdk8NewFeaturesClass = Jdk8NewFeatures.class;
Method testMethod = jdk8NewFeaturesClass.getMethod("test");
MyAnnotation[] annotationsByType = testMethod.getAnnotationsByType(MyAnnotation.class);
Arrays.stream(annotationsByType).forEach(System.out::println);
// 打印结果:
// @com.ours.www.demo.utils.MyAnnotation("fun1")
// @com.ours.www.demo.utils.MyAnnotation("fun2")
Arrays.stream(annotationsByType).forEach(e->{
System.out.println(e.value());
});
// 打印结果:
// fun1
// fun2
}
@MyAnnotation("fun1")
@MyAnnotation("fun2")
public void test(){
}
}
2、类型注解
JDK8为@Target元注解新增了两种类型:TYPE_PARAMETER、TYPE_USE
● TYPE_PARAMETER:表示该注解能写在类型参数的声明语句中,类型参数声明如:,就是被标记的注解可以使用在泛型中T的前面,例如:<@MyAnnotation T>
● TYPE_USE:表示注解可以在任何用到类型的地方使用
依据重复注解案例中提供的@MyAnnotation注解,来继续案例
TYPE_PARAMETER使用案例
public class TypeParameterDemo<@MyAnnotation("xxx") T> {
// 可以在类定义的时候泛型T前面使用,也可以在方法定义的时候泛型K前面使用
public <@MyAnnotation("yyy") K> K typeParameterDemoTest() {
return null;
}
}// @MyAnnotation注解定义的时候,元注解@Target有TYPE_PARAMETER类型,所以该注解就支持在泛型T前面使用。
TYPE_USE使用案例
public class TypeUseDemo {
private @MyAnnotation("aaa") Integer age ;// 成员变量Integer类型前面可以使用
// 方法返回类型、参数类型前面统统都可以使用
public @MyAnnotation("bbb") String typeUseDemoTest(@MyAnnotation("ccc") Integer a,@MyAnnotation("ddd") boolean b) {
return null;
}
}// @MyAnnotation注解定义的时候,元注解@Target有TYPE_USE类型,所以该注解就支持在任何类型前面使用。