前言
在学习Java8的新特性之前,需要你对Java SE的基本语法有一定的了解,因此这篇文章不适合对于Java完全不了解的小白。在Java8中,新增了Lambda、Stream API等常用的语法,请阅读下面的文章学习。
一、Lambda表达式
1、Lambda初体验
public static void main(String[] args) {
//使用匿名内部类开启线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程中的代码:" + Thread.currentThread().getName());
}
}).start();
System.out.println("主线程中的代码:" + Thread.currentThread().getName());
//使用Lambda表达式写法,开启线程
new Thread(() -> {
System.out.println("新线程Lambda的代码:" + Thread.currentThread().getName());
}).start();
}
Lambda表达式的优点:简化了匿名内部类的使用,语法更加简单。
匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式。
2、Lambda语法规则
Lambda省去了面向对象的条条框框,在没有Lambda之前,我们声明方法是这样的 private void test () { 逻辑代码等 }
,或者像Main方法一样, public static void main(String[] args) { 逻辑代码等 }
,但是有了Lambda之后,我们不需要关注publib访问修饰符、void返回值、test方法名等,我们只需要关心方法的参数列表及方法体,因此,Lambda的标准格式由3个部分组成:
(参数类型 参数名称) -> {
业务逻辑代码;
}
(参数类型 参数名称)
:参数列表{ 业务逻辑代码 }
:方法体->
:分割参数列表和方法体
2.1、无参无返回值的Lambda
首先定义interface和抽象方法。
public interface UserService {
//接口中,随便定义一个方法
void show();
}
然后创建Main方法,对比匿名内部类与Lambda的语法区别。
public class App {
//定义一个静态方法,入参为UserService接口
public static void goShow(UserService userService) {
userService.show();
}
public static void main(String[] args) {
goShow(new UserService() {
@Override
public void show() {
System.out.println("匿名内部类的方式重写show方法...");
}
});
System.out.println("-----------分割符----------");
goShow(() -> {
System.out.println("Lambda的方式重写show方法...");
});
}
}
2.2、有参有返回值的Lambda
首先定义一个Person对象。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private Integer age;
}
然后创建多个Person对象存入List集合,并使用匿名内部类的方式实现Comparator,对List进行排序。
public class App {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("周杰伦", 33));
list.add(new Person("刘德华", 43));
list.add(new Person("郭富城", 36));
list.add(new Person("周星驰", 48));
//使用匿名内部类实现Comparator,从而排序
Collections.sort(list, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
//打印查看效果
for (Person person : list) {
System.out.println(person);
}
}
}
最后,我们使用Lambda代替匿名内部类,来达到排序效果。查看Collections.sort的源码可以发现,它需要一个Comparator的实现类,而我们通过Lambda implements了 Comparator,因此可以得出一个结论,Lambda简化了匿名内部类的语法。
public class App {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("周杰伦", 33));
list.add(new Person("刘德华", 43));
list.add(new Person("郭富城", 36));
list.add(new Person("周星驰", 48));
//使用Lambda实现Comparator,达到排序效果
Collections.sort(list, (Person o1, Person o2) -> {
return o1.getAge() - o2.getAge();
});
//打印查看效果
for (Person person : list) {
System.out.println(person);
}
}
}
3、@FunctionalInterface注解
使用Lambda和匿名内部类都可以实现某个接口,虽然Lambda表达式比匿名内部类语法简洁,但也不能完全代替匿名内部类。因为当使用Lambda实现某个接口时,Lambda只能重写父接口的一个抽象方法,而匿名内部类却可以重写父接口的所有抽象方法,这就决定了,由Labmda实现的接口只能包含一个抽象方法。
我们回到刚才的UserService来举例说明。
public interface UserService {
//接口中,随便定义一个方法
void show();
}
public class App {
//定义一个静态方法,入参为UserService接口
public static void goShow(UserService userService) {
userService.show();
}
public static void main(String[] args) {
goShow(new UserService() {
@Override
public void show() {
System.out.println("匿名内部类的方式重写show方法...");
}
});
System.out.println("-----------分割符----------");
goShow(() -> {
System.out.println("Lambda的方式重写show方法...");
});
}
}
重点来了,此时我们在UserService接口中,再新增一个show2()抽象方法。
public interface UserService {
//接口中,随便定义一个方法
void show();
void show2();
}
父接口新增show2()抽象方法,基于Java的继承语法规范,那么它的所有子类,也必须重写show2()方法才行。结果可想而知,我们在Main方法调用goShow()的时候使用了匿名内部类,因此,这个匿名内部类也要重写show2()方法。
public class App {
//定义一个静态方法,入参为UserService接口
public static void goShow(UserService userService) {
userService.show();
}
public static void main(String[] args) {
goShow(new UserService() {
@Override
public void show() {
System.out.println("匿名内部类的方式重写show方法了...");
}
@Override
public void show2() {
System.out.println("匿名内部类的方式重写show2方法了...");
}
});
System.out.println("-----------分割符----------");
//此处报错了,一条红色的杠杠!! ~~~ 0.0 >.< ~~~
goShow(() -> {
System.out.println("Lambda的方式重写show方法了...");
});
}
}
问题出现了,我们使用匿名内部类重写show()和show2()时轻而易举,但是使用Lambda却报错了,无法重写。因此得出一个结论,如果想使用Lambda实现某个接口,那么该接口必须只能包含一个抽象方法,因为Lambda无法重写多个抽象方法。
最后介绍一个注解,@FunctionalInterface
,这个注解是一个标志注解,被该注解修饰的接口只能声明一个抽象方法。
如下所示:
//被@FunctionalInterface修饰的接口只能有一个抽象方法
@FunctionalInterface
public interface UserService {
//接口中,随便定义一个方法
void show();
//void show2(); 当声明show2()时,@FunctionalInterface会报错。
}
4、Lambda的省略写法
在Lambda表达式的标准写法基础上,可以使用省略写法的规则为:
- 小括号内的参数类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return语句和分号
首先新建2个interface。
@FunctionalInterface
public interface TeacherService {
Integer getTeacher(String name);
}
@FunctionalInterface
public interface StudentService {
String getStudent(String name, Integer age);
}
然后使用Lambda表达式来实现这2个接口,演示完整写法与省略写法。
public class App {
public static void getTeacher(TeacherService teacherService) {
teacherService.getTeacher("李四");
}
public static String getStudent(StudentService studentService, String name, Integer age) {
return studentService.getStudent(name, age);
}
public static void main(String[] args) {
//teacher1
getTeacher((String myName) -> {
System.out.println("name:" + myName);
return 666;
});
//teacher2
getTeacher(myName -> {
System.out.println("name:" + myName);
return 666;
});
//student1写法
String student1 = getStudent((String myName, Integer myAge) -> {
return "姓名:" + myName+ ";年龄:" + myAge;
}, "张三", 23);
System.out.println("student1:" + student1);
//student2写法,省略写法
String student2 = getStudent((myName, myAge) -> "姓名:" + myName+ ";年龄:" + myAge, "王五", 35);
System.out.println("student2:" + student2);
}
}
App的getStudent方法需要3个参数:StudentService参数、String参数、Integer参数,因此在调用getStudent时需要传入这3个参数,此处使用Lambda表达式的方式来实现StudentService接口。
在teacher1写法处,使用了Lambda表达式的完整写法:Lambda小括号传入了String类型的myName,返回Integer类型的值,这是其父接口TeacherService的getTeacher方法规定的。我们通过 Ctrl + 左键 点击 ->
跳转到TeacherService接口,便可查看getTeacher方法的出入参要求。
在teacher1写法处,使用了Lambda表达式的省略写法:因为Lambda内只有一个参数,所以可以省略小括号。
在student1写法处,使用了Lambda表达式的完整写法:传入了String myName, Integer myAge,并返回了String类型的结果。之所以入参需要String和Interger的参数,返回值需要String类型的结果,那是由StudentService接口的getStudent抽象方法决定的。因为Lambda表达式本质上是代替了匿名内部类,实现了StudentService接口并重写getStudent方法,因此要遵命父接口的规定。我们通过 Ctrl + 左键 点击 ->
跳转到StudentService接口,便可查看getStudent方法的出入参要求。
在student2写法处,使用了Lambda表达式的省略写法:省略了Lambda表达式的参数类型、大括号及return关键字。
通过上面的例子,我们对Lambda表达式有了更清晰的认识。Lambda表达式简化了匿名内部类语法,Lambda表达式小括号内的入参,以及大括号内的出参,都是由它实现的父接口的抽象方法决定的。Lambda表达式的作用是实现了某个接口,因此,若想使用Lambda表达式,就要求方法的入参是某个接口,由Lambda来实现。
虽然Lambda表达式提供了省略写法,使我们的代码看起来更简洁,但是我却不建议过度使用省略写法,至少省略大括号和return关键字这点我是极度反对的,因为这会导致代码的可读性降低,给开发人员造成困扰。因此,省略写法要酌情考虑。
二、接口中新增的方法
1、JDK8中接口增强
在JDK8之前针对接口做了增强,在JDK8之前,接口中只能包含静态常量和抽象方法。
interface 接口名{
静态常量;
抽象方法;
}
public interface TeacherService {
String name = "杰克";
Integer getTeacher(String name);
}
JDK8之后对接口做了增加,接口中可以有默认方法和静态方法。
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
2、默认方法
2.1、为什么要增加默认方法
在JDK8以前,接口中只能有抽象方法和静态变量,会存在以下问题:
如果接口中新增抽象方法,那么实现类都必须要重写这个抽象方法,不利于接口扩展。
比如,假设有个A接口,有数十个类实现了A接口并重写了它的所有抽象方法,这时因为业务拓展,需要在A接口中新增一个抽象方法,我们难道要在A接口的所有实现类(子类)中都重写这个新增的抽象方法吗?那代码量未免也太大了些!在JDK8以前,实现类是必须要重写父接口的所有抽象方法的,可是A接口新增的抽象方法所涉及的业务,未必就是实现类需要的,重写也就没有意义,那么有没有一种更好的方式,既然在A接口中增加方法,又不需要它的所有实现类重写呢?答案是肯定的,基于此,JDK8新增了默认方法。
2.2、接口默认方法的格式
接口中默认方法的语法格式是:
interface 接口名{
修饰符 default 返回值类型 方法名{
方法体;
}
}
2.3、举例说明默认方法
我们举个例子类说明,首先声明一个动物接口,狗狗类和鸽子类都实现动物接口,并重写它的抽象方法。
public interface Animal {
void eat();
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("狗狗会吃饭...");
}
}
public class Pigeon implements Animal{
@Override
public void eat() {
System.out.println("鸽子会吃饭...");
}
}
再写一个main方法,测试eat方法。
public class TestInter {
public static void main(String[] args) {
Animal dog = new Dog();
dog.eat();
Animal pigeon = new Pigeon();
pigeon.eat();
}
}
这是一个很简单的案例,控制台输出结果如下所示:
狗狗会吃饭...
鸽子会吃饭...
接下来,我们改写Animal接口。要在Animal接口中新增一个swim方法,我认为狗狗是会游泳的,而鸽子是不会游泳的,基于这种情况,狗狗类应该重写父接口的swim方法,鸽子类不应该重写swim方法。这种场景下,就要使用默认方法了!
public interface Animal {
void eat();
//游泳
public default void swim() {
System.out.println("除了少数动物外,大多数动物都应该是会游泳的。。。");
}
}
狗狗会游泳,因此Dog类重写Animal接口的swim方法。
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("狗狗会吃饭...");
}
@Override
public void swim(){
System.out.println("狗狗不仅会游泳,而且技术相当不错的。。");
}
}
鸽子不会游泳,所以代码不做任何改动。
public class Pigeon implements Animal{
@Override
public void eat() {
System.out.println("鸽子会吃饭...");
}
}
最后我们在main方法中测试并打印结果。
public class TestInter {
public static void main(String[] args) {
Animal dog = new Dog();
dog.eat();
Animal pigeon = new Pigeon();
pigeon.eat();
//测试狗狗和鸽子的游泳
dog.swim();
pigeon.swim();
}
}
输出结果如下所示:
狗狗会吃饭...
鸽子会吃饭...
狗狗不仅会游泳,而且技术相当不错的。。
除了少数动物外,大多数动物都应该是会游泳的。。。
我们发现,Dog类重写了Animal接口的swim方法,执行了Dog重写后的逻辑;而Pigeon类没有重写swim方法,执行了Animal的默认逻辑。
因此得出一个结论,接口中的默认方法有两种使用方式:
- 实现类直接调用接口的默认方法,参考Pigeon类。
- 实现类重写接口的默认方法,参考Dog类。
3、静态方法
JDK8中为接口新增了静态方法,作用也是为了接口的扩展
3.1、语法规则
顾名思义,所谓静态方法,就是被static修饰的方法,可以直接通过接口名.静态方法名直接调用,和类中的静态方法使用方式相同。需要注意的是,接口中的静态方法,不可以被实现类(子类)重写。
interface 接口名{
修饰符 static 返回值类型 方法名{
方法体;
}
}
3.2、举例说明静态方法
首先,改写Animal接口,新增breathe静态方法。Dog类和Pigeon类没有任何修改。
public interface Animal {
void eat();
//游泳
public default void swim() {
System.out.println("除了少数动物外,大多数动物都应该是会游泳的。。。");
}
//呼吸
public static void breathe() {
System.out.println("任何动物都需要呼吸。。。");
}
}
然后再main方法中测试静态方法,并观察效果。
public class TestInter {
public static void main(String[] args) {
//调用Animal的静态方法
Animal.breathe();
}
}
注意,Dog类和Pigeon类并没有重写新增的breathe静态方法,那是因为静态方法归父接口所有,实现类无法重写,也无法通过实现类调用静态方法。只能通过接口名.静态方法名的方式调用。
4、默认方法和静态方法的区别
- 默认方法通过对象实例调用,静态方法用过接口名调用。
- 默认方法可以被实现类继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被实现类继承,实现类不能重写接口的静态方法,只能使用接口名调用。
三、函数式接口
1、函数式接口的由来
我们知道使用Lambda表达式的前提是需要有函数式接口(即被@FunctionalInterface修饰的接口),而Lambda表达式使用时不关心接口名、也不关心抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让Lambda表达式使用更多的方法,在JDK8中提供了大量常用的函数式接口。
我们通过一段代码,来理解函数式接口。
首先定义一个函数式接口,Operator:
//被@FunctionalInterface修饰的接口,就是函数式接口
@FunctionalInterface
public interface Operator {
//某个操作,可以实现加法或者减法
int operate(int[] arr);
}
然后定义一个调用Operator的地方:
public class Demo01Fun {
//调用Operator函数式接口
public static void fun1(Operator operator) {
int[] arr = {1, 2, 3, 4};
int operate = operator.operate(arr);
System.out.println("operate = " + operate);
}
public static void main(String[] args) {
//eg1:调用Operator接口实现相加法
fun1((arr) -> {
int sum = 0;
for (int item : arr) {
sum += item;
}
return sum;
});
//eg2:调用Operator接口实现减法
fun1((arr) -> {
int sum = 0;
for (int item : arr) {
sum -= item;
}
return sum;
});
}
}
最后输出结果,如下:
operate = 10
operate = -10
在上面的案例中,main方法使用Lambda表达式重写Operator函数式接口实现了相加与相减的数学运算。我们发现,加法与减法,两个截然相反的算法,竟然通过重写同一个抽象方法实现了。在实现数学运算的过程中,业务代码对Operator接口名和operate方法名毫不关心,只需要关注入参以及返回值就行,甚至只要入参是int数组类型、返回值是int类型的函数式接口,都可以代替Operator接口实现相加相减的数学运算,那么这就意味着Operator接口是一个可有可无的接口,白白增加了代码量。为了解决这种没必要的代码,JDK8提供了很多函数式接口供Java程序员使用,程序员们再也不必为了实现业务而重复定义多余的接口。
2、函数式接口介绍
在JDK8中为我们提供了函数式接口,主要是在rt.jar文件下的java.util.function包中。有了这些函数式接口,我们在使用Lambda表达式的时候,可以根据不同的业务场景使用不同的接口,不必再自己编写多余的函数式接口。
下面给大家罗列几个常用的函数式接口,更多接口大家可以去rt.jar文件下的java.util.function包下自行寻找。
2.1、Supplier
无参有返回值的接口,使用的时候需要指定一个泛型来定义方法的返回值类型。源码如下:
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
通过一则案例来演示Supplier的用法,利用Supplier实现数组排序,并返回最大值:
public class SupplierTest {
private static void fun1(Supplier<Integer> supplier) {
Integer integer = supplier.get();
System.out.println("result = " + integer);
}
public static void main(String[] args) {
fun1(() -> {
int[] arr = {34, 2, 47, 15, 99, 67};
//从小到大排序后返回最大值
Arrays.sort(arr);
return arr[arr.length - 1];
});
}
}
fun1()方法需要Supplier类型的参数,所以在main方法调用fun1()方法时,使用Lambda表达式实现了Supplier接口并重写了它的get方法,完成排序并返回最大值的功能。
2.2、Consumer
有参无返回值的接口,Supplier接口是用来生产数据的,而Consumer接口则是用来消费数据的,使用的时候需要指定一个泛型来定义方法的参数类型。源码如下:
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
Consumer函数式接口在日常开发中经常使用,比如集合的forEach循环时,如:
List<Person> list = new ArrayList<>();
list.add(new Person("周杰伦", 33));
list.add(new Person("刘德华", 43));
list.forEach(new Consumer<Person>() {
@Override
public void accept(Person o) {
System.out.println("bb->" + o);
}
});
List利用Consumer实现了遍历后的业务逻辑,我们仿照List,实现大写转换成小写的业务,当然,你也可以根据自己的需求实现其他的业务逻辑。
public class ConsumerTest {
private static void fun1(Consumer<String> action) {
for(int i =0;i<10;i++){
action.accept("HELLO WORLD...");
}
}
public static void main(String[] args) {
fun1((msg) -> {
String s = msg.toLowerCase();
System.out.println("转换为小写 :" + s);
});
}
}
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);
}
2.4、Predicate
有参有返回值的接口,源码如下:
@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);
}
四、方法引用
1、为什么要用方法引用
1.1、Lambda冗余
Lambda表达式诞生之初便是为了解决匿名内部类语法冗余的问题,可是当Lambda表达式也出现冗余,又该如何解决呢?
请观看下列案例:
public class Demo1 {
//调用Consumer的accept方法
private static void printCount(Consumer<int[]> consumer) {
int[] a = {10, 20, 30};
consumer.accept(a);
}
//数组求和
public static void getTotal(int arr[]) {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
}
public static void main(String[] args) {
//Lambda重写Consumer实现数组求和
printCount((arr) -> {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
});
}
}
在上述的案例中,main方法重写Consumer实现了数组求和,getTotal也实现了数组求和,这是两块业务逻辑完全一样的代码。那么能不能在man方法的Lambda表达式中直接使用getTotal方法的逻辑呢?这样的话岂不是节省了代码,答案是肯定的,为了解决这种现象,JDK8新增了方法引用。
1.2、解决Lambda冗余
我们改写上面的案例,来初步认识下方法引用。
public class Demo1 {
//调用Consumer的accept方法
private static void printCount(Consumer<int[]> consumer) {
int[] a = {10, 20, 30};
consumer.accept(a);
}
//数组求和
public static void getTotal(int arr[]) {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
}
public static void main(String[] args) {
//Lambda使用方法引用,重写Consumer实现数组求和
printCount(Demo1::getTotal);
}
}
改写案例后,使用Demo1::getTotal代替了Labmda表达式,实现了同样的业务逻辑。
需要注意的是,这里并不是方法调用。通常情况下,调用getTotal方法是 Demo1.getTotal(new int[]{1, 2, 3}) 这样的写法,需要传一个int类型的数组,但是方法引用不同于过去的方法调用,因为 Demo1::getTotal 并没有传递任何参数,你可以把它理解成复制一块相同的代码,而不是调用。
1.3、匿名内部类 VS Lambda表达式 VS 方法引用
我觉得有必要给大家对比下匿名内部类、Lambda表达式和方法引用的语法区别,让大家对方法引用更清晰的认识。
public class Demo1 {
//调用Consumer的accept方法
private static void printCount(Consumer<int[]> consumer) {
int[] a = {10, 20, 30};
consumer.accept(a);
}
//数组求和
public static void getTotal(int arr[]) {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
}
public static void main(String[] args) {
//eg1:调用getTotal方法
Demo1.getTotal(new int[]{1, 2, 3});
//eg2:匿名内部类重写Consumer实现数组求和
printCount(new Consumer<int[]>() {
@Override
public void accept(int[] arr) {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
}
});
//eg3:Lambda重写Consumer实现数组求和
printCount((arr) -> {
int sum = 0;
for (int item : arr) {
sum += item;
}
System.out.println("数组相加之和:" + sum);
});
//eg4:Lambda使用方法引用,重写Consumer实现数组求和
printCount(Demo1::getTotal);
}
}
Lambda产生的目的在于简化匿名内部类的写法,方法引用产生的目的在于简化Lambda的写法,我们继续通过之前的案例,来对比一下三者的语法区别:
eg1调用getTotal方法,传入int类型的数组;
eg2使用匿名内部类重写Consumer,实现了数组求和;
eg3使用Lambda表达式简化了匿名内部类的写法,重写Consumer,实现了数组求和,使代码更加简洁;
eg4是本次的重点,我们发现eg3的代码逻辑和getTotal方法的代码逻辑一模一样,没必要再写重复的代码,故而使用方法引用,直接引用了getTotal方法的逻辑,实现了同样的功能,最大程度地简化了Lambda的代码。
注意:方法引用和方法调用是两回事,不能混为一谈!
2、方法引用的格式
符号表示:::
符号说明:双冒号为方法引用运算符,而它所在的表达式被称为方法引用
应用场景:如果Lambda表达式所要实现的代码,已经有其他方法存在相同的代码,那么则可以使用方法引用。
常见的引用方式:
- 对象名::方法名
- 类名::静态方法名
- 类名::普通方法名
- 类名::new
- 数组::new
常用的引用方式就是这5种,我们一一举例说明。
2.1、对象名::方法名
这是最常用的一种用法。如果一个类中的已经存在了一个成员方法,则可以通过对象名引用成员方法。
通过下面这块代码演示:
public static void main(String[] args) {
Date now = new Date();
//eg1
Supplier<Long> supplier1 = new Supplier<Long>() {
@Override
public Long get() {
return now.getTime();
}
};
System.out.println(supplier1.get());
//eg2
Supplier<Long> supplier2 = () -> {
return now.getTime();
};
System.out.println(supplier2.get());
}
此处还没有使用方法引用代替Lambda表达式,我先用匿名内部类和Lambda表达式预热一下,以防大家看不懂后面的代码。
Supplier接口是java.util.function包下的函数式接口,eg1使用匿名内部类的方式重写了Supplier接口,eg2使用lambda表达式重写了Supplier接口。
在上面的代码中,我们发现eg2 Lambda表达式中重写Supplier接口时,方法体内使用了now.getTime()方法,因此,我们可以使用方法引用来代替Lambda表达式。
public static void main(String[] args) {
Date now = new Date();
//eg3
Supplier<Long> supplier3 = now::getTime;
System.out.println(supplier3.get());
}
方法引用的注意事项:
- 被引用的方法,参数要和函数式接口中的抽象方法参数一样。
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值。
2.2、类名::静态方法名
public static void main(String[] args) {
//eg1 Lambda表达式
Supplier<Long> supplier1 = () -> {
return System.currentTimeMillis();
};
System.out.println(supplier1.get());
//eg2 方法引用
Supplier<Long> supplier2 = System::currentTimeMillis;
System.out.println(supplier2.get());
}
eg1使用Lambda表达式输出了当前毫秒值,在Lambda表达式的方法体中没有其他业务逻辑,和System类的currentTimeMillis静态方法业务逻辑完全一致,因此在eg2中使用方法引用,引用System的currentTimeMillis方法代替了Lambda表达式。
2.3、类名::普通方法名
public static void main(String[] args) {
Function<String, Integer> function1 = (s) -> {
return s.length();
};
System.out.println(function1.apply("hello"));
//通过方法引用来实现
Function<String, Integer> function2 = String::length;
System.out.println(function2.apply("hello world"));
}
2.4、类名::new
由于构造器的名称和类名完全一致,所以构造器引用使用:: new
的格式使用。
public static void main(String[] args) {
Supplier<Person> supplier = () -> {
return new Person();
};
System.out.println(supplier.get());
//方法引用
Supplier<Person> supplier2 = Person::new;
System.out.println(supplier2.get());
}
2.5、数组::new
public static void main(String[] args) {
Function<Integer, String[]> fun1 = (len) -> {
return new String[len];
};
String[] a1 = fun1.apply(3);
System.out.println("数组a1的长度是" + a1.length);
//方法引用
Function<Integer, String[]> fun2 = String[]::new;
String[] a2 = fun2.apply(4);
System.out.println("数组a2的长度是" + a2.length);
}
3、方法引用小结
方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加得精简,也可以理解为Lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。
五、Stream API
1、Stream流的概念
Stream流是对Java集合(Collection)对象功能的增强,与Lambda表达式结合,可以提高编程效率。 Stream API能让我们快速完成许多复杂的操作,如筛选、查找、去重、统计、匹配等,逐渐代替之前for循环的写法。
2、Stream流的获取方式
2.1、根据Collection接口的stream默认方法获取
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
Stream<Object> stream = list.stream();
Set<Object> set = new HashSet<>();
Stream<Object> stream1 = set.stream();
Vector<Object> vector = new Vector<>();
Stream<Object> stream2 = vector.stream();
Map<String, Object> map = new HashMap<>();
Stream<String> stream3 = map.keySet().stream();
Stream<Object> stream4 = map.values().stream();
}
Collection的实现类,都可以通过stream方法获取Stream对象,比如List,Set等;Map不属于Collection,所以不能获取Stream对象,但是Map的Key和Value可以。
2.2、根据Stream的of方法
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "a3");
Integer[] arr = {1, 2, 3, 4};
Stream<Integer> arr1 = Stream.of(arr);
}
Stream提供了of方法,可以操作数组,获取Stream对象。
3、Stream常用方法介绍
方法名 | 方法作用 | 返回值类型 | 方法种类 |
---|---|---|---|
count | 统计个数 | long | 终结 |
forEach | 逐一处理 | void | 终结 |
filter | 过滤 | Stream | 函数拼接 |
limit | 取用前几个 | Stream | 函数拼接 |
skip | 跳过前几个 | Stream | 函数拼接 |
map | 映射 | Stream | 函数拼接 |
concat | 组合 | Stream | 函数拼接 |
终结方法: 返回值类型不再是Stream类型的方法,不再支持链式调用。
非终结方法: 返回值类型仍然是Stream类型的方法,支持链式调用。
Stream注意事项:
- Stream只能操作一次
- Stream方法返回的是新的流
- Stream不调用终结方法,中间调用的函数拼接方法都不会调用(Stream调用必须以终结方法结尾才会执行中间调用的非终结方法)
3.1、forEach
forEach方法用来遍历流中的数据。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
a1.forEach(System.out::println);
}
3.2、count
count方法用来统计流中的元素个数。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
long count = a1.count();
System.out.println("a1流元素中的个数:"+count);
}
3.3、filter
filter方法用来过滤数据,返回符合条件的数据。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
a1.filter((item) -> {
return item.contains("a");
}).forEach(System.out::println);
//缩写
Stream<String> a2 = Stream.of("a1", "a2", "b1", "b2");
a2.filter(item -> item.contains("b")).forEach(System.out::println);
}
3.4、limit
limit方法可以对流进行截取处理,只取前n个数据。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
a1.limit(3).forEach(System.out::println);
}
3.5、skip
skip方法可以跳过前n个元素。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
a1.skip(3).forEach(System.out::println);
}
3.6、map
如果需要将流中的元素映射到另一个流中,可以使用map方法。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("1", "2", "3", "4", "5", "6", "7");
a1.map(msg -> {
return Integer.parseInt(msg);
}).forEach(System.out::println);
//缩写形式
//a1.map(Integer::parseInt).forEach(System.out::println);
}
3.7、sorted
sorted方法可以对流数据排序。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("2", "1", "3", "7", "4", "6", "5");
a1.sorted(((o1, o2) -> {
return Integer.parseInt(o2) - Integer.parseInt(o1);
})).forEach(System.out::println);
//缩写形式
//a1.sorted(((o1, o2) -> Integer.parseInt(o2) - Integer.parseInt(o1))).forEach(System.out::println);
}
3.8、distinct
如果要去掉重复数据,可以使用distinct方法。
public static void main(String[] args) {
Stream<String> a1 = Stream.of("2", "1", "3", "3", "3", "6", "5");
a1.distinct().forEach(System.out::println);
}
对于某个实体类,如果想根据某个字段去重,需要重写实体类的equals方法和hashCode方法。
下例根据Person的name属性实现去重。
public class Person {
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public class demo8 {
public static void main(String[] args) {
Stream<Person> personStream = Stream.of(new Person("张三", 23),
new Person("李四", 23),
new Person("张三", 24));
personStream.distinct().forEach(System.out::println);
}
}
输出结果如下:
Person{name='张三', age=23}
Person{name='李四', age=23}
结论: Stream流中的distinct方法对于基本数据类型是可以直接去重的,但是对于自定义类型,需要重写hashCode和equals方法来移除重复元素。
3.9、reduce
如果需要将所有数据归纳得到一个数据,可以使用reduce方法。
public static void main(String[] args) {
//求和
Stream<Integer> a1 = Stream.of(2, 10, 15, 50);
Integer sum = a1.reduce(0, (x, y) -> {
System.out.println("x:" + x + ";y=" + y);
return x + y;
});
System.out.println("sum:" + sum);
System.out.println("**********分隔符*******");
//求最大值
Stream<Integer> a2 = Stream.of(23, 65, 14, 45);
Integer max = a2.reduce(0, (x, y) -> {
return x > y ? x : y;
});
System.out.println("max:" + max);
}
输出如下:
x:0;y=2
x:2;y=10
x:12;y=15
x:27;y=50
sum:77
**********分隔符*******
max:65
结合输出结果,分析得出:第一次循环时,会把默认值赋值给x,之后每次循环会将上一次操作的结果赋值给x。y就是每次从流中获取的元素。
4、数据收集
4.1、collect
通过collect方法,我们可以把流中的数据,收集到集合中。
public static void main(String[] args) {
//Stream转List
List<String> list = Stream.of("aaa", "bbb", "ccc", "aaa")
.collect(Collectors.toList());
//Stream转ArrayList
ArrayList<String> arrayList = Stream.of("aaa", "bbb", "ccc", "aaa")
.collect(Collectors.toCollection(() -> new ArrayList<>()));
//Stream转Set
Set<String> set = Stream.of("aaa", "bbb", "ccc", "aaa")
.collect(Collectors.toSet());
//Stream转HashSet
HashSet<String> hashSet = Stream.of("aaa", "bbb", "ccc", "aaa")
.collect(Collectors.toCollection(() -> new HashSet<>()));
}
如果想把流中的数据收集到Map中,也是可以的。
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("李四", 18, 174));
list.add(new Student("王五", 21, 173));
//收集到Map,注意不能出现相同的key
Map<String, Student> collect = list.stream().collect(Collectors.toMap(Student::getName, item -> item));
System.out.println(collect);
}
输出如下:
{李四=Student{name='李四', age=18, height=174}, 张三=Student{name='张三', age=15, height=170}, 王五=Student{name='王五', age=21, height=173}}
需要注意的是,如果在收集到Map的过程中,出现相同的key,是会报错的。需要采用如下方式:
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("张三", 15, 172));
list.add(new Student("李四", 18, 174));
list.add(new Student("王五", 21, 173));
//收集到Map,可以出现相同的key
Map<String, Student> collect = list.stream().collect(Collectors.toMap(Student::getName, item -> item,(k, v) -> k));
System.out.println(collect);
}
4.2、toArray
通过toArray方法,我们可以把流中的数据,收集到数组中。
public static void main(String[] args) {
//默认转成Object类型的数组
Object[] objects = Stream.of("aaa", "bbb", "ccc", "aaa")
.toArray();
//使用匿名内部类转成String类型的数组
String[] strings = Stream.of("aaa", "bbb", "ccc", "aaa")
.toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
//使用Lambda转成String类型的数组
String[] strings2 = Stream.of("aaa", "bbb", "ccc", "aaa")
.toArray((value -> new String[value]));
//使用方法引用转成String类型的数组
String[] strings3 = Stream.of("aaa", "bbb", "ccc", "aaa")
.toArray((String[]::new));
}
5、数据聚合与分组
为了方便演示,先定义一个学生类:
//学生类
public class Student {
//姓名
private String name;
//年龄
private Integer age;
//身高
private Integer height;
//省略set、get方法等
}
5.1、数据聚合
在一个集合中,如果想根据某个字段,查询出最大值、或平均值,不再需要for循环,Stream提供了更简洁的方式:
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("张三", 16, 175));
list.add(new Student("张三", 16, 173));
list.add(new Student("李四", 18, 174));
list.add(new Student("李四", 18, 168));
list.add(new Student("李四", 20, 178));
list.add(new Student("王五", 21, 173));
//查询年龄最大的学生
Optional<Student> maxAgeStudent = list.stream()
.collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
System.out.println("年龄的最大值:" + maxAgeStudent.get().getAge());
System.out.println("**************分隔符*************");
//查询年龄的平均数
Double avg = list.stream().collect(Collectors.averagingLong(item -> item.getAge()));
System.out.println("年龄的平均值:" + avg);
}
输出如下:
年龄的最大值:21
**************分隔符*************
年龄的平均值:17.714285714285715
5.2、数据分组
Stream提供了对流中数据分组的方式,利用它,可以根据某个字段进行分组操作。
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("张三", 16, 175));
list.add(new Student("张三", 16, 173));
list.add(new Student("李四", 18, 174));
list.add(new Student("李四", 18, 168));
list.add(new Student("李四", 20, 178));
list.add(new Student("王五", 21, 173));
//按照学生姓名进行分组
Map<String, List<Student>> collect = list.stream().collect(Collectors.groupingBy(item -> item.getName()));
collect.forEach((key, value) -> {
System.out.println("名字是 " + key + " 的学生有:");
value.forEach(student -> {
System.out.println("\t" + student.toString());
});
System.out.println("-----------------");
});
System.out.println("**************分隔符*************");
//先按照学生姓名分组,再按照年龄分组
Map<String, Map<Integer, List<Student>>> collect1 = list.stream()
.collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(Student::getAge)));
collect1.forEach((key, value) -> {
System.out.println("名字是 " + key + " 的学生有:");
value.forEach((k1, v1) -> {
System.out.println("\t年龄是 " + k1 + " 的学生有:");
v1.forEach(student -> {
System.out.println("\t\t" + student.toString());
});
});
System.out.println("-----------------");
});
}
输出如下:
名字是 李四 的学生有:
Student{name='李四', age=18, height=174}
Student{name='李四', age=18, height=168}
Student{name='李四', age=20, height=178}
-----------------
名字是 张三 的学生有:
Student{name='张三', age=15, height=170}
Student{name='张三', age=16, height=175}
Student{name='张三', age=16, height=173}
-----------------
名字是 王五 的学生有:
Student{name='王五', age=21, height=173}
-----------------
**************分隔符*************
名字是 李四 的学生有:
年龄是 18 的学生有:
Student{name='李四', age=18, height=174}
Student{name='李四', age=18, height=168}
年龄是 20 的学生有:
Student{name='李四', age=20, height=178}
-----------------
名字是 张三 的学生有:
年龄是 16 的学生有:
Student{name='张三', age=16, height=175}
Student{name='张三', age=16, height=173}
年龄是 15 的学生有:
Student{name='张三', age=15, height=170}
-----------------
名字是 王五 的学生有:
年龄是 21 的学生有:
Student{name='王五', age=21, height=173}
-----------------
6、数据分区
可以通过Collectors.partitioningBy()方法,把流中的数据依据某个字段分成true和false两个列表。该效果和Collectors.groupingBy()类似。
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("张三", 16, 175));
list.add(new Student("张三", 16, 173));
list.add(new Student("李四", 18, 174));
list.add(new Student("李四", 18, 168));
list.add(new Student("李四", 20, 178));
list.add(new Student("王五", 21, 173));
//根据年龄来分区
Map<Boolean, List<Student>> collect = list.stream().collect(Collectors.partitioningBy(item -> item.getAge() > 18));
collect.forEach((key, value) -> {
System.out.println("分区是 " + key + " 的学生有:");
value.forEach(student -> {
System.out.println("\t" + student.toString());
});
System.out.println("-----------------");
});
}
输出如下:
分区是 false 的学生有:
Student{name='张三', age=15, height=170}
Student{name='张三', age=16, height=175}
Student{name='张三', age=16, height=173}
Student{name='李四', age=18, height=174}
Student{name='李四', age=18, height=168}
-----------------
分区是 true 的学生有:
Student{name='李四', age=20, height=178}
Student{name='王五', age=21, height=173}
-----------------
7、数据拼接
使用Collectors.joining()方法,可以把流中的数据拼接到一个字符串中。下例读取集合中的名字,拼接到一个字符串中,使用、分隔:
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("张三", 15, 170));
list.add(new Student("李四", 20, 178));
list.add(new Student("王五", 21, 173));
//读取集合中的姓名,拼接到一个字符串中,使用、分隔
String collect = list.stream().map(item -> item.getName())
.collect(Collectors.joining("、"));
System.out.println("拼接后的结果:" + collect);
}
拼接后的结果:张三、李四、王五
8、并行流
之前使用的Stream流是单线程的,为了提高流的处理效率,Stream提供了并行流,多线程地处理数据,提升效率。下面分别演示并行流和串行流。
8.1、串行流
Stream默认的是串行流,单线程的处理方式。请看下面的案例:
public static void main(String[] args) {
Stream.of("a", "b", "c", "d", "a")
//随便使用filter方法,为了给大家看输出的线程名
.filter(item -> {
System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
return item.equals("a");
}).count();
}
输出如下:
当前线程名:main;item:a
当前线程名:main;item:b
当前线程名:main;item:c
当前线程名:main;item:d
当前线程名:main;item:a
在上面的案例中,我们输出了线程名,发现是一样的,这证明Stream默认是单线程的。
8.2、并行流
可以通过2种方式开启并行流:
- Stream的parallel()方法。
- Collection的parallelStream()方法。
public static void main(String[] args) {
Stream.of("a", "b", "c", "d", "a")
.parallel()
//随便使用filter方法,为了给大家看输出的线程名
.filter(item -> {
System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
return item.equals("a");
}).count();
System.out.println("-------------分割线-----------");
List<String> list = new ArrayList<>();
list.add("H");
list.add("I");
list.add("J");
list.parallelStream()
.filter(item -> {
System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
return item.equals("a");
}).count();
}
输出如下:
当前线程名:main;item:c
当前线程名:ForkJoinPool.commonPool-worker-9;item:b
当前线程名:ForkJoinPool.commonPool-worker-11;item:a
当前线程名:ForkJoinPool.commonPool-worker-2;item:a
当前线程名:ForkJoinPool.commonPool-worker-4;item:d
-------------分割线-----------
当前线程名:main;item:I
当前线程名:ForkJoinPool.commonPool-worker-2;item:J
当前线程名:ForkJoinPool.commonPool-worker-4;item:H
本文只罗列了一部分Stream API,大家可以参考另一篇文章,了解更多API用法。Java Stream流详解
六、Optional类
Optional是用来存储对象的容器,使用这个容器可以避免空指针异常。
1、为什么使用Optional
观察下列代码,在没有Optional类之前,为了避免空指针异常,需要在代码中使用 if
来判断,不够简洁。
public static void main(String[] args) {
String name = null;
if (name != null) {
System.out.println("name的长度是:" + name.length());
} else {
System.out.println("name为空");
}
}
2、创建Optional容器的方式
Optional提供了3个静态方法来创建Optional容器:
Optional.of(T t)
of()方法需要传入一个参数,且参数不能为null,否则会被空指针异常。Optional.empty()
empty()方法用来创建一个空的Optional容器,不需要传入参数。Optional.ofNullable(T t)
ofNullable()方法是of()和empty()的整合版,需要传入一个参数,允许参数为空。
public static void main(String[] args) {
//使用of方法创建Optional对象
Optional<String> jack = Optional.of("jack");
//使用empty方法创建Optional对象
Optional<Object> empty = Optional.empty();
//使用ofNullable方法创建Optional对象
Optional<String> tom = Optional.ofNullable("Tom");
Optional<String> o = Optional.ofNullable(null);
}
ofNullable()方法会根据传入的参数是否为null,调用of()或empty(),因此更加灵活,工作中建议使用ofNullable()来创建Optional对象。
3、Optional的常用方法
3.1、get方法
gat用来获取Optional容器中的对象。
public static void main(String[] args) {
String name = "jack";
//使用of方法创建Optional对象
Optional<String> nameOpt = Optional.ofNullable(name);
System.out.println("name的值是:" + nameOpt.get());
}
name的值是:jack
如果Optional容器中的对象是null,get时会抛出 No value present
错误提示。因此,get方法常和isPresent方法一起用。
3.2、isPresent方法
isPresent方法用来判断容器中的值是否为null,如果有值返回true,无值返回false。
public static void main(String[] args) {
String name = null;
Optional<String> nameOpt = Optional.ofNullable(name);
if (nameOpt.isPresent()) {
System.out.println("name的值是:" + nameOpt.get());
} else {
System.out.println("nameOpt是一个空Optional对象");
}
}
nameOpt是一个空Optional对象
3.3、orElse方法
orElse会返回容器中的值,如果值为null,orElse会返回一个默认值。
public static void main(String[] args) {
Optional<String> o1 = Optional.ofNullable("jack");
Optional<String> o2 = Optional.ofNullable(null);
String q1 = o1.orElse("默认值");
String q2 = o2.orElse("默认值");
System.out.println("o1的值:" + q1);
System.out.println("o2的值:" + q2);
}
o1的值:jack
o2的值:默认值
3.4、orElseGet方法
orElseGet与orElse作用相同,唯一的区别在于orElseGet需要传入Supplier函数式接口,可以使用Lambda表达式来入参。当业务逻辑比较复杂时,可以使用orElseGet方法来代替orElse方法。
public static void main(String[] args) {
Optional<String> o = Optional.ofNullable(null);
String s = o.orElseGet(() -> {
//其他业务逻辑
return "默认值";
});
System.out.println("o的值是:" + s);
}
o的值是:默认值
3.5、ifPresent方法
public static void main(String[] args) {
Optional<String> op1 = Optional.ofNullable("jack");
//如果容器有值,就执行某些操作
op1.ifPresent((s) -> {
System.out.println(s + "666");
});
}
jack666
七、新时间日期API
1、常用API介绍
JDK8中增加了一套全新的日期时间API,是线程安全的,位于java.time包下,下面是一些关键类:
- LocalDate:表示日期,包含年月日,格式为2022-12-18
- LocalTime:表示时间,包含时分秒,格式为16:38:54.158549300
- LocalDataTime:表示日期时间,包含年月日时分秒,格式为2018-09-06T15:33:56.750
- DataTimeFormatter:日期时间格式化类
- Instant:时间戳,表示一个特定的时间瞬间
- Duration:用于计算2个时间(LocalTime,时分秒)的距离
- Period:用于计算2个日期(LocalDate,年月日)的距离
- ZonedDateTime:包含时区的时间
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年有366天。此外Java8还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中华民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
2、日期时间类的常用操作
2.1 LocalDate类
public static void main(String[] args) {
//创建指定的日期
LocalDate date1 = LocalDate.of(2022, 3, 1);
System.out.println("date1:" + date1);
//得到当前的日期
LocalDate date2 = LocalDate.now();
System.out.println("date2:" + date2);
//根据LocalDate对象获取对应的日期信息
System.out.println("year:"+date2.getYear());
System.out.println("month:"+date2.getMonthValue());
System.out.println("day:"+date2.getDayOfMonth());
}
date1:2022-03-01
date2:2022-12-19
year:2022
month:12
day:19
2.1 LocalTime类
public static void main(String[] args) {
//创建指定时间
LocalTime time1 = LocalTime.of(10, 35, 54);
System.out.println("time1:" + time1);
//得到当前时间
LocalTime time2 = LocalTime.now();
System.out.println("time2:" + time2);
//根据LocalTime对象获取对应的时间信息
System.out.println("hour:" + time2.getHour());
System.out.println("minute:" + time2.getMinute());
System.out.println("second:" + time2.getSecond());
}
time1:10:35:54
time2:21:02:45.501
hour:21
minute:2
second:45
2.1 LocalDateTime类
public static void main(String[] args) {
//获取指定的日期时间
LocalDateTime dateTime1 = LocalDateTime.of(2022, 4, 5, 13, 43, 40);
System.out.println("dateTime1:" + dateTime1);
//获取当前的日期时间
LocalDateTime dateTime2 = LocalDateTime.now();
System.out.println("dateTime2:" + dateTime2);
//根据LocalDateTime获取数据
System.out.println("year:" + dateTime2.getYear());
System.out.println("month:" + dateTime2.getMonthValue());
System.out.println("day:" + dateTime2.getDayOfMonth());
System.out.println("hour:" + dateTime2.getHour());
System.out.println("minute:" + dateTime2.getMinute());
System.out.println("second:" + dateTime2.getSecond());
}
dateTime1:2022-04-05T13:43:40
dateTime2:2022-12-19T21:03:28.279
year:2022
month:12
day:19
hour:21
minute:3
second:28
3、修改日期时间
3.1、withXXX方法,修改为指定的日期时间
LocalDateTime的withYear方法用来修改年份,还可以使用withMonth修改月份,withHour修改小时等…此处不一一演示了。withXXX方法,会返回一个新的LocalDateTime对象,新老LocalDateTime对象之间数据不相同。
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("当前时间:" + now);
LocalDateTime dateTime = now.withYear(1995).withMonth(6).withDayOfMonth(8);
System.out.println("修改年份后的时间:" + dateTime);
}
输出如下:
当前时间:2022-12-20T21:25:15.224
修改年份后的时间:1995-06-08T21:25:15.224
3.2、plusXXX/minusXXX,加减日期时间
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("当前时间:" + now);
LocalDateTime dateTime = now.plusYears(10).minusMonths(1);
System.out.println("修改后的时间:" + dateTime);
}
输出如下:
当前时间:2022-12-20T21:27:18.773
修改后的时间:2032-11-20T21:27:18.773
4、日期时间比较
LocalDateTime提供了isAfter、isBefore和isEqual方法,用来比较两个日期时间的大小,返回布尔值。
public static void main(String[] args) throws InterruptedException {
//获取当前时间dateTime1
LocalDateTime dateTime1 = LocalDateTime.now();
//睡眠2秒,获取当前时间dateTime2
Thread.sleep(2000);
LocalDateTime dateTime2 = LocalDateTime.now();
System.out.println("dateTime1在dateTime2之后吗?答案:" + dateTime1.isAfter(dateTime2));//false
System.out.println("dateTime1在dateTime2之前吗?答案:" + dateTime1.isBefore(dateTime2));//true
System.out.println("dateTime1与dateTime2相等吗?答案:" + dateTime1.isEqual(dateTime2));//false
}
dateTime1在dateTime2之后吗?答案:false
dateTime1在dateTime2之前吗?答案:true
dateTime1与dateTime2相等吗?答案:false
5、日期格式化
5.1、把日期时间对象转成字符串
public static void main(String[] args) throws InterruptedException {
//获取当前时间
LocalDateTime now = LocalDateTime.now();
//使用默认格式
DateTimeFormatter dtf1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
String format = now.format(dtf1);
System.out.println("默认格式:"+format);
//自定义格式
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format1 = now.format(dtf2);
System.out.println("自定义格式:"+format1);
}
输出如下:
默认格式:2022-12-21T21:43:00.052
自定义格式:2022-12-21 21:43:00
5.2、把字符串转成日期时间对象
public static void main(String[] args) throws InterruptedException {
String date= "1995-06-08 13:43:26";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parse = LocalDateTime.parse(date, dtf);
System.out.println(parse);
}
输出如下:
1995-06-08T13:43:26
6、Instant类(时间戳)
Instant类内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
6.1、Instance获取毫米值
public static void main(String[] args) {
Instant now = Instant.now();
System.out.println("now = " + now);
long epochSecond = now.getEpochSecond();
long milli = now.toEpochMilli(); //和 System.currentTimeMillis() 获取的毫秒值相同
System.out.println("当前时间秒:" + epochSecond);
System.out.println("当前毫秒值:" + milli);
}
输出如下:
now = 2022-12-21T14:11:38.076Z
当前时间秒:1671631898
当前毫秒值:1671631898076
6.2、获取某个时间点的毫秒值
public static void main(String[] args) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//将 string 装换成带有 T 的国际时间,但是没有进行,时区的转换,即没有将时间转为国际时间,只是格式转为国际时间
LocalDateTime parse = LocalDateTime.parse("1995-06-08 10:03:46", dateTimeFormatter);
//+8 小时,offset 可以理解为时间偏移量
ZoneOffset offset = OffsetDateTime.now().getOffset();
//转换为 通过时间偏移量将 string -8小时 变为 国际时间,因为亚洲上海是8时区
Instant instant = parse.toInstant(offset);
long l = instant.toEpochMilli();
System.out.println("毫秒值:"+l);
}
毫秒值:802577026000
7、计算日期时间差
7.1、Duration计算时间差
public static void main(String[] args) {
LocalTime now = LocalTime.now();
System.out.println("时间1:" + now);
//22点48分59秒
LocalTime of = LocalTime.of(23, 0, 0);
System.out.println("时间2:" + of);
Duration between = Duration.between(now, of);
System.out.println("两时间相隔的小时数:" + between.toHours());
System.out.println("两时间相隔的分钟数:" + between.toMinutes());
}
输出如下:
时间1:21:26:37.159
时间2:23:00
两时间相隔的小时数:1
两时间相隔的分钟数:93
7.2、Period计算日期差
public static void main(String[] args) {
LocalDate now = LocalDate.now();
LocalDate of = LocalDate.of(2022, 10, 15);
System.out.println("日期1:" + now);
System.out.println("日期2:" + of);
Period between = Period.between(of, now);
int months = between.getMonths();
int days = between.getDays();
System.out.println("两日期相隔 " + months +" 个月零 "+days+" 天");
}
输出如下:
日期1:2022-12-22
日期2:2022-10-15
两日期相隔 2 个月零 7 天
如果要计算两日期相隔的总天数:
public static void main(String[] args) {
LocalDate start = LocalDate.of(2022, 10, 1);//开始时间
LocalDate end = LocalDate.now();//当前时间
Long cha = end.toEpochDay() - start.toEpochDay();//天数差
System.out.println("日期1:" + start);
System.out.println("日期2:" + end);
System.out.println("量日期相隔的总天数:" + cha);
}
输出如下:
日期1:2022-10-01
日期2:2022-12-22
量日期相隔的总天数:82
8、新API与数据库的对应关系
在数据库中,时间类型有date、datetime、time、timestamp。很多人不知道在Java中如何去接收,我的建议是:
date类型使用LocalDate接收;
time类型使用LocalTime接收;
datetime类型使用LocalDateTime接收;
timestamp类型使用LocalDateTime接收。
总结
JDK8新特性到这里就结束了,还有某些API没有介绍到,需要大家在工作中慢慢积累。加油!