JDK8新特性 (Lambda表达式和Stream流式编程)
目录
一: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
一:JDK8新特性
1. Java SE的发展历史
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月
........................................................
2. 了解Open JDK 和 Oracle JDK
(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的新特性!
3. JDK 8新特性
3.1 Lambda表达式(重点)
(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 staticvoid swimming();
}
测试类:
package com.zl;public class SwimmableTest {public static void main(String[] args) {// 使用匿名内部类play(new Swimmable() {@Overridepublic 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() {@Overridepublic 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;}@Overridepublic 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<Person> 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<Person>() {@Overridepublic 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表达式;其他其他情况还是需要使用匿名内部类!
3.2 接口的增强
(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{@Overridepublic 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("小鸟尝试抓虫子");}
}
默认方法和静态方法两者的区别:
①默认方法通过实例调用,静态方法通过接口名调用。
②默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法;静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。
3.3 函数式接口
①如果接口只声明有一个抽象方法,则此接口就称为函数式接口。简单的说,在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 | 对类型为T的对象应用操作,包含方法: void accept(T t) |
Supplier<T> |
供给型接口 | 无 | 返回类型为T的对象,包含方法:T get() |
Function<T, R> |
函数型接口 | T | 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t) |
Predicate<T> |
判断型接口 | T | 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t) |
(1)Supplier
Supplier是一个无参、有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型。
@FunctionalInterface
public interface Supplier<T> {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<Integer> supplier){Integer max = supplier.get();System.out.println("max = "+max);}
}
(2)Consumer
Consumer是一个有参、无返回值得接口,前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型
@FunctionalInterface
public interface Consumer<T> {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<String> consumer){consumer.accept("Hello AAA");}
}
默认方法:andThen
如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现效果,消费一个数据的时候, 首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default方法 andThen方法
default Consumer<T> 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<String> c1,Consumer<String> c2){String str = "HELLO world";c1.accept(str);c2.accept(str);// 上面就等价于c1.andThen(c2).accept(str);}
}
(3)Function
Function是一个有参、有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
@FunctionalInterface
public interface Function<T, R> {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<String, Integer> function){Integer apply = function.apply("666");System.out.println("applay = "+apply);}
}
默认方法:andThen,也是用来进行组合操作
default <V> Function<T, V> 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<String, Integer> f1,Function<Integer,Integer> 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<T> {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<String> 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<String> p1, Predicate<String> p2){// 1. 判断p1包含H,同时P2包含Wboolean b1 = p1.and(p2).test("Hello World");System.out.println(b1);// 2. 判断p1包含H,或者P2包含Wboolean b2 = p1.or(p2).test("Hello World");System.out.println(b2);// 3.结果取反,表示p1不包含Hboolean b3 = p1.negate().test("HelloWorld");System.out.println(b3);// 4. 判断两个结果是否相等boolean bb1 = p1.test("Hello");boolean bb2 = p2.test("World");}}
3.4 方法引用
①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<int[]> 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<int[]> 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<String> c1 = new Consumer<String>() {@Overridepublic void accept(String s) {System.out.println(s);}};c1.accept("张三");// 方法二:Lambda表达式Consumer<String> c2 = (msg)->{System.out.println(msg);};c2.accept("李四");// 方法三:方法引用Consumer<String> c3 = System.out::println;c3.accept("王五");// 练习2:Date date = new Date();Supplier<Long> supplier = ()->{ return date.getTime();};System.out.println(supplier.get());// 通过 方法引用 的方式处理Supplier<Long> 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<Long> s = System::currentTimeMillis;System.out.println(s.get());}public static void fun(Supplier<Long> supplier){Long time = supplier.get();System.out.println(time);// 上面就等价于System.out.println(supplier.get());}
}
精简的方式:
public class FunctionRefTest04 {public static void main(String[] args) {Supplier<Long> supplier1 = ()->{return System.currentTimeMillis();};System.out.println(supplier1.get());// 通过 方法引用 来实现Supplier<Long> 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<String> com1 = new Comparator<String>() {@Overridepublic int compare(String o1, String o2) { // 两个参数return o1.compareTo(o2); // 一个参数}};System.out.println(com1.compare("abc", "abd"));// 第二种:LambdaComparator<String> com2 = (s1,s2)->{return s1.compareTo(s2);};System.out.println(com2.compare("abc", "abd"));// 第三种:类::实例方法Comparator<String> 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<String,Integer> function = (s)->{return s.length();};System.out.println(function.apply("hello"));// 使用方法引用Function<String,Integer> 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;}@Overridepublic 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<Person> supplier = ()->{return new Person();};System.out.println(supplier.get());// 通过 方法引用来实现Supplier<Person> supplier1 = Person::new;System.out.println(supplier1.get());// 也可以给属性赋值BiFunction<String ,Integer,Person> function = Person::new;System.out.println(function.apply("张三",22)); // 底层调用有参数构造方法}
}
总结:调用了类名对应的类中的某一个确定的构造器,具体调用的是类中的哪一个构造器?取决于函数式接口的抽象方法的形参列表!
(7)数组::构造器
当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。
格式:数组类型名::new
TypeName[]::new 数组名[]::new
例1:传一个数字,返回一个对应长度的数组
public static void main(String[] args) {Function<Integer,String[]> fun1 = (len)->{return new String[len];};String[] a1 = fun1.apply(3);System.out.println("数组的长度是:" + a1.length);// 方法引用 的方式来调用数组的构造器Function<Integer,String[]> fun2 = String[]::new;String[] a2 = fun2.apply(5);System.out.println("数组的长度是:" + a2.length);}
总结:方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加 的精简,也可以理解为lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。
3.5 集合之Stream流式操作(重点)
①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<E> stream() : 返回一个顺序流
default Stream<E> 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<int[]> list = Arrays.asList(arr);// 得到Stream实例(返回一个顺序流)Stream<int[]> stream = list.stream();// 得到Stream实例(返回一个并行流)Stream<int[]> stream1 = list.parallelStream();}
}
方式二:通过数组
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
static <T> Stream<T> 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<Integer> stream1 = Arrays.stream(integers);}
}
方式三:通过Stream的of()
可以调用Stream类静态方法 of(), 通过显示值创建一个流,它可以接收任意数量的参数。
public static<T> Stream<T> of(T... values) : 返回一个流
主要用在多个数据没有容器(数组、集合)进行存储
package com.zl.methods;import java.util.stream.Stream;public class Test04 {public static void main(String[] args) {// 通过静态方法进行调用Stream<Integer> 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;}@Overridepublic String toString() {return "Employee{" + "id=" + id + ", name='" + name + '\\'' + ", age=" + age + ", salary=" + salary + '}';}@Overridepublic 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);}@Overridepublic 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<Employee> getEmployees(){List<Employee> 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<Employee> list = EmployeeData.getEmployees();// 实例StreamStream<Employee> 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<Employee> list = EmployeeData.getEmployees();// 实例StreamStream<Employee> 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<Employee> list = EmployeeData.getEmployees();// 实例StreamStream<Employee> 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<Employee> 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));// 实例StreamStream<Employee> 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<String> list = Arrays.asList("aa", "bb", "cc");// 实例StreamStream<String> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> list = EmployeeData.getEmployees();// 返回的是一个OptionOptional<Employee> 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<Employee> 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<Employee> list = EmployeeData.getEmployees();// 先获取返回最高工资的员工// 得到的是一个OptionOptional<Employee> 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<Employee> 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<T> |
需求:计算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<Integer> 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<Employee> 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<T, ?, List<T>> | 把流中元素收集到List |
List<Employee> emps= list.stream().collect(Collectors.toList());
方法 | 返回类型 | 作用 |
---|---|---|
toSet | Collector<T, ?, Set<T>> | 把流中元素收集到Set |
Set<Employee> emps= list.stream().collect(Collectors.toSet());
方法 | 返回类型 | 作用 |
---|---|---|
toCollection | Collector<T, ?, C> | 把流中元素收集到创建的集合 |
Collection<Employee> 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<Employee> list = EmployeeData.getEmployees();// 进行过滤(并把数据存储到List集合)List<Employee> list1 = list.stream().filter(emp -> emp.getSalary() > 6000).collect(Collectors.toList());// 进行打印list1.forEach(System.out::println);}
}
3.6 新的时间和日期 API
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(); // 1681181172832System.out.println(timeMillis);// 只有一个有参构造器java.sql.Date date = new Date(timeMillis);// 只打印年月日,但是其实也包含时分秒,只是没有显示,从下面调用getTime方法的结果与前面的时间戳相等也能体现System.out.println(date); // 2023-04-11System.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);// 第二种方法(常用)实际上也是子类GregorianCalendarCalendar 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转换为DateSystem.out.println(calendar.getTime()); // Wed Apr 12 11:20:29 CST 2023// setTime方法,使用指定的Date重置CalendarDate 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-11System.out.println(localTime); // 14:41:45.596System.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-11System.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方法转换为LocalDateTimeLocalDateTime localDateTime1 = LocalDateTime.from(temporalAccessor);System.out.println(localDateTime1); // 2023-10-10T15:30:30}
}