目录
一:JDK8新特性
1. Java SE的发展历史
2. 了解Open JDK 和 Oracle JDK
3. JDK 8新特性
3.1 Lambda表达式(重点)
3.2 接口的增强
3.3 函数式接口
3.4 方法引用
3.5 集合之Stream流式操作(重点)
3.6 新的时间和日期 API
Sun公司在1991年成立了一个称为绿色计划( Green Project )的项目,由James Gosling(高斯林)博土领导,绿色计划 的目的是开发一种能够在各种消费性电子产品(机顶盒、冰箱、收音机等)上运行的程序架构。这个项目的产品就是Java语言的前身: Oak(橡树)。Oak当时在消费品市场上并不算成功,但随着1995年互联网潮流的兴起,Oak迅速找到 了最适合自己发展的市场定位。
现在我们就介绍一下JDK的更新版本和命名:
JDK Beta - 1995 JDK 1.0 - 1996年1月 (真正第一个稳定的版本JDK 1.0.2,被称作 Java 1 ) JDK 1.1 - 1997年2月
J2SE 1.2 - 1998年12月
J2ME(Java 2 Micro Edition,Java 2平台的微型版),应用于移动、无线及有限资源的环境
J2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境。
J2EE(Java 2 Enterprise Edition,Java 2平台的企业版),应用于基于Java的应用服务器。
J2SE 1.3 - 2000年5月
J2SE 1.4 - 2002年2月
J2SE 5.0 - 2004年9月
Java SE 6 - 2006年12月
Java SE 7 - 2011年7月
Java SE 8(LTS) - 2014年3月
Java SE 9 - 2017年9月
Java SE 10(18.3) - 2018年3月
Java SE 11(18.9 LTS) - 2018年9月
Java SE 12(19.3) - 2019年3月
Java SE 13(19.9) - 2019年9月
........................................................
(1)Open JDK来源
Java 由 Sun 公司发明,Open JDK是Sun在2006年末把Java开源而形成的项目。也就是说Open JDK是Java SE平台版 的开源和免费实现,它由 SUN 和 Java 社区提供支持,2009年 Oracle 收购了 Sun 公司,自此 Java 的维护方之一的SUN 也变成了 Oracle。
(2)Open JDK 和 Oracle JDK的关系
大多数 JDK 都是在 Open JDK 的基础上进一步编写实现的,比如 IBM J9, Oracle JDK 和 Azul Zulu, Azul Zing。Oracle JDK完全由 Oracle 公司开发,Oracle JDK是基于Open JDK源代码的商业版本;此外,它包含闭源组件。Oracle JDK根据二进制代码许可协议获得许可,在没有商业许可的情况下,在2019年1月之后发布的Oracle Java SE 8的公开更新将无法用于商业或生产用途。但是 Open JDK是完全开源的,可以自由使用。
(3)Open JDK 官网介绍
Open JDK 官网:https://openjdk.org/
JDK Enhancement Proposals(JDK增强建议);通俗的讲JEP就是JDK的新特性!
(1)匿名内部类存在的问题
我们先写一个线程的实现,通过匿名内部类的方式;由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。
public class Test {
public static void main(String[] args) {
// 匿名内部类
new Thread(new Runnable() {
public void run() {
System.out.println("Hello");
}
}).start();
}
}
代码分析:对于 Runnable 的匿名内部类用法,可以分析出几点内容:
①Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类。
②为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类 。
③必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;而实际上似乎只有方法体才是关键所在。
(2)Lambda表达式初体验
Lambda是一个匿名函数,可以理解为一段可以传递的代码;借助Java8的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到相同的效果!
public class Test {
public static void main(String[] args) {
// Lambda表达式
new Thread(() ->
System.out.println("Hello")).start();
}
}
代码分析:这段代码和刚才的执行效果是完全一样的,可以在JDK8或更高的编译级别下通过。
①从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定;我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
②Lambda的优点:简化匿名内部类的使用,语法更加简单;实际上Lambda是匿名内部类的简写。
(3)Lambda的标准格式
Lambda表达式是一个匿名函数,而函数相当于Java中的方法;Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:
(参数类型 参数名称) -> {
代码体;
}
格式说明:
①(参数类型 参数名称):参数列表;
②{代码体;}:方法体;
③-> :箭头,分隔参数列表和方法体,起到连接的作用;
Lambda与方法的实现对比
例如:public static void main(String[] args),Lambda就是这种形式(String[] args);可以省略修饰符列表、返回值、方法名:
匿名内部类
public void run() {
System.out.println("aa");
}
Lambda表达式
() -> System.out.println("bb")
(4)无参数、无返回值的Lambda
定义一个接口,无参数方法返回void:
package com.zl;
public interface Swimmable {
// 抽象方法,省略了public static
void swimming();
}
测试类:
package com.zl;
public class SwimmableTest {
public static void main(String[] args) {
// 使用匿名内部类
play(new Swimmable() {
@Override
public void swimming() {
System.out.println("匿名内部类游泳!");
}
});
// 使用Lambda表达式:对匿名内部类的简写
play(()-> System.out.println("Lambda的游泳!"));
}
// 调用方法
public static void play(Swimmable s){
s.swimming();
}
}
(5)有参数、有返回值的Lambda
定义一个接口,有参数方法返回int类型:
package com.zl;
public interface Love {
int myLove(String name);
}
测试类
package com.zl;
public class LoveTest {
public static void main(String[] args) {
// 匿名内部类
lovers(new Love() {
@Override
public int myLove(String name) {
System.out.println("匿名内部类的方式");
return 1;
}
});
// Lambda表达式,多行代码需要一个大括号{}
lovers((String name) ->{
System.out.println("Lambda表达式的方式");
return 1;
});
}
// 调用方法
public static void lovers(Love love){
int count = love.myLove("小红");
System.out.println("返回值是"+count);
}
}
小总结:
①以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写。
② 匿名内部类和Lambda表达式结果的对比:
匿名内部类:在编译后会形成一个新的类,叫做:类名$.class,可以直接打开。
Lambda表达式:在编译后不会生成新的类(运行的时候才会形成类)就是原来的类,但是此时原来的类打不开,并且反编译工具无法反编译;我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令:javap -c -p 文件名.class ,发现会在这个类当中生成一个私有的静态方法;实际上Lambda表达式中的代码就会放到这个新增的静态方法当中。
总结:匿名内部类在编译的时候会一个class文件;Lambda在程序运行的时候形成一个类:在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码、还会形成一个匿名内部类,实现接口,重写抽象方法、在接口的重写方法中会调用新生成的方法。
(6)Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
①小括号内参数的类型可以省略;
②如果小括号内有且仅有一个参数,则小括号可以省略;
③如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号;
对于一个Lambda表达式:
(int a) -> {
return new Person();
}
第一步省略:省略类型
(a) -> {
return new Person();
}
第二步省略:只有一个参数时,小括号也可以省略
a -> {
return new Person();
}
第三步省略:发现括号内只有一条语句,省略大括号、return关键字、结束分号(必须同时省略)
a -> new Person()
例:在调用Collectons.sort()方法排序时,里面的参数只能是一个 List集合;对于自定义的类型进行排序还需要传一个比较器(比较器中编写比较的逻辑)
Person类:
package com.zl.mapper;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
PersonSort类:把上面的Person放到List集合当中,然后进行排序
package com.zl.mapper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class PersonSort {
public static void main(String[] args) {
// 创建一个ArrayList集合
List pList = new ArrayList<>();
// 准备数据
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 19);
Person p3 = new Person("王五", 22);
// 把数据添加到集合当中
pList.add(p1);
pList.add(p2);
pList.add(p3);
// 调用Collections工具类的sort方法进行排序
// 第一种方法:采用匿名内部类的方式
Collections.sort(pList, new Comparator() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});
// 第二种方式:采用Lambda表达式
Collections.sort(pList,(Person o1,Person o2)->{
return o1.getAge()-o2.getAge();
});
// 第三种方式:使用Lambda表达式的省略模式
Collections.sort(pList,((o1, o2) -> o1.getAge()-o2.getAge()));
// 打印
for (Person person : pList) {
System.out.println(person);
}
// 打印的第二种方式
pList.forEach(person -> System.out.println(person));
}
}
(7)Lambda的前提条件
Lambda的语法非常简洁,但是Lambda表达式使用时有几个条件要特别注意:
①方法的参数或局部变量类型必须为接口才能使用Lambda表达式;
②接口中有且仅有一个抽象方法;
怎么判定在接口中只有一个抽象方法呢?使用函数式接口
①函数式接口在Java中是指:有且仅有一个抽象方法的接口。
②函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
③使用FunctionalInterface注解,这个注解与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上
@FunctionalInterface
public interface Operator {
void myMethod();
}
④一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样!
(8)Lambda和匿名内部类对比
了解Lambda和匿名内部类在使用上的区别?
①所需的类型不一样
匿名内部类:需要的类型可以是:类、抽象类、接口。
Lambda表达式:需要的类型必须是接口。
②抽象方法的数量不一样
匿名内部类:所需的接口中抽象方法的数量随意。
Lambda表达式:所需的接口中只能有一个抽象方法。
③实现原理不同:
匿名内部类:是在编译后会形成class。
Lambda表达式:是在程序运行的时候动态生成class。
总结:一方面Lambda表达式作为接口的实现类的对象,另一方面Lambda表达式是一个匿名函数;当接口中只有一个抽象方法时,建议使用Lambda表达式;其他其他情况还是需要使用匿名内部类!
(1)JDK8中接口的新增
在JDK8中针对接口有做增强,在JDK8之前,接口中只能有常量和抽象方法
interface 接口名{
静态常量;
抽象方法;
}
JDK8之后对接口做了增加,接口中可以有默认方法和静态方法
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
(2)默认方法
我们先创建一个接口,然后再创建一个类实现这个接口,并且必须重写接口中的方法!
Anmail接口
package com.zl.anmails;
public interface Anmails {
// 抽象方法
void fly();
}
Bird类实现接口,并重写里面的方法
package com.zl.anmails;
public class Bird implements Anmails{
@Override
public void fly() {
System.out.println("小鸟起飞!");
}
}
如果此时我们在接口中又增加了其它方法呢?那么实现类都必须要重写这个抽象方法,这样就不利于接口的扩展;所以我们就可以定义成默认方法,语法格式如下:
interface 接口名{
修饰符 default 返回值类型 方法名{
方法体;
}
}
在接口中定义默认方法:
①这个默认方法,实现类会默认继承过去,继承过去的是和父类一模一样的方法!
②当然也可以进行重写,在方法体中编写自己的业务逻辑。
package com.zl.anmails;
public interface Anmails {
// 抽象方法
void fly();
// 默认方法
public default void eat(){
System.out.println("小鸟爱吃虫子");
}
}
(3)静态方法
JDK8中为接口新增了静态方法,作用也是为了接口的扩展,语法规则:
interface 接口名{
修饰符 static 返回值类型 方法名{
方法体;
}
}
接口中增加静态方法:
①接口中的静态方法在实现类中是不能被重写的,换言之默认也没有被继承过去。
②调用的话只能通过接口类型来调用: 接口名.静态方法名();使用多态的形式创建对象,使用对象. 的方式进行访问不行,因为实现类中根本没有这个静态方法。
package com.zl.anmails;
public interface Anmails {
// 抽象方法
void fly();
// 默认方法
public default void eat() {
System.out.println("小鸟爱吃虫子");
}
// 静态方法
public static void tryCatch(){
System.out.println("小鸟尝试抓虫子");
}
}
默认方法和静态方法两者的区别:
①默认方法通过实例调用,静态方法通过接口名调用。
②默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法;静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。
①如果接口只声明有一个抽象方法,则此接口就称为函数式接口。简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例;这就是Lambda表达式和函数式接口的关系。
②我们知道使用Lambda表达式的前提是需要有函数式接口,而Lambda表达式使用时不关心接口名, 抽象方法名;只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda表达式更加的方便,在JDK中提供了大量常用的函数式接口。
自定义函数式接口
package com.zl.anmails;
public class Computer {
public static void main(String[] args) {
// 调用fun方法
fun((arr) -> {
int sum = 0;
for (int i : arr) {
sum += i;
}
return sum;
});
}
// 调用求和的方法
public static void fun(Operator operator){
int[] arr = {1,2,3,4};
int sum = operator.getSum(arr);
System.out.println("sum = "+sum);
}
}
/**
* 函数式接口
*/
// 用来求和的接口
@FunctionalInterface
interface Operator{
int getSum(int[] arr);
}
在JDK中帮我们提供的有函数式接口,主要是在 java.util.function 包中!
四大核心函数式接口
注:以下的接口是已经提供好的,和我们上面自定义的函数接口Operate作用是类似的!
函数式接口 | 称谓 | 参数类型 | 用途 |
---|---|---|---|
Consumer |
消费型接口 | T | 对类型为T的对象应用操作,包含方法: void accept(T t) |
Supplier |
供给型接口 | 无 | 返回类型为T的对象,包含方法:T get() |
Function |
函数型接口 | T | 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t) |
Predicate |
判断型接口 | T | 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t) |
(1)Supplier
Supplier是一个无参、有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型。
@FunctionalInterface
public interface Supplier {
T get();
}
例:求一组数据中的最大值,不自己定义接口和抽象方法了,使用Supplier函数式接口作为参数
package com.zl.fun;
import java.util.Arrays;
import java.util.function.Supplier;
public class SupplierFun {
public static void main(String[] args) {
// 使用Lambda表达式,相当于重写get()方法的逻辑
fun(()->{
int arr[] = {1,2,7,8,4,5,6};
// 排序
Arrays.sort(arr);
// 找到最后一个元素就是最大值
return arr[arr.length-1];
});
}
// 调用接口中的方法
public static void fun(Supplier supplier){
Integer max = supplier.get();
System.out.println("max = "+max);
}
}
(2)Consumer
Consumer是一个有参、无返回值得接口,前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型
@FunctionalInterface
public interface Consumer {
void accept(T t);
}
例:将输入的数据统一转换为小写输出
package com.zl.fun;
import java.util.function.Consumer;
public class ConsumerFun {
public static void main(String[] args) {
fun((msg)->{
// 小写转大写
String s = msg.toLowerCase();
System.out.println(msg+"对应的小写"+s); // Hello AAA对应的小写hello aaa
});
}
// 调用接口中的方法
public static void fun(Consumer consumer){
consumer.accept("Hello AAA");
}
}
默认方法:andThen
如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现效果,消费一个数据的时候, 首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default方法 andThen方法
default Consumer andThen(Consumer super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
具体的操作:
package com.zl.fun;
import java.util.Locale;
import java.util.function.Consumer;
public class ConsumerAndFun {
public static void main(String[] args) {
fun(msg1->{
System.out.println(msg1 + "-> 转换为小写:" + msg1.toLowerCase());
},msg2->{
System.out.println(msg2 + "-> 转换为大写:" + msg2.toUpperCase(Locale.ROOT));
});
}
// 调用接口中的方法
public static void fun(Consumer c1,Consumer c2){
String str = "HELLO world";
c1.accept(str);
c2.accept(str);
// 上面就等价于
c1.andThen(c2).accept(str);
}
}
(3)Function
Function是一个有参、有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
@FunctionalInterface
public interface Function {
R apply(T t);
}
例:传递进入一个字符串返回一个数字
package com.zl.fun;
import javax.print.DocFlavor;
import java.util.function.Function;
public class FuncationFun {
public static void main(String[] args) {
fun((num)->{
Integer number = Integer.parseInt(num);
return number;
});
}
// 调用接口中的方法
public static void fun(Function function){
Integer apply = function.apply("666");
System.out.println("applay = "+apply);
}
}
默认方法:andThen,也是用来进行组合操作
default Function andThen(Function super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
具体的操作:
package com.zl.fun;
import java.util.function.Function;
public class FuncationAndthenFun {
public static void main(String[] args) {
fun(num1->{
return Integer.parseInt(num1);
},num2->{
return num2*10+6;
});
}
// 调用接口中的方法
public static void fun(Function f1,Function f2){
String str = "666";
Integer i1 = f1.apply(str);
Integer i2 = f2.apply(i1);
System.out.println("i2 = "+i2);
}
}
注:默认的compose方法的作用顺序和andThen方法刚好相反 !而静态方法identity则是,输入什么参数就返回什么参数 !
(4)Predicate
Predicate是一个有参、且返回值为Boolean的接口。
@FunctionalInterface
public interface Predicate {
boolean test(T t);
}
例:求一个字符串的长度是否大于5
package com.zl.fun;
import java.util.function.Predicate;
public class PredicateFun {
public static void main(String[] args) {
fun(msg->{
return msg.length() > 5;
},"Hello World");
}
// 调用接口中的方法
public static void fun(Predicate p,String str){
boolean b = p.test(str);
System.out.println("b = "+b);
}
}
在Predicate中的默认方法提供了逻辑关系操作 and、or、negate、isEquals方法
package com.zl.fun;
import java.util.function.Predicate;
public class PredicateTest {
public static void main(String[] args) {
fun(msg1->{
return msg1.contains("H");
},msg2->{
return msg2.contains("W");
});
}
// 调用接口中的方法
public static void fun(Predicate p1, Predicate p2){
// 1. 判断p1包含H,同时P2包含W
boolean b1 = p1.and(p2).test("Hello World");
System.out.println(b1);
// 2. 判断p1包含H,或者P2包含W
boolean b2 = p1.or(p2).test("Hello World");
System.out.println(b2);
// 3.结果取反,表示p1不包含H
boolean b3 = p1.negate().test("HelloWorld");
System.out.println(b3);
// 4. 判断两个结果是否相等
boolean bb1 = p1.test("Hello");
boolean bb2 = p2.test("World");
}
}
①Lambda表达式是可以简化函数式接口的变量或形参赋值的语法;而方法引用和构造器引用是为了简化Lambda表达式的!
②当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
③方法引用的本质:方法引用作为了函数式接口的实例!
(1)为什么使用方法引用?
在使用Lambda表达式的时候,也会出现代码冗余的情况,如:用Lambda表达式求一个数组的和
package com.zl.method;
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
printSum(arr->{
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("数组之和:" + sum);
});
}
/**
* 求数组的和,和上面功能代码相同
* @param arr
*/
public static void getSum(int arr[]){
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("数组之和:" + sum);
}
public static void printSum(Consumer consumer){
int[] arr = {30,10,26,18,45,9};
consumer.accept(arr);
}
}
以上两个方法的逻辑业务明显相同,代码冗余,这样就可以使用方法的引用
package com.zl.method;
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
// 引用下面相同功能的代码
printSum(Test::getSum);
}
/**
* 求数组的和,和上面功能代码相同
* @param arr
*/
public static void getSum(int arr[]){
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("数组之和:" + sum);
}
public static void printSum(Consumer consumer){
int[] arr = {30,10,26,18,45,9};
consumer.accept(arr);
}
}
(2)方法引用的格式
符号表示: ::
符号说明:双冒号为方法引用运算符,而它所在的表达式被称为方法引用
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用!
方法引用在JDK8中使用是相当灵活的,常见的引用方式有以下几种形式:
①instanceName::methodName 对象::方法名
②ClassName::staticMethodName 类名::静态方法
③ClassName::methodName 类名::普通方法
(3)对象名::方法名
最常见的一种用法;如果一个类中的已经存在了一个成员方法,则可以通过对象名引用成员方法
package com.zl.method;
import java.util.Date;
import java.util.function.Supplier;
public class MethodTest01 {
public static void main(String[] args) {
// 练习1:
// 第一种方式:匿名内部类
Consumer c1 = new Consumer() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
c1.accept("张三");
// 方法二:Lambda表达式
Consumer c2 = (msg)->{
System.out.println(msg);
};
c2.accept("李四");
// 方法三:方法引用
Consumer c3 = System.out::println;
c3.accept("王五");
// 练习2:
Date date = new Date();
Supplier supplier = ()->{ return date.getTime();};
System.out.println(supplier.get());
// 通过 方法引用 的方式处理
Supplier supplier = date::getTime;
System.out.println(supplier.get());
}
}
总结:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的形参列表、返回值类型都相同(或一致:比如多态,自动拆箱装箱等),则我们可以使用方法b实现对方法a的重写、替换。 此方法b是非静态的方法,需要对象来调用!
(4)类名::静态方法名
也是一种比较常用的方式,通过类名调用静态方法!
原来的方式:
package com.zl.method;
import java.util.Date;
import java.util.function.Supplier;
public class MethodTest01 {
public static void main(String[] args) {
fun(()->{
return System.currentTimeMillis();
});
// 采用方法引用的方式
Supplier s = System::currentTimeMillis;
System.out.println(s.get());
}
public static void fun(Supplier supplier){
Long time = supplier.get();
System.out.println(time);
// 上面就等价于
System.out.println(supplier.get());
}
}
精简的方式:
public class FunctionRefTest04 {
public static void main(String[] args) {
Supplier supplier1 = ()->{
return System.currentTimeMillis();
};
System.out.println(supplier1.get());
// 通过 方法引用 来实现
Supplier supplier2 = System::currentTimeMillis;
System.out.println(supplier2.get());
}
}
总结:函数式接口中的抽象方法a与其内部实现时调用的类的某个方法b的形参列表、返回值类型都相同(或一致:比如多态,自动拆箱装箱等),则我们可以使用方法b实现对方法a的重写、替换。 此方法b是静态的方法,需要类来调用!
(5)类名::引用实例方法(难)
①Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者!
②抽象方法有两个参数(n),内部实现的方法有一个参数(n-1),n个参数可以分为1和n-1,且第1个参数是方法的调用者!
例题1:
package com.zl.methods;
import java.util.Comparator;
public class Test03 {
public static void main(String[] args) {
// 第一种:匿名内部类
Comparator com1 = new Comparator() {
@Override
public int compare(String o1, String o2) { // 两个参数
return o1.compareTo(o2); // 一个参数
}
};
System.out.println(com1.compare("abc", "abd"));
// 第二种:Lambda
Comparator com2 = (s1,s2)->{
return s1.compareTo(s2);
};
System.out.println(com2.compare("abc", "abd"));
// 第三种:类::实例方法
Comparator com3 = String::compareTo;
System.out.println(com3.compare("abc","abb"));
}
}
例题2:
package com.zl.method;
import java.util.function.Function;
public class MethodTest02 {
public static void main(String[] args) {
Function function = (s)->{
return s.length();
};
System.out.println(function.apply("hello"));
// 使用方法引用
Function function2 = String::length;
System.out.println(function2.apply("hello"));
}
}
总结:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的返回值类型都相同。同时,抽象方法a中有n个参数,方法b中有n-1个参数,且抽象方法a的第一个参数作为方法b的调用者,且抽象方法a的后n-1个参数与方法b的n-1个参数的类型相同(或一致:比如多态,自动拆箱装箱等)。则我们可以使用方法b实现对方法a的重写、替换。 此方法b是非静态的方法,需要对象来调用,但是形式上是写成对象a所属的类!
(6)类名::构造器
由于构造器的名称和类名完全一致,所以构造器引用使用 ::new 的格式使用!
ClassName::new 类名::new
Person类:提供有参和无参构造方法
package com.zl.method;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
注:如果使用一个参数的构造方法,可以定义为Function;定义为两个参数的构造方法,使用BiFunction;最终返回的都是一个Person类型!
package com.zl.method;
import java.util.function.BiFunction;
import java.util.function.Supplier;
public class MythodTest03 {
public static void main(String[] args) {
Supplier supplier = ()->{
return new Person();
};
System.out.println(supplier.get());
// 通过 方法引用来实现
Supplier supplier1 = Person::new;
System.out.println(supplier1.get());
// 也可以给属性赋值
BiFunction function = Person::new;
System.out.println(function.apply("张三",22)); // 底层调用有参数构造方法
}
}
总结:调用了类名对应的类中的某一个确定的构造器,具体调用的是类中的哪一个构造器?取决于函数式接口的抽象方法的形参列表!
(7)数组::构造器
当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。
格式:数组类型名::new
TypeName[]::new 数组名[]::new
例1:传一个数字,返回一个对应长度的数组
public static void main(String[] args) {
Function fun1 = (len)->{
return new String[len];
};
String[] a1 = fun1.apply(3);
System.out.println("数组的长度是:" + a1.length);
// 方法引用 的方式来调用数组的构造器
Function fun2 = String[]::new;
String[] a2 = fun2.apply(5);
System.out.println("数组的长度是:" + a2.length);
}
总结:方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加 的精简,也可以理解为lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。
①Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。
②Stream API ( java.util.stream) 把真正的函数式编程风格引入到 Java 中。这是目前为止 对 Java 类库最好的补充,因为 Stream API 可以极大提供 Java 程序员的生产力,让程 序员写出高效率、干净、简洁的代码。
③Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来 并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
(1)什么是 Stream?
①实际开发中,项目中多数数据源都来自于MySQL、Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。
②Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
③Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构, 讲的是数据,而 Stream 是有关计算的,讲的是数据的计算(排序、查找、过滤、映射、遍历等)。前者是主要面向内存, 存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。
注意:
①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行;即 一旦执行终止操作,就执行中间操作链,并产生结果。
④ Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。
(2)Stream的操作三个步骤?
1- 创建 Stream
Stream的实例化:一个数据源(如:集合、数组),获取一个流 !
2- 中间操作
每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是个
操作链
,可对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行。
3- 终止操作(终端操作)
终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。
(3) 创建Stream实例
方式一:通过集合
Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
default Stream stream() : 返回一个顺序流
default Stream parallelStream() : 返回一个并行流
例:
package com.zl.methods;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Test04 {
public static void main(String[] args) {
// 创建一个数组
int[] arr = {1,2,3,4,5,6};
// 把一个数组转换成一个集合
List list = Arrays.asList(arr);
// 得到Stream实例(返回一个顺序流)
Stream stream = list.stream();
// 得到Stream实例(返回一个并行流)
Stream stream1 = list.parallelStream();
}
}
方式二:通过数组
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
static Stream stream(T[] array): 返回一个流
public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)
例:
package com.zl.methods;
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Test04 {
public static void main(String[] args) {
// 创建一个数组
int[] arr = {1,2,3,4,5,6};
// 得到Stream实例(返回一个顺序流)
IntStream stream = Arrays.stream(arr);
// 再例如
Integer[] integers = {1,2,3,4,5};
Stream stream1 = Arrays.stream(integers);
}
}
方式三:通过Stream的of()
可以调用Stream类静态方法 of(), 通过显示值创建一个流,它可以接收任意数量的参数。
public static Stream of(T... values) : 返回一个流
主要用在多个数据没有容器(数组、集合)进行存储
package com.zl.methods;
import java.util.stream.Stream;
public class Test04 {
public static void main(String[] args) {
// 通过静态方法进行调用
Stream stream = Stream.of(1, 2, 3, 4);
}
}
(4)一系列中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
准备Employee类
package com.zl.data;
import java.util.Objects;
public class Employee {
private int id;
private String name;
private int age;
private double salary;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Employee() {
System.out.println("Employee().....");
}
public Employee(int id) {
this.id = id;
}
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", salary=" + salary + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id && age == employee.age && Double.compare(employee.salary, salary) == 0 && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age, salary);
}
}
准备数据
package com.zl.data;
import java.util.ArrayList;
import java.util.List;
public class EmployeeData {
public static List getEmployees(){
List list = new ArrayList<>();
list.add(new Employee(1001, "马化腾", 34, 6000.38));
list.add(new Employee(1002, "马云", 2, 19876.12));
list.add(new Employee(1003, "刘强东", 33, 3000.82));
list.add(new Employee(1004, "雷军", 26, 7657.37));
list.add(new Employee(1005, "李彦宏", 65, 5555.32));
list.add(new Employee(1006, "比尔盖茨", 42, 9500.43));
list.add(new Employee(1007, "任正非", 26, 4333.32));
list.add(new Employee(1008, "扎克伯格", 35, 2500.32));
return list;
}
}
1-筛选与切片
方 法 | 描 述 |
---|---|
filter(Predicate) | 接收 Lambda , 从流中排除某些元素 |
distinct() | 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素 |
limit(long maxSize) | 截断流,使其元素不超过给定数量 |
skip(long n) | 跳过元素,返回一个扔掉了前 n 个元素的流。 若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 |
需求:查询员工表中薪资大于7000的员工
package com.zl.data;
import java.util.List;
import java.util.stream.Stream;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 实例Stream
Stream stream = list.stream();
// 进行筛选(参数是Predicate函数式接口)
// stream.filter(emp -> emp.getSalary() > 7000);
// 进行打印,实际上也就是终止条件
// stream.filter(emp -> emp.getSalary() > 7000).forEach(emp-> System.out.println(emp));
// 后面也可以使用方法引用
stream.filter(emp -> emp.getSalary() > 7000).forEach(System.out::println);
}
}
需求:截断流,使元素不超过给定数量
package com.zl.data;
import java.util.List;
import java.util.stream.Stream;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 实例Stream
Stream stream = list.stream();
// 截断流
stream.limit(2).forEach(System.out::println);
}
}
需求:跳过前2个,只打印后面的;实际上和limit(只取前几个)是一个互补的关系
package com.zl.data;
import java.util.List;
import java.util.stream.Stream;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 实例Stream
Stream stream = list.stream();
// 跳过前2个
stream.skip(2).forEach(System.out::println);
}
}
需求:去重,根据equals方法和hashCode方法进行去重
package com.zl.data;
import java.util.List;
import java.util.stream.Stream;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 添加几个相同的元素
list.add(new Employee(1008, "特朗布", 35, 2500.32));
list.add(new Employee(1008, "特朗布", 35, 2500.32));
list.add(new Employee(1008, "特朗布", 35, 2500.32));
// 实例Stream
Stream stream = list.stream();
// 进行去重
stream.distinct().forEach(System.out::println);
}
}
2-映 射
方法 | 描述 |
---|---|
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |
需求:大写转小写
package com.zl.data;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Test01 {
public static void main(String[] args) {
// 把一个数组转化成集合
List list = Arrays.asList("aa", "bb", "cc");
// 实例Stream
Stream stream = list.stream();
// 大写转小写
// stream.map(str->str.toUpperCase()).forEach(System.out::println);
// 使用方法引用也可以
stream.map(String::toUpperCase).forEach(System.out::println);
}
}
需求:获取员工姓名长度大于3的员工姓名
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 获取员工姓名长度大于3的员工姓名
// 方法一:先过滤到名字长度低于3的,在映射名字进行打印
list.stream().filter(emp->emp.getName().length()>3).map(emp->emp.getName()).forEach(System.out::println);
// 方式二:先映射名字
list.stream().map(emp->emp.getName()).filter(name->name.length()>3).forEach(System.out::println);
// 方式三:使用方法引用
list.stream().map(Employee::getName).filter(name->name.length()>3).forEach(System.out::println);
}
}
3-排序
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序 |
sorted(Comparator com) | 产生一个新流,其中按比较器顺序排序 |
需求:进行自然排序
package com.zl.data;
import java.util.Arrays;
public class Test01 {
public static void main(String[] args) {
// 定义一个数组
int[] arr = new int[]{1,2,9,76,3,4,5};
Arrays.stream(arr).sorted().forEach(System.out::println);
}
}
需求:借用比较器进行排序
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 借用比较器进行排序,按照年级进行比较
list.stream().sorted((o1,o2)-> o1.getAge() - o2.getAge()).forEach(System.out::println);
}
}
(5)终止操作
①终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
②流进行了终止操作后,不能再次使用。
1-匹配与查找
方法 | 描述 |
---|---|
allMatch(Predicate p) | 检查是否匹配所有元素 |
anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
findFirst() | 返回第一个元素 |
findAny() | 返回当前流中的任意元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。 相反,Stream API 使用内部迭代——它帮你把迭代做了) |
需求:是否所有的员工的年龄都大于18
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
boolean b = list.stream().allMatch(emp -> emp.getAge() > 18);
System.out.println(b); // false
}
}
需求:是否存在员工的工资大于10000(至少有一个)
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
boolean b = list.stream().anyMatch(emp -> emp.getSalary() > 10000);
System.out.println(b); // true
}
}
需求:返回第一个元素
package com.zl.data;
import java.util.List;
import java.util.Optional;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 返回的是一个Option
Optional optionalEmployee = list.stream().findFirst();
System.out.println(optionalEmployee); // Optional[Employee{id=1001, name='马化腾', age=34, salary=6000.38}]
// 在调用get方法就能拿到第一个值
System.out.println(optionalEmployee.get()); // Employee{id=1001, name='马化腾', age=34, salary=6000.38}
}
}
需求:返回流中薪资大于7000元素的总个数
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
long count = list.stream().filter(emp -> emp.getSalary() > 7000).count();
System.out.println(count); // 3
}
}
需求:返回最高的薪资
package com.zl.data;
import java.util.List;
import java.util.Optional;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 先获取返回最高工资的员工
// 得到的是一个Option
Optional optionalEmployee = list.stream().max((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary()));
System.out.println(optionalEmployee); // Optional[Employee{id=1002, name='马云', age=2, salary=19876.12}]
// 方法1(根据上面在调用get方法,获取员工,在调用getSalary获取到薪资)
System.out.println(list.stream().max((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary())).get().getSalary());
// 方法2(映射)
System.out.println(list.stream().map(emp -> emp.getSalary()).max((salgrade1, salgrade2) -> Double.compare(salgrade1, salgrade2)).get());
// 使用方法引用
System.out.println(list.stream().map(emp -> emp.getSalary()).max(Double::compare).get());
}
}
注:在JDK8中增加了一个遍历集合的方法
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 进行遍历---使用lambda表达式
list.forEach((emp)-> System.out.println(emp));
// 使用方法的引用
list.forEach(System.out::println);
}
}
2-归约
方法 | 描述 |
---|---|
reduce(T identity, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional |
需求:计算1-10的自然数的和
第一个参数是一个随机数的种子:相当于指定一个初始值,后续的累加的最终结果,都需要加上这个数值。
第二个参数BinaryOperator:实际上是继承了BiFunction类,两个参数返回一个值。
package com.zl.data;
import java.util.Arrays;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 进行求和
// 第一种方法
System.out.println(list.stream().reduce(0, (x1, x2) -> x1 + x2)); // 55
// 第二种方法(调用Integer的sum方法进行累加)
System.out.println(list.stream().reduce(0, (x1, x2) -> Integer.sum(x1,x2))); // 55
// 第三种方法:在二的基础上就可以使用方法的引用
System.out.println(list.stream().reduce(0, Integer::sum)); // 55
}
}
需求:计算公司所员工的总和(先映射在规约)
package com.zl.data;
import java.util.List;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 先映射在规约(得到的是Option)
// 在调用get方法得到值
System.out.println(list.stream().map(emp -> emp.getSalary()).reduce((s1,s2)->Double.sum(s1,s2))); // Optional[58424.08]
System.out.println(list.stream().map(emp -> emp.getSalary()).reduce(Double::sum).get()); // 58424.08
}
}
备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。
3-收集
调用的sort方法进行排序,但是并不会影响到原来的数据,所以我们可以把排序过后的数据存储到一个集合当中;Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。
方 法 | 描 述 |
---|---|
collect(Collector c) | 将流转换为其他形式。接收一个 Collector接口的实现, 用于给Stream中元素做汇总的方法 |
另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:
方法 | 返回类型 | 作用 |
---|---|---|
toList | Collector |
把流中元素收集到List |
List emps= list.stream().collect(Collectors.toList());
方法 | 返回类型 | 作用 |
---|---|---|
toSet | Collector |
把流中元素收集到Set |
Set emps= list.stream().collect(Collectors.toSet());
方法 | 返回类型 | 作用 |
---|---|---|
toCollection | Collector |
把流中元素收集到创建的集合 |
Collection emps =list.stream().collect(Collectors.toCollection(ArrayList::new));
需求:查找工资大于6000的员工,结果返回到一个List
package com.zl.data;
import java.util.List;
import java.util.stream.Collectors;
public class Test01 {
public static void main(String[] args) {
List list = EmployeeData.getEmployees();
// 进行过滤(并把数据存储到List集合)
List list1 = list.stream().filter(emp -> emp.getSalary() > 6000).collect(Collectors.toList());
// 进行打印
list1.forEach(System.out::println);
}
}
JDK8之前:日期时间API
(1)java.lang.System类的currentTimeMills方法
①System类提供的public static long currentTimeMillis():用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。
②此方法适于计算时间差。
package com.zl.data;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test01 {
public static void main(String[] args) {
// 获取当前时间的毫秒数
long timeMillis = System.currentTimeMillis();
System.out.println(timeMillis);
// 获取昨天此时的时间
Date date = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
}
}
(2)java.util.Date
①构造器
Date():使用无参构造器创建的对象可以获取本地当前时间
Date(long 毫秒数):把该毫秒值换算成日期时间对象
②常用方法
getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数
toString(): 把此 Date 对象转换为以下形式的 String
例:
package com.zl.data;
import java.util.Date;
public class Test01 {
public static void main(String[] args) {
// 无参构造器
Date date = new Date();
System.out.println(date.toString()); // Mon Apr 10 19:46:50 CST 2023
// 有参构造
Date date1 = new Date(100L);
System.out.println(date1.toString()); // Thu Jan 01 08:00:00 CST 1970
// 调用getTime方法获取当前的毫秒数(和System.currentTimeMillis相同的效果)
System.out.println(date.getTime());
System.out.println(System.currentTimeMillis());
}
}
③其子类java.sql.Date,对应着数据库中的Date类型,只有有参构造器
package com.zl.data;
import java.sql.Date;
import java.text.ParseException;
public class Test01 {
public static void main(String[] args) throws ParseException {
// java.sql.Date
// 获取时间戳
long timeMillis = System.currentTimeMillis(); // 1681181172832
System.out.println(timeMillis);
// 只有一个有参构造器
java.sql.Date date = new Date(timeMillis);
// 只打印年月日,但是其实也包含时分秒,只是没有显示,从下面调用getTime方法的结果与前面的时间戳相等也能体现
System.out.println(date); // 2023-04-11
System.out.println(date.getTime()); // 1681181172832
}
}
(3)java.text.SimpleDateFormat
java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类
①可以进行格式化:日期 --> 文本
②可以进行解析:文本 --> 日期
构造器:
SimpleDateFormat() :默认的模式和语言环境创建对象
public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象
格式化:
public String format(Date date):方法格式化时间对象date
解析:
public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期
例:
package com.zl.data;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test01 {
public static void main(String[] args) throws ParseException {
// 无参构造器
Date date = new Date();
System.out.println(date); // Mon Apr 10 20:02:33 CST 2023
// 指定格式化的格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 把日期格式化为字符串
System.out.println(sdf.format(date)); // 2023-04-10 20:02:33
// 把时间的字符串解析为日期
System.out.println(sdf.parse("2022-4-10 6:10:10")); // Sun Apr 10 06:10:10 CST 2022
}
}
(4)java.util.Calendar(日历)
①Date类的API大部分被废弃了,替换为Calendar(抽象类)。
②Calendar
类是一个抽象类,主用用于完成日期字段之间相互操作的功能。
获取Calendar实例
第一种方法:创建子类GregorianCalendar
第二种方法:调用Calendar的静态方法getInstance
package com.zl.data;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class Test01 {
public static void main(String[] args) {
// 第一种方法
GregorianCalendar gregorianCalendar = new GregorianCalendar();
System.out.println(gregorianCalendar);
// 第二种方法(常用)实际上也是子类GregorianCalendar
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getClass()); // class java.util.GregorianCalendar
}
}
常用的方法
public int get(int field):返回给定日历字段的值
public void set(int field,int value) :将给定的日历字段设置为指定的值
public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或者减去指定的时间量
public final Date getTime():将Calendar转成Date对象
public final void setTime(Date date):使用指定的Date对象重置Calendar的时间
常用的field字段
package com.zl.data;
import java.util.Calendar;
import java.util.Date;
public class Test01 {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
// get方法
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 3,今天是这个周的第几天
System.out.println(calendar.get(Calendar.DAY_OF_YEAR)); // 101,今天是这个年的第几天
// set方法
calendar.set(Calendar.DAY_OF_WEEK,5); // 把今天是这个周的第几天设置为另一个值
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 5,把今天设置为这个周的第5天
// add方法
calendar.add(Calendar.DAY_OF_WEEK,1);
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 6,在原来的基础上增加一天
calendar.add(Calendar.DAY_OF_WEEK,-2);
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 4,在原来的基础上减少两天
// getTime方法,从Calendar转换为Date
System.out.println(calendar.getTime()); // Wed Apr 12 11:20:29 CST 2023
// setTime方法,使用指定的Date重置Calendar
Date date = new Date();
calendar.setTime(date);
// 其实再次获取今天是这周的第几天
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 3,重置过后还是原来的第三天
}
}
例题:
例:将一个java.util.Date的实例转换为java.sql.Date的实例
package com.zl.data;
import java.util.Date;
public class Test01 {
public static void main(String[] args) {
// 很容易想到的思路,进行强转,会出现ClassCastException异常
// 原因我们创建的就是父类java.util.Date,不能往下转为java.sql.Date(可以王当前的父类转,但是不能往当前的子类进行转)
/*Date date = new Date();
java.sql.Date date2 = (java.sql.Date) date; // err*/
// 正确的做法---根据时间戳进行转换
Date date = new Date();
java.sql.Date date1 = new java.sql.Date(date.getTime());
}
}
JDK8:新的日期时间API
如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
①可变性:像日期和时间这样的类应该是不可变的。
②偏移性:Date中的年份是从1900开始的,而月份都从0开始。
③格式化:格式化只对Date有用,Calendar则不行。
④此外,它们也不是线程安全的;不能处理闰秒等。
Java 8 以一个新的开始为 Java 创建优秀的 API。新的日期时间API包含:
java.time – 包含值对象的基础包
java.time.chrono – 提供对不同的日历系统的访问。
java.time.format – 格式化和解析时间和日期
java.time.temporal – 包括底层框架和扩展特性
java.time.zone – 包含时区支持的类
(1)本地日期时间:LocalDate、LocalTime、LocalDateTime---类似于Calendar
获取实例对象
方法 | 描述 |
---|---|
now() / now(ZoneId zone) |
静态方法,根据当前时间创建对象/指定时区的对象 |
of(xx,xx,xx,xx,xx,xxx) |
静态方法,根据指定日期/时间创建对象 |
package com.zl.data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class Test01 {
public static void main(String[] args) {
// now():获取当前日期和时间对应的实例
LocalDate localDate = LocalDate.now(); // 年月日
LocalTime localTime = LocalTime.now(); // 时分秒
LocalDateTime localDateTime = LocalDateTime.now(); // 年月日和时分秒
System.out.println(localDate); // 2023-04-11
System.out.println(localTime); // 14:41:45.596
System.out.println(localDateTime); // 2023-04-11T14:41:45.596
// of():获取指定的日期,时间对应的实例
LocalDate localDate1 = LocalDate.of(2023, 4, 11);
LocalDateTime localDateTime1 = LocalDateTime.of(2023, 4, 11, 14, 45, 30);
System.out.println(localDate1); // 2023-04-11
System.out.println(localDateTime1); // 2023-04-11T14:45:30
}
}
常用方法:获取(get)、修改(with)、增加(plus)、减少(min)
方法 | 描述 |
---|---|
getDayOfMonth()/getDayOfYear() | 获得月份天数(1-31) /获得年份天数(1-366) |
getDayOfWeek() | 获得星期几(返回一个 DayOfWeek 枚举值) |
getMonth() | 获得月份, 返回一个 Month 枚举值 |
getMonthValue() / getYear() | 获得月份(1-12) /获得年份 |
getHours()/getMinute()/getSecond() | 获得当前对象对应的小时、分钟、秒 |
withDayOfMonth()/withDayOfYear()/withMonth()/withYear() | 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象 |
with(TemporalAdjuster t) | 将当前日期时间设置为校对器指定的日期时间 |
plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours() | 向当前对象添加几天、几周、几个月、几年、几小时 |
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours() | 从当前对象减去几月、几周、几天、几年、几小时 |
plus(TemporalAmount t)/minus(TemporalAmount t) | 添加或减少一个 Duration 或 Period |
isBefore()/isAfter() | 比较两个 LocalDate |
isLeapYear() | 判断是否是闰年(在LocalDate类中声明) |
format(DateTimeFormatter t) | 格式化本地日期、时间,返回一个字符串 |
parse(Charsequence text) | 将指定格式的字符串解析为日期、时间 |
package com.zl.data;
import java.time.LocalDateTime;
public class Test01 {
public static void main(String[] args) {
LocalDateTime localDateTime = LocalDateTime.now();
// getXxx()获取
int dayOfMonth = localDateTime.getDayOfMonth();
System.out.println(dayOfMonth); // 11,获取今天是这个月的第几天
// withXxx()修改
LocalDateTime localDateTime1 = localDateTime.withDayOfMonth(15); // 把今天修改为这个月的第15天
System.out.println(localDateTime); // 2023-04-11T14:57:05.485,原来的并没有更改
System.out.println(localDateTime1); // 2023-04-15T14:57:05.485,返回新的LocalDateTime对象才被更改,体现了不可变性
// plusXxx()增加
LocalDateTime localDateTime2 = localDateTime1.plusDays(5); // 又会生成一个新的LocalDateTime对象
System.out.println(localDateTime2); // 2023-04-20T15:00:25.650,在原来的基础上增加了5天
// minXxx()减少
LocalDateTime localDateTime3 = localDateTime2.minusDays(4);
System.out.println(localDateTime3); // 2023-04-16T15:02:58.050,在原来的基础上增加了4天
}
}
(2)瞬时:Instant---类似于Date
①Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
②java.time.Instant
表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。
方法 | 描述 |
---|---|
now() |
静态方法,返回默认UTC时区的Instant类的对象 |
ofEpochMilli(long epochMilli) |
静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象 |
atOffset(ZoneOffset offset) | 结合即时的偏移来创建一个 OffsetDateTime |
toEpochMilli() |
返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳 |
package com.zl.data;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class Test01 {
public static void main(String[] args) {
// 类似于Date的无参构造器
Instant instant = Instant.now();
System.out.println(instant); //2023-04-11T07:20:51.614Z与现在的时间差了8个小时
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8)); // 设置偏移的8个小时
System.out.println(offsetDateTime); // 2023-04-11T15:20:51.614+08:00
// 类似于Date的有参构造器
Instant instant1 = Instant.ofEpochMilli(System.currentTimeMillis());
System.out.println(instant1); // 2023-04-11T07:20:51.695Z
// 获取当前的毫秒数
long toEpochMilli = instant.toEpochMilli();
System.out.println(toEpochMilli); // 1681197782827
}
}
(3)日期时间格式化:DateTimeFormatter---类似于SimpleDateFormat
DateTimeFormatter用户格式化和解析:LocalDate、LocalTime、LocalDateTime!
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
方 法 | 描 述 |
---|---|
ofPattern(String pattern) | 静态方法,返回一个指定字符串格式的DateTimeFormatter |
format(TemporalAccessor t) | 格式化一个日期、时间,返回字符串 |
parse(CharSequence text) | 将指定格式的字符序列解析为一个日期、时间 |
package com.zl.data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
public class Test01 {
public static void main(String[] args) {
LocalDateTime localDateTime = LocalDateTime.now();
// 自定义格式
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化
String strDateTime = dtf.format(localDateTime);
System.out.println(strDateTime); // 2023-04-11 15:44:14
// 解析(得到的是一个TemporalAccessor接口,LocalDateTime实现了这个接口)
TemporalAccessor temporalAccessor = dtf.parse("2023-10-10 15:30:30");
// 在调用LocalDateTime.from方法转换为LocalDateTime
LocalDateTime localDateTime1 = LocalDateTime.from(temporalAccessor);
System.out.println(localDateTime1); // 2023-10-10T15:30:30
}
}