Java 8的这些新特性,不一样的全新版本(万字长文详细说明)
1995 年 5 月 23 日,Oak 语言改名为 Java,然后有了那句著名的口号——“Write Once,Run Anywhere”。
1996 年 JDK1.0发布了,标志着一个新的时代已经到来。
1998 年 Java 迎来 1.2 版本,有此诞生了我熟知的 J2ME、J2SE 以及 J2EE。ME 主要用于移动端(还记得大明湖畔诺基亚 S40 吗?不小心暴露了年龄),而 SE 做为标准版主要用于桌面程序,EE 则主要针对企业应用所打造,也是我们主要的研究对象。1.2 的发布标志着Java开始普及。
2000 年 1.3 发布,并得到了 Mac OS X 工业标准的支持。
2002 年,也就是中国首次进入世界杯(也是至今唯一的一次,扎铁了老心)的那年。这一年 1.4 问世,这一版本极大的丰富了 Java 的类库,如:XML、Socket、全新的 I/O API、正则、日志、断言等如今我们耳熟能详的功能。
2004 年,我们刚刚经历完非典,Java 也迎来了重要更新,为了突出这次更新的重要性,命名方式从原来的 J2XE 1.X 变成了现在的 JavaXE X,于是有了 JavaSE 5。这一版本增加了泛型、自动拆装箱、循环增强(foreach)、枚举、注解、可变参数等,堪称有史以来最重大的更新,Java 5 应该有姓名!
接下来(2006年)又发布了 Java 6 ,据说这个版本在国内很流行(不要告诉我你们公司还在用 JDK1.6 )。同年,发生了一件大事——Java 开源了!我爱开源!
2009 年 Oracle(就是前段时间裁员 N+6 的那家公司)收购了 Sun , Java 从此跟了后妈,过着寄人篱下的日子。
后来,在 2011 年发布了 Java 7 。
三年后发布了 Java 8 ,为我们带来了 Lambda 表达式、Stream 以及新的日期时间 API 。Java 8 应该也是目前被使用最多的版本。
后来相继发布了 9、10、11、12、13,今年(2020 年)3 月 17 日 Oracle 发布了 JDK 14。
在Java8中产生了许多重大更新。
1、Lambda表达式
Lambda轻松上手,快速传送门:Lambda表达式,你真的不了解一下吗?
2、Stream API
说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带
来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
2.1 入门介绍
几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元
素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
import java.util.ArrayList;
import java.util.List;
public class Demo01ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
for (String name : list) {
System.out.println(name);
}
}
}
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端 :
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行
了对比说明。现在,我们仔细体会一下上例代码,可以发现:
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从
第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
-
将集合A根据条件一过滤为子集B;
-
然后再根据条件二过滤为子集C。
可以进行多次遍历后筛选:
import java.util.ArrayList;
import java.util.List;
public class Demo02NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
List<String> zhangList = new ArrayList<>();
//第一次筛选以‘张‘开始的name
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
//以第一次筛选的结果作为第二次筛选的输入
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
这段代码中含有三个循环,每一个作用不同:
- 首先筛选所有姓张的人;
- 然后筛选名字有三个字的人;
- 最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循
环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使
用另一个循环从头开始。
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
下面来看一下借助Java 8的Stream API,如果优雅的写代码:
import java.util.ArrayList;
import java.util.List;
public class Demo03StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.stream()
.filter(s ‐> s.startsWith("张"))
.filter(s ‐> s.length() == 3)
.forEach(System.out::println);
}
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码
中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
2.2、什么是Stream流
流(Stream) 到底是什么呢?是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算! ”
注意:
- Stream不会存储元素。
- Stream不会改变元对象,相反,他们会返回一个持有结果的新Strean【类似建造者模式】。
- Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
Stream的操作三部曲:
- 创建Stream:一个数据源(如:集合、数组),获取一个流。
- 中间操作:一个中间操作链,对数据源的数据进行处理。
- 终止操作:一个终止操作,执行中间操作链,得到结果。
2.3、Stream的创建
①、通过Collection接口获取
- default Stream
stream() : 返回一个顺序流 - default Stream
parallelStream() : 返回一个并行流
//1.通过Collection系列集合提供的Stream()[串行流]方法或者parallelStream()[并行流]
List<Integer>list=new ArrayList<>();
Stream<Integer> stream1 = list.stream();
②、由数组创建流
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
- static
Stream stream(T[] array): 返回一个流
//2.通过Arrays中的静态方法Stream()获取数组流,各种数组转为Stream
int[] arr={1,2,3,4};
IntStream stream2= Arrays.stream(arr);
③、由值创建流
可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。
- public static
Stream of(T… values) : 返回一个流
Stream<String> stream3 = Stream.of("a", "b");
④、由函数创建流:创建无限流
可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。 【流的初始大小未固定】
-
①、迭代
public static
Stream iterate(final T seed, finalUnaryOperator f) -
②、生成
public static
Stream generate(Supplier s)
/**
* public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
* 参数 seed:初始值
* UnaryOperator<T> f函数式接口,单参数有返回值
*
*/
//①迭代
Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2);
stream4.limit(10)
.forEach(System.out::println);
//②生成
Stream.generate(()->Math.random())
.limit(10)
.forEach(System.out::println);
2.4、Stream的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值” 。
①、筛选与切片
- filter-接受lambda表达式,从流中排除某些元素;
- limit-截断流,试元素不超过给定的数量;
- skip(n)-跳过元素,返回一个扔掉了前n个元素的流。若流中的元素不足n个,则返回一个空流,与limit(n)互补;
- distinct-筛选,通过流所生成的hashCode()和equls()去掉重复元素。
@Test
public void test01(){
list.stream()
.filter((x)->Integer.parseInt(x.getAge())>50)
.limit(1)
.forEach(System.out::println);
//终止操作:最后一次性执行全部内容,并不会先执行某句话再执行
}
@Test
public void test02(){
list.stream()
.skip(2)//跳过前两个
.distinct()//去重 其中对象得重写hashcode和equls
.forEach(System.out::println);
}
说明:比如filter(Predicate<? super T> predicate)需要传递的是一个函数式接口,而上述代码使用lambda表达式去实现。
②、映射
- flatMap(Function<? super T, ? extends Stream<? extends R>> mapper):【多个流合并为一个流】把流中的元素一个个加入到当前的流中。
- map(Function<? super T, ? extends R> mapper):函数会对每一个元素进行映射得到一个新的元素。
下面为了测试这两种的区别,我们自定义一个返回Stream流的方法:
@Test
public void test04(){
List<String>str=Arrays.asList("aaa","bbb","ccc");
//直接使用map map本地得到的就是一个新的Stream流,而map执行多次得到多个流,最终Stream流存放的依然是 Stream流【Stream流中存放的是Stream流】
//map:不会合并流,仅仅是对元素输入函数进行映射,得到一个个Stream加入到当前的流中
Stream<Stream<Character>> sm = str.stream().map(TestStream::filterCharter);//直接调用此类的方法
sm.forEach((ssmm)->{
ssmm.forEach(System.out::println);
});
//flatMap:将返回的流进行合并,得到一个流
str.stream()
.flatMap(TestStream::filterCharter)//lambda表达式的类::静态方法调用 直接调用此类的方法
.forEach(System.out::println);
}
//将字符串切割得到字符
public static Stream<Character>filterCharter(String str){
List<Character> list=new ArrayList<>();
for (Character c:str.toCharArray()) {
list.add(c);
}
return list.stream();
}
映射
③、排序
List<Employee> list= Arrays.asList(
new Employee("张三","15"),
new Employee("李四","45"),
new Employee("王五","35"),
new Employee("王五","35"),
new Employee("王六","35"),
new Employee("小刘","65")
);
/**
* 排序:
* sorted()-自然排序-(Comparable)
* sorted(Comparator com)-定制排序
*/
@Test
public void test05(){
List<String>str=Arrays.asList("ddd","aaaaa","bbb","ccccc");
str.stream()
.sorted()
.forEach(System.out::println);
list.stream()
.sorted((x,y)->{
if(x.getAge().equals(y.getAge())){
return -x.getName().compareTo(y.getName());
}else{
return -(Integer.parseInt(x.getAge())-Integer.parseInt(y.getAge()));
}
})
.forEach(System.out::println);
}
2.4、Stream的终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、 Integer,甚至是 void 。
①、查找与匹配
- allMatch-检查是否匹配所有元素
- anyMatch-检查是否至少匹配一个元素
- noneMatch-检查是否没有匹配所有元素
- findFirst-返回第一个元素
- findAny-返回当前流中的任意元素
- count-返回流中元素的总个数
- max-返回流中最大值
- min-返回流中的最小值
是不是已经感觉到迷糊了,这么多!别急,一个小案例就清晰了。
List<Student> list= Arrays.asList(
new Student("张三",18, Student.Status.FREE),
new Student("李四",13, Student.Status.BUSY),
new Student("李四",13, Student.Status.BUSY),
new Student("王五",11, Student.Status.BUSY),
new Student("刘六",55, Student.Status.VOCATION),
new Student("王麻子",77, Student.Status.FREE)
);
//注:Student.Status是一个enum
@Test
public void test01(){
//是否匹配所有的
boolean match = list.stream()
.allMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
System.out.println(match);//false
//至少有一个匹配
boolean match1 = list.stream()
.anyMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
System.out.println("至少有一个元素与之匹配"+match1);//至少有一个元素与之匹配true
//没有匹配的元素
boolean match2 = list.stream()
.noneMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
System.out.println("没有与之匹配的元素:"+match2);//没有与之匹配的元素:false
//避免空指针异常,将结果封装到Optional,一旦有可能为空就封装到Optional中
Optional<Student> op = list.stream()
.sorted((x, y) ->Integer.compare(x.getAge(),y.getAge()))
.findFirst();
System.out.println("排序后拿到第一个元素:"+op.get());//Student{name='王五', age=11, Status=BUSY}
//随便找到一个空闲状态的人,先过滤出来,再随便找一个
Optional<Student> any = list.stream()
.filter((e) -> e.getStatus().equals(Student.Status.FREE))
.findAny();
System.out.println("随便-找到一个status为空闲的人"+any.get());//Student{name='张三', age=18, Status=FREE}
System.out.println("人员总数"+list.stream().count());//人员总数6
Optional<Student> max = list.stream()
.max((x, y) -> Integer.compare(x.getAge(), y.getAge()));
System.out.println("获取年龄最大的:"+max.get());//Student{name='王麻子', age=77, Status=FREE}
}
下面对上述方法的具体描述:
②、归约
- reduce(T identity,BinaryOperator)
T identity 初始值
BinaryOperator->继承BiFunction<T,U, R>
将identity作为起始值,做为x,再从流中取出一个元素作为y; - reduce(BinaryOperator)–可以将流中元素反复结合起来,得到一个值
@Test
public void test02(){
List<Integer>list=Arrays.asList(1,2,3,4,5,6,7,8);
Integer sum = list.stream()
.reduce(0, (x, y) -> x + y);//初始值,一开始x=初始值,再从流中拿到第一个值作为y
System.out.println(sum);//36
/**
* 下列包含Lambda表达式中的方法方法引用的 类::实例方法
* 此种情况较为特殊:
* 若lambda参数列表中的第一个参数是实例方法的调用者,
* 而第二个参数是实例方法的参数时或者没有第二个参数,
* 可以使用ClassName::method
*/
Optional<Integer> op = this.list.stream()
.map((x) -> x.getAge())
.reduce(Integer::sum);
System.out.println(op.get());//187
Optional<Integer> op2 = this.list.stream()
.map(Student::getAge)
.reduce(Integer::sum);
System.out.println(op2.get());//187
}
③、收集
Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、 Set、 Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例, 具体方法与实例如下表:
- collect-将流转换为其他形式。接受一个Collector接口的实现,用于Stream中元素做汇总的方法
List<Student> list= Arrays.asList(
new Student("张三",18, Student.Status.FREE),
new Student("李四",13, Student.Status.BUSY),
new Student("李四",13, Student.Status.BUSY),
new Student("王五",11, Student.Status.BUSY),
new Student("刘六",55, Student.Status.VOCATION),
new Student("王麻子",77, Student.Status.FREE)
);
//需求:将当前学生的名字提取出后放在一个集合里面
@Test
public void test03(){
System.out.println("-----------将结果收集到list集合并返回--------------------");
List<String> list1 = list.stream()
.map(Student::getName)
.collect(Collectors.toList());
list1.forEach(System.out::println);
System.out.println("-----------将结果收集到set集合并返回-----------------------");
Set<String> set1 = list.stream()
.map(Student::getName)
.collect(Collectors.toSet());
set1.stream().forEach(System.out::println);
System.out.println("-----------自定义返回的集合类型---------------------------");
HashSet<String> set2 = list.stream()
.map(Student::getName)
.collect(Collectors.toCollection(HashSet::new));
set2.forEach(System.out::println);
System.out.println("-----------将结果收集为总数数量---------------------------");
Long number = list.stream()
.collect(Collectors.counting());
System.out.println(number);
System.out.println("-----------得到结果的平均值---------------------------");
Double av = list.stream()
.collect(Collectors.averagingInt(value -> value.getAge()));
System.out.println(av);
System.out.println("-----------得到结果的总和---------------------------");
Integer sum = list.stream()
.collect(Collectors.summingInt(Student::getAge));
System.out.println(sum);
System.out.println("-----------得到结果的最大值---------------------------");
Optional<Student> student = list.stream()
.collect(Collectors.maxBy((o1, o2) -> Double.compare(o1.getAge(), o2.getAge())));
System.out.println(student.get());
System.out.println("-----------连接字符串---------------------------");
String s = list.stream()
.map(Student::getName)
.collect(Collectors.joining("中间连接符,","首","尾"));
System.out.println(s);
}
需求一:按照状态进行分组 对应sql的分组
@Test
public void test04(){
Map<Student.Status, List<Student>> map = list.stream()
.collect(Collectors.groupingBy(Student::getStatus));
System.out.println(map);
}
//结果:
{VOCATION=[Student{name='刘六', age=55, Status=VOCATION}],
BUSY=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}],
FREE=[Student{name='张三', age=18, Status=FREE}, Student{name='王麻子', age=77, Status=FREE}]}
需求二:多级分组
注意:分组后一般返回的是一个map,其中key作为分组的依据,value就是分组后的值
/**
*
* Collectors.groupingBy(Function,Collectors)
* 分组后,对已分组的数据进行二次分组
*/
@Test
public void test05(){
Map<Student.Status, Map<String, List<Student>>> map = list.stream()
.collect(Collectors.groupingBy(Student::getStatus, Collectors.groupingBy(o -> {
if (((Student) o).getAge() <= 18) {
return "青年";
} else if (((Student) o).getAge() <= 50) {
return "中年";
} else {
return "老年";
}
})));
System.out.println(map);
}
//结果:
{BUSY={青年=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}]},
VOCATION={老年=[Student{name='刘六', age=55, Status=VOCATION}]},
FREE={青年=[Student{name='张三', age=18, Status=FREE}], 老年=[Student{name='王麻子', age=77, Status=FREE}]}}
分片和分区:根据处理的结果ture/false进行分区
@Test
public void test06(){
Map<Boolean, List<Student>> map = list.stream()
.collect(Collectors.partitioningBy(o -> o.getAge() > 50));
System.out.println(map);
}
//结果:
{false=[Student{name='张三', age=18, Status=FREE}, Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}],
true=[Student{name='刘六', age=55, Status=VOCATION}, Student{name='王麻子', age=77, Status=FREE}]}
3、新的时间日期API
3.1、LocalDate、 LocalTime、 LocalDateTime
LocalDate、 LocalTime、 LocalDateTime 类的实例是不可变的对象【线程安全】,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。 新的日期类将统一放在java.time包下。
注意:三者使用的方式完全相同,只是一个表示日期,一个表示时间,一个表示时间和日期
下面让我们快速入门吧!
@Test
public void test01(){
System.out.println("---------------------获取当前系统时间----------------------------");
LocalDateTime dateTime1 = LocalDateTime.now();
System.out.println(dateTime1);
System.out.println("---------------------指定时间,年月日时分秒----------------------------");
LocalDateTime dateTime2 = LocalDateTime.of(2015, 10, 16, 13, 22, 33);
System.out.println(dateTime2);
//时间运算,添加一日
System.out.println("---------------------日期运算,当前系统时间往后添加一日-----------------");
LocalDateTime dateTime3 = dateTime1.plusDays(1);
System.out.println(dateTime3);
System.out.println("---------------------日期运算,当前系统时间往后减少一个月---------------");
LocalDateTime dateTime4 = dateTime1.minusMonths(1);
System.out.println(dateTime4);
//获取单独的年月日
//可以直接获取值,也可以获取年月日对象
System.out.println("年:"+dateTime1.getYear());
System.out.println("月:"+dateTime1.getMonthValue());
System.out.println("日:"+dateTime1.getDayOfMonth());
System.out.println("时:"+dateTime1.getHour());
System.out.println("分:"+dateTime1.getMinute());
System.out.println("秒:"+dateTime1.getSecond());
}
输出:
---------------------获取当前系统时间---------------------------------
2020-08-13T22:36:07.123
---------------------指定时间,年月日时分秒----------------------------
2015-10-16T13:22:33
---------------------日期运算,当前系统时间往后添加一日-----------------
2020-08-14T22:36:07.123
---------------------日期运算,当前系统时间往后减少一个月---------------
2020-07-13T22:36:07.123
年:2020
月:8
日:13
时:22
分:36
秒:7
①、Instant时间戳
Instant 时间戳 给机器读的用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算。
@Test
public void test02(){
Instant instant = Instant.now();//默认获取的是UTC时区为基础的
System.out.println("默认获取的是UTC时区为基础的时间:"+instant);
//对时区做一个偏移量运算
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println("带偏移量的时区时间:"+offsetDateTime);
//输出时间格式为毫秒格式
System.out.println("输出时间格式为毫秒格式"+instant.toEpochMilli());
//相较1970年的时间
System.out.println("相较1970年的时间 向后添加60秒"+Instant.ofEpochSecond(60));//1970-01-01T00:01:00Z
}
输出:
默认获取的是UTC时区为基础的时间:2020-08-15T02:49:41.828Z
带偏移量的时区时间:2020-08-15T10:49:41.828+08:00
输出时间格式为毫秒格式1597459781828
相较1970年的时间 向后添加60秒1970-01-01T00:01:00Z
②、Duration 和 Period
- Duration:计算两个时间之间的间隔。
- Period:计算两个日期之间的间隔。
@Test
public void test03() throws InterruptedException {
Instant instant1 = Instant.now();
Thread.sleep(100);
Instant instant2 = Instant.now();
//计算两个时间戳之间的间隔
Duration duration = Duration.between(instant1, instant2);
//获取毫秒
System.out.println(duration.toMillis());//119
//获取纳秒
System.out.println(duration.getNano());//119000000
//获取秒
System.out.println(duration.getSeconds());//0
//获取纳秒第二种方式
System.out.println(duration.toNanos());//119000000
LocalDateTime dateTime1 = LocalDateTime.now();
Thread.sleep(1);
LocalDateTime dateTime2 = LocalDateTime.now();
System.out.println(duration.between(dateTime1,dateTime2).toMillis());//1
}
@Test
public void test04(){
LocalDate date1 = LocalDate.of(2018,1,1);
LocalDate date2 = LocalDate.now();
Period period = Period.between(date1, date2);
System.out.println(period);//P2Y5M13D
//直接输出格式不是很明显
System.out.println("相差几年:"+period.getYears());//
System.out.println("相差多少月:"+period.getMonths());//
System.out.println("相差多少天:"+period.getDays());//
}
输出:
P2Y7M14D
相差几年:2
相差多少月:7
相差多少天:14
③、日期的操纵
- TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
- TemporalAdjusters : 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。
方法public LocalDate with(TemporalAdjuster adjuster) ,传入接口TemporalAdjuster 对日期进行调整,我们查看TemporalAdjuster 接口的源码:
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
可以发现其是一个函数式接口
@Test
public void test05(){
//下一个周五是啥时候
LocalDate date = LocalDate.now();
//下一个周五是啥时候
LocalDate date1 = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
System.out.println("下一个周五是啥时候"+date1);
//自定义指定时间,获取当前时间距离周六的天数,
LocalDate date2 = date.with(t -> {
//LocalDate实现了接口Temporal,所以可以实现强转
LocalDate lt = (LocalDate) t;
//获取今天是周几
//lt.plusDays():向当前日期添加几天
DayOfWeek dayOfWeek = lt.getDayOfWeek();
if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
return lt.plusDays(0);//其实这一句是多余的
} else {
return lt.plusDays(DayOfWeek.SATURDAY.getValue() - dayOfWeek.getValue());
}
});
System.out.println("前时间距离周六的时间"+date2);
}
输出:
下一个周五是啥时候2020-08-21
前时间距离周六的时间2020-08-15
说明:DayOfWeek是java.time包下的一个enum类型,其中定义好了周一~周日的枚举值。
④、解析与格式化
java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:
- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
@Test
public void test07(){
System.out.println("-------------------------使用自带的各种格式---------------");
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime dateTime = LocalDateTime.now();
String strtime = formatter.format(dateTime);
System.out.println(strtime);
System.out.println("-------------------------自定义日期格式---------------");
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String strtime2 = formatter2.format(dateTime);
System.out.println(strtime2);
System.out.println("--------------------把字符串解析成为时间LocalDateTime---");
LocalDateTime dateTime2 = dateTime.parse(strtime2, formatter2);
System.out.println(dateTime2);
}
输出:
-------------------------使用自带的各种格式---------------
2020-08-15T11:14:51.74
-------------------------自定义日期格式---------------
2020年08月15日 11:14:51
--------------------把字符串解析成为时间LocalDateTime---
2020-08-15T11:14:51
注意:根据字符串解析LocalDateTime时,其formatter需保持一致,否则会报错java.time.format.DateTimeParseException
⑤、时区的处理
Java8 中加入了对时区的支持,带时区的时间为分别为: ZonedDate、 ZonedTime、 ZonedDateTime 。
其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式例如 : Asia/Shanghai 等。
ZoneId:该类中包含了所有的时区信息
- getAvailableZoneIds() : 可以获取所有时区时区信息
- of(id) : 用指定的时区信息获取 ZoneId 对象
@Test
public void test08(){
System.out.println("---------------获取支持的所有时区----------------------");
Set<String> ids = ZoneId.getAvailableZoneIds();
ids.forEach(System.out::println);
System.out.println("---------------获取指定时区地区的时间信息---------------");
LocalDateTime now = LocalDateTime.now(ZoneId.of("Europe/London"));
System.out.println(now);//2020-06-15T08:18:27.396
System.out.println("---------------获取带时区信息的时间,包含了与UTC标准的时差---");
LocalDateTime now1 = LocalDateTime.now();
ZonedDateTime zone = now1.atZone(ZoneId.of("Europe/London"));
System.out.println(zone);//2020-06-15T15:18:27.515+01:00[Europe/London]
}
输出:
---------------获取支持的所有时区----------------------
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
Asia/Shanghai
等等.....
---------------获取指定时区地区的时间信息---------------
2020-08-15T04:22:09.009
---------------获取带时区信息的时间,包含了与UTC标准的时差---
2020-08-15T11:22:09.029+01:00[Europe/London]
⑥、与传统时器处理与转换
4、接口中的默认方法和静态方法
我们都知道,当实体类(非抽象类)A继承了某个接口B,那么类A必须重写接口中的方法。此模式在springboot 1.X的版本中均使用此模式,其解决方式是在接口和实体类中添加了一层:抽象类,使之抽象类继承接口,实体类再去继承抽象类。比如:
在springboot 1.X版本时,当需要对springmvc进行扩展时,相关配置需要继承抽象类WebMvcConfigureAdapter:
@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
/**
* {@inheritDoc}
* <p>This implementation is empty.
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
}
...WebMvcConfigurer接口的其他方法
}
进入源码我们可以发现其对WebMvcConfigurer实现,但是均没有具体实现。
当springboot 2.X版本时,此时我们进入接口WebMvcConfigurer中查看:
public interface WebMvcConfigurer {
/**
* Helps with configuring HandlerMappings path matching options such as trailing slash match,
* suffix registration, path matcher and path helper.
* Configured path matcher and path helper instances are shared for:
* <ul>
* <li>RequestMappings</li>
* <li>ViewControllerMappings</li>
* <li>ResourcesMappings</li>
* </ul>
* @since 4.0.3
*/
default void configurePathMatch(PathMatchConfigurer configurer) {
}
@Nullable
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
...等等...
}
对比一下:此接口中定义方法与我们平时定义的有何不同?
- 接口中的方法前多了一个关键字default,而且对其进行了实现。
最后插入一下springboot中对于springmvc扩展时的部分配置:
@Configuration
//@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {
//需要什么方法,从写什么方法
/**
* 配置嵌入式servlet容器
* @return
*/
// @Bean //一定要将这个定制器加入到容器中
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
//定制嵌入式的Servlet容器相关的规则
/* return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setPort(8081);
}
};*/
return (factory)->factory.setPort(8081);
}
/**
* 自定义一个controller
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//返回的路径也是经过thymeleaf模板引擎的
//这个相当于在controller中映射了一个@RequestMapping()
registry.addViewController("/page03").setViewName("thymeleaf01");
}
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
/**
* 自定义一个视图解析器
* 将被ContentNegotiatingViewResolver自动加载
*/
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
private static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
/**
*注册拦截器
*/
/* @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(Arrays.asList("/index.html","/","/user/login","/assert/**","/css/**","/js/**","/img/**","/webjars/**"))
.addPathPatterns("/**");
}*/
}
好了,上述听着可能比较繁琐,而我们比较直观的看见的是:当接口中的方法使用default修饰后,其实现类不强制去实现,也只是为了引入default关键字,那么接口中的默认方法和静态方法有啥注意事项呢?
接口默认方法的” 类优先” 原则 :
若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时 :
- 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
- 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突 。
好了,下面使用一个例子来说明上述两条规则:
定义一个接口Myfun:
/**
* 在以前接口中只能有:全局静态常量和抽象方法
* 在java8中添加默认方法、静态方法
*/
public interface Myfun {
default String getName(){
return "这是接口Myfun!";
}
public static void shwo(){
System.out.println("这是Myfun接口中的静态方法");
}
}
定义一个实体类TestDefault,其下有一个与接口Myfun同名方法:
public class TestDefault {
public String getName(){
return "这是类testDefault!";
}
}
定义一个实现类MyClass去继承第接口和实现类:
public class MyClass extends TestDefault implements Myfun{
}
此时若调用MyClass的getName()方法,那么调用的是接口中的方法,或者是实体类终的方法呢???
public class TestDefaultInterface {
/**
* 类MyClass同时继承了TestDefault、Myfun
* 在类TestDefault和接口Myfun均存在方法getName()
* 问题:当子孙类同时继承时,调用getName()方法时,调用的是谁的方法?
*/
//注意:当继承的多个接口中存在同名的默认方法时,那么实现类中必须对此方法进行重写
public static void main(String[] args) {
//调用的是类中的方法
System.out.println(new MyClass().getName());
//接口直接调用静态方法执行
Myfun.shwo();
}
}
结果:
这是类testDefault!
这是Myfun接口中的静态方法
5、Optional类
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