JDK8新特性
文章目录
- JDK8新特性
- 前言
- 一、Java发展历史
- 1、Java的发展历史
- 2、OpenJDK和OracleJDK
- 2.1、Open JDK来源
- 2.2、Open JDK 和 Oracle JDK的关系
- 2.3、Open JDK 官网介绍
- 二、Lambda表达式
- 1、需求分析
- 2、Lambda表达式初体验
- 3、Lambda的语法规则
- 3.1、Lambda练习1
- 3.2、Lambda练习2
- 4、@FunctionalInterface注解
- 5、Lambda表达式的原理
- 6、Lambda表达式的省略写法
- 7、Lambda表达式的使用前提
- 8、Lambda和匿名内部类的对比
- 三、JDK8接口中新增的方法
- 1、JDK8中接口的新增
- 2、默认方法
- 2.1 为什么要增加默认方法
- 2.2 接口默认方法的格式
- 3、静态方法
- 3.1、实例代码
- 3.2、静态方法的使用
- 4、两者的区别介绍
- 四、函数式接口
- 1、函数式接口的由来
- 2、函数式接口介绍
- 2.1、Supplier
- 2.2、Consumer
- 2.3、Function
- 2.4、Predicate
- 五、方法引用
- 1、为什么要用方法引用
- 1.1、lambda表达式冗余
- 1.2、解决方案
- 2、方法引用的格式
- 2.1、对象名::方法名
- 2.2、类名::静态方法名
- 2.3、类名::引用实例方法
- 2.4、类名::构造器
- 2.5、数组::构造器
- 2.6、小结
- 六、Stream API
- 1、集合处理数据的弊端
- 2、Steam流式思想概述
- 3、Stream流的获取方式
- 3.1、根据Collection获取
- 3.2、通过Stream的of方法
- 4、Stream常用方法介绍
- 4.1、forEach
- 4.2、count
- 4.3、filter
- 4.4、limit
- 4.5、skip
- 4.6、map
- 4.7、sorted
- 4.8、distinct
- 4.9、match
- 4.10、find
- 4.11、max和min
- 4.12、reduce方法
- 4.13、map和reduce的组合
- 4.14、mapToInt
- 4.15、concat
- 4.16、综合案例
- 5、Stream结果收集
- 5.1、结果收集到集合中
- 5.2、结果收集到数组中
- 5.3、对流中的数据做聚合计算
- 5.4、对流中数据做分组操作
- 5.5、对流中的数据做分区操作
- 5.6、对流中的数据做拼接
- 6、并行的Stream流
- 6.1、串行的Stream流
- 6.2、并行流
- 6.2.1、获取并行流
- 6.2.2、并行流操作
- 6.3、并行流和串行流对比
- 6.4、线程安全问题
- 七、Optional类
- 1、以前对null 的处理
- 2、Optional类
- 3、Optional的基本使用
- 4、Optional的常用方法
- 八、新时间日期API
- 1、旧版日期时间的问题
- 2、新日期时间API介绍
- 2.1、日期时间的常见操作
- 2.2、日期时间的修改和比较
- 2.3、格式化和解析操作
- 2.4、Instant类
- 2.5、计算日期时间差
- 2.6、时间校正器
- 2.7、日期时间的时区
- 2.8、小结
- 九、其他新特性
- 1、重复注解
- 1.1、定义一个重复注解的容器
- 1.2、定义一个可以重复的注解
- 1.3、配置多个重复的注解
- 1.4、解析得到指定的注解
- 2、类型注解
前言
Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台
本文课程大纲介绍:
- 了解Java发展史
- Lambda表达式
- 接口的增强
- 函数式接口
- 方法引用
- Stream API
- Optional
- 新时间日期API
- 其他新特性
一、Java发展历史
1、Java的发展历史
Sun公司在1991年成立了一个称为绿色计划( Green Project )的项目,由James Gosling(高斯林)博土领导,绿色计划的目的是开发一种能够在各种消费性电子产品(机顶盒、冰箱、收音机等)上运行的程序架构。这个项目的 产品就是Java语言的前身: Oak(橡树)。Oak当时在消费品市场上并不算成功,但随着1995年互联网潮流的兴起, Oak迅速找到了最适合自己发展的市场定位。
- 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月Java SE 14(20.3) - 2020年3月Java SE 15(20.9) - 2020年9月
我们可以看到Java SE的主要版本大约每两年发布一次,直到Java SE 6到Java SE 7开始花了五年时间, 之后又花了三年时间到达Java SE 8。
2、OpenJDK和OracleJDK
2.1、Open JDK来源
- Java 由 Sun 公司发明,Open JDK是Sun在2006年末把Java开源而形成的项目。
- 也就是说Open JDK是Java SE平台版的开源和免费实现,它由 SUN 和 Java 社区提供支持
- 2009年 Oracle 收购了 Sun 公司,自此 Java 的维护方之一的SUN 也变成了 Oracle。
2.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是完全开源的,可以自由使用。
2.3、Open JDK 官网介绍
- Open JDK 官网: http://openjdk.java.net
- JDK Enhancement Proposals(JDK增强建议)。通俗的讲JEP就是JDK的新特性
小结:
- Oracle JDK是基于Open JDK源代码的商业版本。我们要学习Java新技术可以去Open JDK 官网学习。
二、Lambda表达式
1、需求分析
创建一个新的线程,指定线程要执行的任务
代码分析:
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 17:13 星期三
*/
public class Test001 {
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());
}
}
执行结果:
主线程=main
新线程=Thread-0
对上面代码我们做如下分析:
- Thread类需要一个Runnable接口作为参数传进去,其中run方法是主要的线程体,执行业务代码到地方
- 为了指定run方法体,不得不需要Runnable的实现类
- 为了省去定义一个Runnable的实现类,我们不得不使用匿名内部类
- 必须覆盖重写抽象的run方法,所有的方法名称,方法参数,方法返回值不得不都重写一遍,而且不能出错
- 但其实,我们只在乎run方法体中的代码
那么有没有简单点的写法呢?
是有点,那就是采用Lambda表达式写
2、Lambda表达式初体验
Lambda表达式是一个匿名函数,可以理解为一段可以传递的代码,如下所示:
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 17:13 星期三
*/
public class Test001 {
public static void main(String[] args){
new Thread(() -> {
System.out.println("新线程=" + Thread.currentThread().getName());
}).start();
System.out.println("主线程=" + Thread.currentThread().getName());
}
}
运行效果都一样,打印结果也一样,但是代码简化了不少
- Lambda表达式的优点:简化了匿名内部类的使用,语法更加简单
- 匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式
3、Lambda的语法规则
Lambda省去了面向对象的条条框框,Lambda的标准格式由3个部分组成:
(参数类型 参数名称) -> {
代码体;
}
格式说明:
- (参数类型 参数名称):参数列表
- {代码体;} :方法体
- -> :箭头,分割参数列表和方法体
3.1、Lambda练习1
我们先练习无参无返回值的Lambda
先定义一个接口:
package cn.wujinagbo.test;
public interface OrderService {
//下单接口
void addOrder();
}
然后创建主方法使用
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 18:25 星期三
*/
public class LambdaDemo001 {
public static void main(String[] args){
//第一种方式:匿名内部类
addOrderTest(new OrderService() {
@Override
public void addOrder() {
System.out.println("匿名内部类:新增一个订单");
}
});
//第二种方式:Lambda写法
addOrderTest(() -> {
System.out.println("Lambda:新增一个订单");
});
}
public static void addOrderTest(OrderService orderService){
orderService.addOrder();
}
}
打印结果:
匿名内部类:新增一个订单
Lambda:新增一个订单
3.2、Lambda练习2
接下来我们一起完成一个有参且有返回值的案例
需求:
创建一个Student对象,然后我们在List集合中保存多个Student对象,然后对这些对象做根据age排序操作
我们先定义一个Student对象:
package cn.wujinagbo.test.dto;
import lombok.Data;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 18:34 星期三
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private Long id;
private String name;
private Integer age;
}
然后开始写代码完成需求:
package cn.wujinagbo.test;
import cn.wujinagbo.test.dto.Student;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 18:25 星期三
*/
public class LambdaDemo002 {
public static void main(String[] args){
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1L, "张三", 20));
studentList.add(new Student(2L, "李四", 22));
studentList.add(new Student(3L, "王五", 28));
studentList.add(new Student(4L, "赵六", 25));
//排序:升序
Collections.sort(studentList, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
});
//打印List集合值
for(int i=0; i<studentList.size(); i++){
System.out.println(studentList.get(i));
}
}
}
打印结果:
Student(id=1, name=张三, age=20)
Student(id=2, name=李四, age=22)
Student(id=4, name=赵六, age=25)
Student(id=3, name=王五, age=28)
从结果我们分析:
我们发现在sort方法的第二个参数是一个Comparator接口的匿名内部类,且执行的方法有参数和返回值
那么我们可以将上面代码改写为Lambda表达式,代码如下:
package cn.wujinagbo.test;
import cn.wujinagbo.test.dto.Student;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 18:25 星期三
*/
public class LambdaDemo002 {
public static void main(String[] args){
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1L, "张三", 20));
studentList.add(new Student(2L, "李四", 22));
studentList.add(new Student(3L, "王五", 28));
studentList.add(new Student(4L, "赵六", 25));
//排序:升序
Collections.sort(studentList, (Student s1, Student s2) -> {
return s1.getAge() - s2.getAge();
});
//打印List集合值
for(int i=0; i<studentList.size(); i++){
System.out.println(studentList.get(i));
}
}
}
该写完之后,执行结果还是一样的
4、@FunctionalInterface注解
- @FunctionalInterface:是一个信息性注解类型,用于指示接口类型符合 Java 语言规范定义的函数式接口要求
- 函数式接口只有一个抽象方法,其他方法都有默认的实现
- 如果接口声明了一个覆盖
java.lang.Object
的公共方法之一的抽象方法,这也不会进入抽象方法计数,因为接口的任何实现都具有来自java.lang.Object
或其他地方的实现
看下面案例:
5、Lambda表达式的原理
首先给出结论:匿名内部类的本质是在编译时生成一个Class文件:XXXXX$1.class
就来下面代码为例:
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 17:13 星期三
*/
public class Test001 {
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());
}
}
我们打开target目录查看一下,如下:
还可以通过反编译工具来查看生成的代码 XJad 工具来查看内容:
那么Lambda表达式的原理是什么呢?
我们代码变成这样了:
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 17:13 星期三
*/
public class Test001 {
public static void main(String[] args){
new Thread(() -> {
System.out.println("新线程=" + Thread.currentThread().getName());
}).start();
System.out.println("主线程=" + Thread.currentThread().getName());
}
}
target下面同样会生成一个Test001$1.class文件
我们也通过反编译工具来查看:
结果发现,写的有Lambda表达式的class文件,通过XJad查看时,会报错,无法查看
那这时,我们只能通过JDK自带的一个工具:javap
javap可以对字节码进行反汇编操作,语法格式如下:
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
好,我们对以下代码使用javap看一下:
package cn.wujinagbo.test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-03 18:25 星期三
*/
public class LambdaDemo001 {
public static void main(String[] args){
//第一种方式:匿名内部类
//addOrderTest(new OrderService() {
// @Override
// public void addOrder() {
// System.out.println("匿名内部类:新增一个订单");
// }
//});
//第二种方式:Lambda写法
addOrderTest(() -> {
System.out.println("Lambda:新增一个订单");
});
}
public static void addOrderTest(OrderService orderService){
orderService.addOrder();
}
}
在这个反编译的源码中我们看到了一个方法 addOrder(),这个方法里面做了什么事情呢?
为了更加直观的理解这个内容,我们可以在运行的时候添加 -Djdk.internal.lambda.dumpProxyClasses, 加上这个参数会将内部class码输出到一个文件中
命令执行:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
也就是运行:
java -Djdk.internal.lambda.dumpProxyClasses cn.wujinagbo.test.LambdaDemo001.class
小结一下:
- 匿名内部类在编译的时候会产生一个class文件
- Lambda表达式在程序运行的时候会形成一个类
- 在类中新增了一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口中重写方法会调用新生成的方法
6、Lambda表达式的省略写法
在lambda表达式的标准写法基础上,可以使用省略写法
规则:
- 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"执行");
});
//简写方式:
Thread t3 = new Thread(() ->
System.out.println(Thread.currentThread().getName()+"执行")
);
- 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写
Collections.sort(ls,(Student o3, Student o4) ->{
return o3.getAge()-o4.getAge();
});
//简写方式:
Collections.sort(ls,(Student o3, Student o4) ->o3.getAge()-o4.getAge());
- 参数类型可以省略不写
Collections.sort(ls,( o3, o4) ->o3.getAge()-o4.getAge());
- 如果只有一个参数,参数类型可以省略,同时()也可以省略
7、Lambda表达式的使用前提
Lambda表达式的语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件需要注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法(@FunctionalInterface)
8、Lambda和匿名内部类的对比
Lambda和匿名内部类的对比
1、所需类型不一样
- 匿名内部类的类型可以是 类,抽象类,接口
- Lambda表达式需要的类型必须是接口
2、抽象方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的
- Lambda表达式所需的接口中只能有一个抽象方法
3、实现原理不一样
- 匿名内部类是在编译后形成一个class
- Lambda表达式是在程序运行的时候动态生成class
三、JDK8接口中新增的方法
1、JDK8中接口的新增
在JDK8中针对接口有做增强,在JDK8之前是这样的:
interface 接口名{
静态常量;
抽象方法;
}
JDK8之后对接口做了增加,接口中可以有默认方法和静态方法了,如下:
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
案例代码:
package cn.wujinagbo.test;
public interface TestInterface {
//常量:默认是 static final 的
String name = "hello";
//抽象方法,默认是public的
void test1();
//默认方法
default void test02(){
System.out.println("我是默认方法");
}
//静态方法
static void test03(){
System.out.println("我是静态方法");
}
}
上面代码是不会报错的,但是在JDK1.7中就会报错了
2、默认方法
2.1 为什么要增加默认方法
在JDK8以前接口中只能有抽象方法和静态常量,会存在以下的问题:
如果接口中新增抽象方法,那么实现类都必须要实现这个抽象方法,非常不利于接口的扩展:
package cn.wujinagbo.test.test001;
public interface DemoA {
void testDemo001();
//接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
void testDemo002();
}
package cn.wujinagbo.test.test001;
public class B implements DemoA {
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
}
package cn.wujinagbo.test.test001;
public class C implements DemoA{
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
}
package cn.wujinagbo.test.test001;
public class DemoTest {
public static void main(String[] args){
DemoA b = new B();
DemoA c = new C();
}
}
2.2 接口默认方法的格式
现在我将DemoA改成下面这样:
package cn.wujinagbo.test.test001;
public interface DemoA {
void testDemo001();
//接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
void testDemo002();
//定义默认方法
default String testDemo003(){
System.out.println("我是默认方法,执行啦.......");
return "我是接口中的默认方法";
}
}
然后B类重写默认方法testDemo003,而C类不重写testDemo003方法:
package cn.wujinagbo.test.test001;
public class B implements DemoA {
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
@Override
public String testDemo003() {
System.out.println("我是B类中的testDemo003");
return "我是B类中的testDemo003";
}
}
package cn.wujinagbo.test.test001;
public class C implements DemoA{
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
}
打印结果:
我是B类中的testDemo003
我是默认方法,执行啦.......
小结:
接口中的默认方法有两种使用方式
- 实现类直接调用接口的默认方法
- 实现类重写接口的默认方法
3、静态方法
JDK8中为接口新增了静态方法,作用也是为了接口的扩展
3.1、实例代码
package cn.wujinagbo.test.test001;
public interface DemoA {
void testDemo001();
//接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
void testDemo002();
//定义默认方法
default String testDemo003(){
System.out.println("我是默认方法,执行啦.......");
return "我是接口中的默认方法";
}
//静态方法
static String testDemo004(){
System.out.println("我是静态方法,执行啦.......");
return "我是接口中的静态方法";
}
}
package cn.wujinagbo.test.test001;
public class B implements DemoA {
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
@Override
public String testDemo003() {
System.out.println("我是B类中的testDemo003");
return "我是B类中的testDemo003";
}
}
package cn.wujinagbo.test.test001;
public class C implements DemoA{
@Override
public void testDemo001() {
}
@Override
public void testDemo002() {
}
}
package cn.wujinagbo.test.test001;
public class DemoTest {
public static void main(String[] args){
DemoA b = new B();
b.testDemo003();
DemoA c = new C();
c.testDemo003();
DemoA.testDemo004();
}
}
运行结果:
我是B类中的testDemo003
我是默认方法,执行啦.......
我是静态方法,执行啦.......
3.2、静态方法的使用
接口中的静态方法在实现类中是不能被重写的,调用的话只能通过接口类型来实现: 接口名.静态方法名();
4、两者的区别介绍
- 默认方法通过实例调用,静态方法通过接口名调用
- 默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
- 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用
四、函数式接口
1、函数式接口的由来
我们知道使用Lambda表达式的前提是需要有函数式接口,而Lambda表达式使用时不关心接口名, 抽象方法名。只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda表达式更加的方 法,在JDK中提供了大量常用的函数式接口
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 08:37 星期一
*/
public class Demo001Fun {
public static void main(String[] args){
function001((param) -> {
int sum = 0;
for(int i : param){
sum += i;
}
return sum;
});
}
public static void function001(Test test){
int[] param = {1,2,3,4,5,6,7};
int sum = test.getSum(param);
System.out.println("sum=" + sum);
}
}
//函数式接口
@FunctionalInterface
interface Test{
int getSum(int param[]);
}
打印结果:
sum=28
2、函数式接口介绍
在JDK中帮我们提供的有函数式接口,主要是在 java.util.function 包中。
2.1、Supplier
无参有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型。
package cn.wujinagbo.demo;
@FunctionalInterface
public interface Supplier<T>{
//获取一个结果
T get();
}
使用:
package cn.wujinagbo.demo;
import java.util.Arrays;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 08:42 星期一
*/
public class SupplierTest {
public static void main(String[] args){
function001(() -> {
int param[] = {1,56,2,45,34,12,901};
//升序排序
Arrays.sort(param);
//返回数组中最大的值
return param[param.length - 1];
});
}
public static void function001(Supplier<Integer> supplier){
//get() 方法是一个无参有返回值的抽象方法
Integer max = supplier.get();
System.out.println("max=" + max);
}
}
2.2、Consumer
有参无返回值得接口,前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型
package cn.wujinagbo.demo;
@FunctionalInterface
public interface Consumer<T> {
//对给定参数执行此操作
void receive(T t);
}
使用:将输入的数据统一转换为小写输出
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 08:50 星期一
*/
public class ConsumerTest {
public static void main(String[] args){
functtion001(msg -> {
System.out.println(msg + "转换成小写:" + msg.toLowerCase());
});
}
public static void functtion001(Consumer<String> consumer){
consumer.receive("Hello World");
}
}
打印结果:
Hello World转换成小写:hello world
还可以使用默认方法:andThen
如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现效果,消费一个数据的时候, 首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default方法andThen方法
现在我们将上面的Consumer接口改成如下这样,加上andThen默认方法:
package cn.wujinagbo.demo;
import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {
//对给定参数执行此操作
void receive(T t);
default Consumer<T> andThen(Consumer<? super T> after){
Objects.requireNonNull(after);
return (T t) -> {receive(t); after.receive(t);};
}
}
然后再测试一下:
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 08:50 星期一
*/
public class ConsumerTest {
public static void main(String[] args){
functtion002(msg1 -> {
System.out.println(msg1 + "转换成小写:" + msg1.toLowerCase());
}, msg2 -> {
System.out.println(msg2 + "转换成大写:" + msg2.toUpperCase());
});
}
public static void functtion001(Consumer<String> consumer){
consumer.receive("Hello World");
}
public static void functtion002(Consumer<String> c1, Consumer<String> c2){
String str = "Hello World";
c1.andThen(c2).receive(str);
}
}
打印结果:
Hello World转换成小写:hello world
Hello World转换成大写:HELLO WORLD
从结果可以看出,c1执行完成后再执行c2,如果这样写:
c2.andThen(c1).receive(str);
那么执行结果就会反过来了:
Hello World转换成大写:HELLO WORLD
Hello World转换成小写:hello world
2.3、Function
有参有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
package cn.wujinagbo.demo;
@FunctionalInterface
public interface Function<T, R> {
//将此函数应用于给定参数
R apply(T t);
}
下面我们来使用:传递进入一个字符串返回一个数字
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 09:15 星期一
*/
public class FunctionTest {
public static void main(String[] args){
function1(msg -> {
return Integer.parseInt(msg);
});
}
public static void function1(Function<String, Integer> function){
Integer apply = function.apply("123456");
System.out.println("apply=" + apply);
}
}
执行结果:
apply=123456
默认方法:andThen,也是用来进行组合操作:
package cn.wujinagbo.demo;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
//将此函数应用于给定参数
R apply(T t);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after){
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
}
测试代码:
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 09:15 星期一
*/
public class FunctionTest {
public static void main(String[] args){
function1(msg1 -> {
return Integer.parseInt(msg1);
}, msg2 -> {
return msg2 + 4;
});
}
public static void function1(Function<String, Integer> f1, Function<Integer, Integer> f2){
Integer apply = f1.andThen(f2).apply("123456");
System.out.println("apply=" + apply);
}
}
打印结果:
apply=123460
从结果可以看出,先对123456字符串进行转换,转成Integer类型后,再加4操作
2.4、Predicate
有参且返回值为Boolean的接口
package cn.wujinagbo.demo;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
使用:
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 09:27 星期一
*/
public class PredicateTest {
public static void main(String[] args){
function1(msg -> {
return msg.length() > 3;
}, "HelloWorld");
}
private static void function1(Predicate<String> predicate, String msg){
boolean result = predicate.test(msg);
System.out.println("result=" + result);
}
}
运行结果:
result=true
从结果可以看出,计算字符串【HelloWorld】的长度大于3,所以返回了true
五、方法引用
1、为什么要用方法引用
1.1、lambda表达式冗余
在使用Lambda表达式的时候,也会出现代码冗余的情况,比如:用Lambda表达式求一个数组的和
package cn.wujinagbo.demo;
import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {
//对给定参数执行此操作
void receive(T t);
}
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 09:32 星期一
*/
public class FunctionRefTest01 {
public static void main(String[] args){
printSum(a -> {
//Lambda表达式中的代码和getTotal中的代码冗余了
int sum = 0;
for(int i : a){
sum += i;
}
System.out.println("数组之和=" + sum);
});
}
public void getTotal(int a[]){
int sum = 0;
for(int i : a){
sum += i;
}
System.out.println("数组之和=" + sum);
}
private static void printSum(Consumer<int[]> consumer){
int[] a = {20,30,40,50,60,70,90};
consumer.receive(a);
}
}
打印结果:
数组之和=360
1.2、解决方案
因为在Lambda表达式中要执行的代码和我们另一个方法中的代码是一样的,这时就没有必要重写一 份逻辑了,这时我们就可以“引用”重复代码
package cn.wujinagbo;
import cn.wujinagbo.demo.Consumer;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 09:38 星期一
*/
public class FunctionRefTest02 {
public static void main(String[] args){
// :: 方法引用 也是JDK8中的新的语法
printSum(FunctionRefTest02 :: getTotal);
}
//计算入参数组中的和
public static void getTotal(int a[]){
int sum = 0;
for(int i : a){
sum += i;
}
System.out.println("数组之和=" + sum);
}
private static void printSum(Consumer<int[]> consumer){
int[] a = {20,30,40,50,60,70,90};
consumer.receive(a);
}
}
打印结果还是:
数组之和=360
:: 方法引用,也是JDK8中的新的语法
2、方法引用的格式
符号表示: ::
符号说明:双冒号为方法引用运算符,而它所在的表达式被称为方法引用
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用
常见的引用方式:
方法引用在JDK8中使用是相当灵活的,有以下几种形式:
- instanceName::methodName 对象::方法名
- ClassName::staticMethodName 类名::静态方法
- ClassName::methodName 类名::普通方法
- ClassName::new 类名::new 调用的构造器
- TypeName[]::new String[]::new 调用数组的构造器
2.1、对象名::方法名
这是最常见的一种用法。如果一个类中的已经存在了一个成员方法,则可以通过对象名引用成员方法
package cn.wujinagbo.demo;
@FunctionalInterface
public interface Supplier<T>{
//获取一个结果
T get();
}
public static void main(String[] args){
Date now = new Date();
Supplier<Long> supplier1 = () -> {return now.getTime();};
System.out.println(supplier1.get());
//然后我们通过 方法引用 的方法来处理
Supplier<Long> supplier2 = now :: getTime;
System.out.println(supplier2.get());
}
运行结果是一样的
方法引用的注意事项:
- 被引用的方法,参数要和接口中的抽象方法的参数一样
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值
2.2、类名::静态方法名
也是比较常用的方式:
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());
}
打印结果也是一样的
2.3、类名::引用实例方法
Java面向对象中,类名只能调用静态方法,类名引用实例方法是用前提的,实际上是拿第一个参数作为方法的调用者
package cn.wujinagbo.test;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:10 星期一
*/
public class FunctionRefTest05 {
public static void main(String[] args){
Function<String, Integer> function1 = (s) -> {
return s.length();
};
System.out.println(function1.apply("hello world"));
//通过方法引用来实现
Function<String, Integer> function2 = String :: length;
System.out.println(function2.apply("wa ha ha"));
BiFunction<String, Integer, String> function3 = String :: substring;
String msg = function3.apply("HelloWorld", 5);
System.out.println(msg);
}
}
2.4、类名::构造器
由于构造器的名称和类名完全一致,所以构造器引用使用::new 的格式使用,如下:
package cn.wujinagbo.test;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:17 星期一
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private Integer age;
}
package cn.wujinagbo.test;
import cn.wujinagbo.demo.Supplier;
import java.util.function.BiFunction;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:18 星期一
*/
public class FunctionRefTest06 {
public static void main(String[] args){
Supplier<Student> student1 = () -> {
return new Student("张三", 23);
};
System.out.println(student1.get());
//通过方法引用来实现
Supplier<Student> student2 = Student :: new;
System.out.println(student2.get());
BiFunction<String, Integer, Student> student3 = Student :: new;
System.out.println(student3.apply("李四", 29));
}
}
运行结果:
Student(name=张三, age=23)
Student(name=null, age=null)
Student(name=李四, age=29)
2.5、数组::构造器
package cn.wujinagbo.test;
import java.util.function.Function;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:22 星期一
*/
public class FunctionRefTest07 {
public static void main(String[] args){
Function<Integer, String[]> fun1 = (length) -> {
return new String[length];
};
String[] a1 = fun1.apply(5);
System.out.println("数组a1的长度=" + a1.length);
//方法引用的方式来调用数组的构造器
Function<Integer, String[]> fun2 = String[] :: new;
String[] a2 = fun2.apply(10);
System.out.println("数组a2的长度=" + a2.length);
}
}
打印结果:
数组a1的长度=5
数组a2的长度=10
2.6、小结
方法引用是对 Lambda 表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加的精简,也可以理解为lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法
六、Stream API
1、集合处理数据的弊端
当我们在需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历,如下所示:
package cn.wujinagbo.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:30 星期一
*/
public class StreamTest01 {
public static void main(String[] args) {
//定义一个集合
List<String> list = Arrays.asList("张三", "张无忌", "李四", "王五", "赵六", "陈军", "刘德华");
//需求1:查询出所有姓张的信息
List<String> list1 = new ArrayList<>();
for (String s : list) {
if (s.startsWith("张")) {
list1.add(s);
}
}
//输出结果
for (String s : list1) {
System.out.println(s);
}
System.out.println("=============================");
//需求2:查询出所有长度为3的信息
List<String> list2 = new ArrayList<>();
for (String s : list) {
if (s.length() == 3) {
list2.add(s);
}
}
//输出结果
for (String s : list2) {
System.out.println(s);
}
}
}
运行结果:
张三
张无忌
=============================
张无忌
刘德华
上面的代码针对与我们不同的需求总是一次次的循环循环再循环,这时我们希望有更加高效的处理方式,我们就可以通过JDK8中提供的Stream API来解决这个问题了
Stream更加优雅的解决方案:
package cn.wujinagbo.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:30 星期一
*/
public class StreamTest02 {
public static void main(String[] args) {
//定义一个集合
List<String> list = Arrays.asList("张三", "张无忌", "李四", "王五", "赵六", "陈军", "刘德华");
//1、打印出长度为3且姓张的信息
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(s -> {
System.out.println(s);
});
System.out.println("===================");
//第二张打印方式
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(System.out :: println);
}
}
运行结果:
张无忌
===================
张无忌
上面的SteamAPI代码的含义:获取流,过滤张,过滤长度,逐一打印。代码相比于上面的案例更加的简洁直观
2、Steam流式思想概述
注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有 象!
- Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理
- Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品
- Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约
3、Stream流的获取方式
3.1、根据Collection获取
首先,java.util.Collection 接口中加入了default方法 stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流。
package cn.wujinagbo.test;
import java.util.*;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
List<String> list = new ArrayList<>();
list.stream();
Set<String> set = new HashSet<>();
set.stream();
Vector vector = new Vector();
vector.stream();
}
}
但是Map接口别没有实现Collection接口,那这时怎么办呢?
这时我们可以根据Map获取对应的key value的集合。
package cn.wujinagbo.test;
import java.util.*;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Map<String, Object> map = new HashMap<>();
//key
Stream<String> key = map.keySet().stream();
//value
Stream<Object> value = map.values().stream();
//entry
Stream<Map.Entry<String, Object>> stream = map.entrySet().stream();
}
}
3.2、通过Stream的of方法
在实际开发中我们不可避免的还是会操作到数组中的数据,由于数组对象不可能添加默认方法,所 有Stream接口中提供了静态方法of
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Stream<String> a1 = Stream.of("s1", "s2", "s3");
a1.forEach(System.out :: println);
System.out.println("--------------");
String[] arr1 = {"a1", "a2", "a3"};
Stream<String> arr11 = Stream.of(arr1);
arr11.forEach(System.out :: println);
System.out.println("--------------");
Integer[] arr2 = {1,2,3,4};
Stream<Integer> arr22 = Stream.of(arr2);
arr22.forEach(System.out :: println);
System.out.println("--------------");
//注意:基本数据类型的数组是不行的
int[] arr3 = {1,2,3,4};
Stream.of(arr3).forEach(System.out :: println);
}
}
打印结果:
s1
s2
s3
--------------
a1
a2
a3
--------------
1
2
3
4
--------------
[I@69d0a921
4、Stream常用方法介绍
Stream流模型的操作很丰富,这里介绍一些常用的API。
这些方法可以被分成两种:
- 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 【count 】和 【forEach 】方法。
- 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
Stream注意事项(重要):
- Stream只能操作一次
- Stream方法返回的是新的流
- Stream不调用终结方法,中间的操作不会执行
4.1、forEach
forEach用来遍历流中的数据的
void forEach(Consumer<? super T> action);
该方法接受一个Consumer接口,会将每一个流元素交给函数处理
public static void main(String[] args){
Stream.of("a1","a2","a3").forEach(System.out :: println);
}
4.2、count
Stream流中的count方法用来统计其中的元素个数的
long count();
该方法返回一个long值,代表元素的个数。
public static void main(String[] args){
long count = Stream.of("a1", "a2").count();
System.out.println(count);
}
4.3、filter
filter方法的作用是用来过滤数据的。返回符合条件的数据
如上图所示,按照原型进行过滤
可以通过filter方法将一个流转换成另一个子集流
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个Predicate函数式接口参数作为筛选条件
public static void main(String[] args){
Stream.of("a1", "a2", "a3", "aa", "cc", "bb", "dd")
.filter((s) -> s.contains("a"))
.forEach(System.out :: println);
}
运行结果:
a1
a2
a3
aa
4.4、limit
limit方法可以对流进行截取处理,截取前n个数据
Stream<T> limit(long maxSize);
参数是一个long类型的数值,如果集合当前长度大于参数就进行截取,否则不操作:
public static void main(String[] args){
Stream.of("a1", "a2", "a3", "zz", "cc", "bb", "dd")
.limit(4)
.forEach(System.out :: println);
}
运行结果:
a1
a2
a3
zz
4.5、skip
如果希望跳过前面几个元素,可以使用skip方法获取一个截取之后的新流:
Stream<T> skip(long n);
操作:
public static void main(String[] args){
Stream.of("a1", "a2", "a3", "zz", "cc", "bb", "dd")
.skip(2)
.forEach(System.out :: println);
}
运行结果:
a3
zz
cc
bb
dd
4.6、map
如果我们需要将流中的元素映射到另一个流中,可以使用map方法:
<R> Stream<R> map(Function<? super> T, ? extends R> mapper);
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的数据
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Stream.of("1", "2", "3", "5", "25", "7", "9", "11", "29")
.map(Integer :: parseInt)
.forEach(System.out :: println);
}
}
运行结果:
1
2
3
5
25
7
9
11
29
4.7、sorted
如果需要将数据排序,可以使用sorted方法:
Stream<T> sorted();
在使用的时候可以根据自然规则排序,也可以通过比较强来指定对应的排序规则
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Stream.of("1", "2", "3", "5", "25", "7", "9", "11", "29")
.map(Integer :: parseInt)
.sorted() //升序
//.sorted((o1, o2) -> o2 - o1) //降序
.forEach(System.out :: println);
}
}
运行结果:
1
2
3
5
7
9
11
25
29
4.8、distinct
如果要去掉重复数据,可以使用distinct方法:
Stream<T> distinct();
使用:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Stream.of("1", "3", "3", "5", "6", "7", "1")
.map(Integer :: parseInt)
.sorted((o1, o2) -> o2 - o1) //降序
.distinct() //去掉重复记录
.forEach(System.out :: println);
System.out.println("------------------");
Stream.of(
new Student("张三", 25),
new Student("李四", 29),
new Student("张三", 25),
new Student("王五", 19)
).distinct()
.forEach(System.out :: println);
}
}
运行结果;
7
6
5
3
1
------------------
Student(name=张三, age=25)
Student(name=李四, age=29)
Student(name=王五, age=19)
Stream流中的distinct方法对于基本数据类型是可以直接出重的,但是对于自定义类型,我们是需要重写hashCode和equals方法来移除重复元素。
4.9、match
如果需要判断数据是否匹配指定的条件,可以使用match相关的方法
boolean anyMatch(Predicate<? super T> predicate);//元素是否有任一满足条件
boolean allMatch(Predicate<? super T> predicate);//元素是否都满足条件
boolean noneMatch(Predicate<? super T> predicate);//元素是否都不满足条件
测试代码:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
boolean result = Stream.of("1", "3", "3", "5", "6", "7", "1")
.map(Integer :: parseInt)
.allMatch(s -> s > 0)//返回true
//.anyMatch(s -> s > 4) //返回true
//.noneMatch(s -> s > 4)//返回false
;
System.out.println(result);
}
}
注意:match是一个终结方法
4.10、find
如果我们需要找到某些数据,可以使用find方法来实现
Optional<T> findFirst();//查找Stream中的第一个元素。 当您特别需要序列中的第一个元素时,可以使用此方法
Optional<T> findAny();//顾名思义,findAny()方法允许您从Stream中查找任何元素。 在寻找元素时使用时不会关注元素顺序
实例代码:
package cn.wujinagbo.test;
import java.util.Optional;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Optional<String> first = Stream.of("1", "3", "4", "5", "6", "1", "7").findFirst();
System.out.println(first.get());
Optional<String> any = Stream.of("1", "3", "4", "5", "6", "1", "7").findAny();
System.out.println(any.get());
}
}
运行结果:
1
1
4.11、max和min
如果我们想要获取最大值和最小值,那么可以使用max和min方法
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
示例代码:
package cn.wujinagbo.test;
import java.util.Optional;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest03 {
public static void main(String[] args){
Optional<Integer> max = Stream.of("1", "3", "4", "5", "6", "1", "7")
.map(Integer :: parseInt)
.max((o1, o2) -> o1 - o2);
System.out.println(max.get());
System.out.println("-----------------------");
Optional<Integer> min = Stream.of("1", "3", "4", "5", "6", "1", "7")
.map(Integer :: parseInt)
.min((o1, o2) -> o1 - o2);
System.out.println(min.get());
}
}
运行结果:
7
-----------------------
1
4.12、reduce方法
如果需要将所有数据归纳得到一个数据,可以使用reduce方法
T reduce(T identity, BinaryOperator<T> accumulator);
示例代码:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest04 {
public static void main(String[] args){
//获取所有数的和
Integer sum = Stream.of(4, 5, 6, 9, 2, 3)
//identity默认值
//第一次时,会将默认值赋值给x
//之后每次会将上一次操作结果赋值给x,y就是每次从数据中获取到元素
.reduce(0, (x ,y) -> {
System.out.println("x=" + x + ",y=" + y);
return x+y;
});
System.out.println(sum);
System.out.println("------------");
//获取最大值
Integer max = Stream.of(4, 5, 6, 9, 2, 3)
.reduce(0, (x, y) -> {
return x > y ? x : y;
});
System.out.println(max);
}
}
运行结果:
x=0,y=4
x=4,y=5
x=9,y=6
x=15,y=9
x=24,y=2
x=26,y=3
29
------------
9
4.13、map和reduce的组合
在实际开发中我们经常会将map和reduce一块来使用
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest04 {
public static void main(String[] args){
//1、计算所有人的年龄之和
Integer sumAge = Stream.of(
new Student("刘德华", 24),
new Student("谢霆锋", 22),
new Student("梁朝伟", 25),
new Student("吴彦祖", 23)
).map(Student::getAge)
.reduce(0, Integer::sum);
System.out.println("sumAge=" + sumAge);
System.out.println("-----------------------");
//2、计算所有年龄中的最大值
Integer maxAge = Stream.of(
new Student("刘德华", 24),
new Student("谢霆锋", 22),
new Student("梁朝伟", 25),
new Student("吴彦祖", 23)
).map(Student::getAge)
.reduce(0, Math::max);
System.out.println("maxAge=" + maxAge);
System.out.println("-----------------------");
//3、统计字符【刘】出现的次数
Integer count = Stream.of("张", "李", "刘", "王", "刘"
).map(ch -> "刘".equals(ch) ? 1 : 0)
.reduce(0, Integer :: sum);
System.out.println("count=" + count);
}
}
运行结果:
sumAge=94
-----------------------
maxAge=25
-----------------------
count=2
4.14、mapToInt
如果需要将Stream中的Integer类型转换成int类型,可以使用mapToInt方法来实现
示例代码:
package cn.wujinagbo.test;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:57 星期一
*/
public class StreamTest05 {
public static void main(String[] args){
//Integer 占用的内存比 int 多很多,再 Strenm 流操作中会进行【自动装箱】和【自动拆箱】操作
Integer arr[] = {1,2,3,4,5,7,8,9};
Stream.of(arr)
.filter(i -> i>3)
.forEach(System.out :: println);
System.out.println("----------------------");
//为了提高程序代码到效率,我们可以先将流中的Integer数据转换成int数据,然后再做其他操作
IntStream intStream = Stream.of(arr).mapToInt(Integer :: intValue);
intStream.filter(i -> i>3)
.forEach(System.out :: println);
}
}
运行结果:
4
5
7
8
9
----------------------
4
5
7
8
9
4.15、concat
如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
@SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}
示例代码:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest05 {
public static void main(String[] args){
Stream<String> s1 = Stream.of("a","b","c");
Stream<String> s2 = Stream.of("x","y","z");
//通过concat方法将两个流合并为一个新的流
Stream.concat(s1, s2).forEach(System.out :: println);
}
}
运行结果:
a
b
c
x
y
z
4.16、综合案例
定义两个集合,然后在集合中存储多个用户名称。然后完成如下的操作:
- 第一个队伍只保留姓名长度为3的成员
- 第一个队伍筛选之后只要前3个人
- 第二个队伍只要姓张的成员
- 第二个队伍筛选之后不要前两个人
- 将两个队伍合并为一个队伍
- 根据姓名创建Person对象
- 打印整个队伍的Person信息
代码:
package cn.wujinagbo.test;
import lombok.Data;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
@Data
public class Student {
private String name;
private Integer age;
public Student() {
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public Student(String name) {
this.name = name;
}
}
package cn.wujinagbo.test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest06 {
/**
* 1. 第一个队伍只保留姓名长度为3的成员
* 2. 第一个队伍筛选之后只要前3个人
* 3. 第二个队伍只要姓张的成员
* 4. 第二个队伍筛选之后不要前两个人
* 5. 将两个队伍合并为一个队伍
* 6. 根据姓名创建 Student 对象
* 7. 打印整个队伍的 Student 信息
*/
public static void main(String[] args){
List<String> list1 = Arrays.asList("迪丽热巴", "孙俪", "刘亦菲", "佟丽娅", "关之琳", "王天霸", "张天工");
List<String> list2 = Arrays.asList("赵丽颖", "邓超", "刘德华", "张三丰", "张家辉", "张靓颖", "李四", "王麻子", "张天爱");
//1、第一个队伍只保留姓名长度为3的成员
//2、第一个队伍筛选之后只要前3个人
Stream<String> s1 = list1.stream().filter(s -> s.length() == 3).limit(3);
//3、第二个队伍只要姓张的成员
//4、第二个队伍筛选之后不要前两个人
Stream<String> s2 = list2.stream().filter(s -> s.startsWith("张")).skip(2);
//5、将两个队伍合并为一个队伍
//6、根据姓名创建 Student 对象
//7、打印整个队伍的 Student 信息
Stream.concat(s1, s2)
.map(Student :: new)
.forEach(System.out :: println);
}
}
运行结果:
Student(name=刘亦菲, age=null)
Student(name=佟丽娅, age=null)
Student(name=关之琳, age=null)
Student(name=张靓颖, age=null)
Student(name=张天爱, age=null)
5、Stream结果收集
5.1、结果收集到集合中
代码:
package cn.wujinagbo.test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest07 {
public static void main(String[] args){
//收集到 List 集合中
List<String> list = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toList());
System.out.println(list);
System.out.println("----------------------");
//收集到 Set 集合中
Set<String> set = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toSet());
System.out.println(set);
System.out.println("----------------------");
//如果需要获取的类型为具体的实现,比如:ArrayList/HashSet
ArrayList<String> arrayList = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toCollection(ArrayList::new));
System.out.println(arrayList);
System.out.println("----------------------");
HashSet<String> hashSet = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toCollection(HashSet::new));
System.out.println(hashSet);
}
}
打印结果:
[aa, cc, aa, bb]
----------------------
[aa, cc, bb]
----------------------
[aa, cc, aa, bb]
----------------------
[aa, cc, bb]
5.2、结果收集到数组中
Stream中提供了toArray方法来将结果放到一个数组中,返回值类型是Object[],如果我们要指定返回的类型,那么可以使用另一个重载的toArray(IntFunction f)方法
代码:
package cn.wujinagbo.test;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest08 {
public static void main(String[] args){
//返回的数组中的元素是 Object 类型
Object[] objects = Stream.of("aa", "cc", "bb", "aa").toArray();
System.out.println(Arrays.toString(objects));
System.out.println("-----------------------");
//如果我们需要指定返回的数组中的元素类型
String[] strArray = Stream.of("aa", "cc", "bb", "aa").toArray(String[] :: new);
System.out.println(Arrays.toString(strArray));
}
}
运行结果:
[aa, cc, bb, aa]
-----------------------
[aa, cc, bb, aa]
5.3、对流中的数据做聚合计算
当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如获得最大 值,最小值,求和,平均值,统计数量。
示例代码:
package cn.wujinagbo.test;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest08 {
public static void main(String[] args) {
// 获取年龄的最大值
Optional<Student> maxAge = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
System.out.println("最大年龄:" + maxAge.get());
System.out.println("------------------");
// 获取年龄的最小值
Optional<Student> minAge = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
System.out.println("最新年龄:" + minAge.get());
System.out.println("------------------");
// 求所有人的年龄之和
Integer sumAge = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
)
.collect(Collectors.summingInt(Student::getAge));
System.out.println("年龄总和:" + sumAge);
System.out.println("------------------");
// 年龄的平均值
Double avgAge = Stream.of(
new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
).collect(Collectors.averagingInt(Student::getAge));
System.out.println("年龄的平均值:" + avgAge);
System.out.println("------------------");
// 统计数量
Long count = Stream.of(
new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
).filter(p -> p.getAge() > 18)
.collect(Collectors.counting());
System.out.println("满足条件的记录数:" + count);
}
}
运行结果:
最大年龄:Student(name=李四, age=22)
------------------
最新年龄:Student(name=张三, age=13)
------------------
年龄总和:87
------------------
年龄的平均值:17.4
------------------
满足条件的记录数:2
5.4、对流中数据做分组操作
当我们使用Stream流处理数据后,可以根据某个属性将数据分组
示例代码:
package cn.wujinagbo.test;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest09 {
public static void main(String[] args) {
// 根据账号对数据进行分组
Map<String, List<Student>> map1 = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
, new Student("张三", 11)
).collect(Collectors.groupingBy(Student::getName));
map1.forEach((k, v) -> System.out.println("key=" + k + "\t" + "value=" + v));
System.out.println("------------------");
//根据年龄分组,小于18则是未成年
Map<String, List<Student>> map2 = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
, new Student("张三", 11)
).collect(Collectors.groupingBy(s -> s.getAge() >= 18 ? "成年" : "未成年"));
map2.forEach((k, v) -> System.out.println("key=" + k + "\t" + "value=" + v));
}
}
运行结果:
key=李四 value=[Student(name=李四, age=22)]
key=张三 value=[Student(name=张三, age=18), Student(name=张三, age=13), Student(name=张三, age=19), Student(name=张三, age=11)]
key=王五 value=[Student(name=王五, age=15)]
------------------
key=未成年 value=[Student(name=张三, age=13), Student(name=王五, age=15), Student(name=张三, age=11)]
key=成年 value=[Student(name=张三, age=18), Student(name=李四, age=22), Student(name=张三, age=19)]
多级分组,根据name分组后再根据年龄分组
代码:
package cn.wujinagbo.test;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest10 {
public static void main(String[] args) {
// 先根据name分组,然后根据age分组为:成年人和未成年人
Map<String, Map<Object, List<Student>>> map1 = Stream.of(new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
, new Student("张三", 11)
).collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(s -> s.getAge() >= 18 ? "成年" : "未成年")));
map1.forEach((k, v) -> {
System.out.println(k);
v.forEach((k1, v1) -> {
System.out.println("\t" + k1 + "=" + v1);
});
});
}
}
运行结果:
李四
成年=[Student(name=李四, age=22)]
张三
未成年=[Student(name=张三, age=13), Student(name=张三, age=11)]
成年=[Student(name=张三, age=18), Student(name=张三, age=19)]
王五
未成年=[Student(name=王五, age=15)]
5.5、对流中的数据做分区操作
Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个false列表
示例代码:
package cn.wujinagbo.test;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest11 {
public static void main(String[] args) {
Map<Boolean, List<Student>> map = Stream.of(
new Student("张三", 18)
, new Student("李四", 22)
, new Student("张三", 13)
, new Student("王五", 15)
, new Student("张三", 19)
, new Student("张三", 11)
).collect(Collectors.partitioningBy(s -> s.getAge() > 18));
map.forEach((k, v) -> System.out.println(k + "\t" + v));
}
}
运行结果:
false [Student(name=张三, age=18), Student(name=张三, age=13), Student(name=王五, age=15), Student(name=张三, age=11)]
true [Student(name=李四, age=22), Student(name=张三, age=19)]
5.6、对流中的数据做拼接
Collectors.joining会根据指定的连接符,将所有的元素连接成一个字符串
代码:
package cn.wujinagbo.test;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest12 {
public static void main(String[] args) {
String s1 = Stream.of(
new Student("张三", 18)
,new Student("李四", 22)
,new Student("张三", 14)
,new Student("李四", 15)
,new Student("张三", 19)
).map(Student::getName).collect(Collectors.joining());
System.out.println("s1=" + s1);
System.out.println("-------------------");
String s2 = Stream.of(
new Student("张三", 18)
,new Student("李四", 22)
,new Student("张三", 14)
,new Student("李四", 15)
,new Student("张三", 19)
).map(Student::getName).collect(Collectors.joining("_"));
System.out.println("s2=" + s2);
System.out.println("-------------------");
String s3 = Stream.of(
new Student("张三", 18)
,new Student("李四", 22)
,new Student("张三", 14)
,new Student("李四", 15)
,new Student("张三", 19)
).map(Student::getName).collect(Collectors.joining("_", "###", "***"));
System.out.println("s3=" + s3);
}
}
运行结果:
s1=张三李四张三李四张三
-------------------
s2=张三_李四_张三_李四_张三
-------------------
s3=###张三_李四_张三_李四_张三***
6、并行的Stream流
6.1、串行的Stream流
我们前面使用的Stream流都是串行,也就是在一个线程上面执行
示例代码:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest13 {
public static void main(String[] args) {
Stream.of(5,6,8,3,1,6)
.filter(s -> {
System.out.println(Thread.currentThread() + "=" + s);
return s > 3;
}).count();
}
}
运行结果:
Thread[main,5,main]=5
Thread[main,5,main]=6
Thread[main,5,main]=8
Thread[main,5,main]=3
Thread[main,5,main]=1
Thread[main,5,main]=6
6.2、并行流
parallelStream其实就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度
6.2.1、获取并行流
我们可以通过两种方式来获取并行流。
- 通过List接口中的parallelStream方法来获取
- 通过已有的串行流转换为并行流(parallel)
示例代码:
package cn.wujinagbo.test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest14 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//通过List接口直接获取并行流
Stream<Integer> integerStream = list.parallelStream();
//将已有点串行流转换为并行流
Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
}
}
6.2.2、并行流操作
示例代码:
package cn.wujinagbo.test;
import java.util.stream.Stream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest15 {
public static void main(String[] args) {
Stream.of(1,4,2,6,1,5,9)
.parallel() //将流转换为并发流 ,Stream处理的时候就会通过多线程处理
.filter(s -> {
System.out.println(Thread.currentThread() + " s=" + s);
return s > 2;
}).count();
}
}
运行结果:
Thread[ForkJoinPool.commonPool-worker-3,5,main] s=1
Thread[ForkJoinPool.commonPool-worker-5,5,main] s=2
Thread[ForkJoinPool.commonPool-worker-3,5,main] s=6
Thread[ForkJoinPool.commonPool-worker-4,5,main] s=5
Thread[main,5,main] s=1
Thread[ForkJoinPool.commonPool-worker-1,5,main] s=4
Thread[ForkJoinPool.commonPool-worker-2,5,main] s=9
6.3、并行流和串行流对比
我们通过for循环,串行Stream流,并行Stream流来对500000000亿个数字求和。来看消耗时间
示例代码:
package cn.wujinagbo.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.stream.LongStream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest16 {
private static long times = 600000000;//6亿
private long start;
@Before
public void start(){
start = System.currentTimeMillis();
}
@After
public void end(){
long end = System.currentTimeMillis();
System.out.println("消耗时间:" + (end - start));
}
/**
* 普通for循环,消耗时间:217
*/
@Test
public void test001(){
System.out.println("普通for循环");
long res = 0;
for(int i=0; i<times; i++){
res += i;
}
System.out.println("res=" + res);
}
/**
* 串行流处理,消耗时间:250
*/
@Test
public void test002(){
System.out.println("串行流处理");
LongStream.rangeClosed(0, times).reduce(0, Long :: sum);
}
/**
* 并行流处理,消耗时间:131
*/
@Test
public void test003(){
System.out.println("并行流处理");
LongStream.rangeClosed(0, times).parallel().reduce(0, Long :: sum);
}
}
运行结果:耗时已经写明在方法的注释上了
pom测试包依赖:
<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
通过案例我们可以看到parallelStream的效率是最高的
Stream并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作
6.4、线程安全问题
在多线程的处理下,肯定会出现数据安全问题。如下:
package cn.wujinagbo.test;
import java.util.ArrayList;
import java.util.List;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest17 {
public static void main(String[] args){
List<Integer> list = new ArrayList<>();
for(int i=0; i<1000; i++){
list.add(i);
}
System.out.println(list.size());
List<Integer> listNew = new ArrayList<>();
//使用并行流来向集合中添加数据
list.parallelStream().forEach(listNew :: add);
System.out.println(listNew.size());
}
}
运行效果:
1000
934
或者直接抛异常:
针对这个问题,我们的解决方案有哪些呢?
- 加同步锁
- 使用线程安全的容器
- 通过Stream中的toArray/collect操作
代码实现:
package cn.wujinagbo.test;
import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 星期一
*/
public class StreamTest18 {
/**
* 加同步锁
* 运行结果:1000
*/
@Test
public void test001(){
List<Integer> listNew = new ArrayList<>();
Object obj = new Object();
IntStream.rangeClosed(1, 1000)
.parallel()
.forEach(i -> {
synchronized (obj){
listNew.add(i);
}
});
System.out.println(listNew.size());
}
/**
* 使用线程安全的容器
* 运行结果:1000
*/
@Test
public void test002(){
Vector v = new Vector();
Object obj = new Object();
IntStream.rangeClosed(1, 1000)
.parallel()
.forEach( i-> {
synchronized (obj){
v.add(i);
}
});
System.out.println(v.size());
}
/**
* 将线程不安全的容器转换为线程安全的容器
* 运行结果:1000
*/
@Test
public void test003(){
List<Integer> listNew = new ArrayList<>();
//将线程不安全的容器包装为线程安全的容器
List<Integer> synchronizedList = Collections.synchronizedList(listNew);
IntStream.rangeClosed(1, 1000)
.parallel()
.forEach(i -> {
synchronizedList.add(i);
});
System.out.println(synchronizedList.size());
}
/**
* 还可以通过Stream中的 toArray 方法或者 collect 方法来操作
* 就是满足线程安全的要求
* 运行结果:1000
*/
@Test
public void test004(){
List<Integer> listNew = new ArrayList<>();
Object obj = new Object();
//将线程不安全的容器包装为线程安全的容器
List<Integer> list = IntStream.rangeClosed(1, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println(list.size());
}
}
七、Optional类
这个Optional类注意是解决空指针的问题
1、以前对null 的处理
以前是这样对null值进行处理的,如下:
package cn.wujinagbo.demo;
import org.junit.Test;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class OptionalTest01 {
@Test
public void test001(){
//String userName = "王天霸";
String userName = null;
if(userName != null){
System.out.println("字符串长度=" + userName.length());
}else{
System.out.println("字符串为空");
}
}
}
2、Optional类
Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象,它的主要作用就是为了避免Null检查,防止NullpointerException,如下:
3、Optional的基本使用
Optional对象的创建方式如下:
/**
* Optional 对象的创建方式
*/
@Test
public void test002(){
//第一种方式:通过 of 方法,但 of 方法是不支持null的
Optional<String> op1 = Optional.of("王天霸");
//Optional<String> op2 = Optional.of(null);//这样写会报NullPointerException
//第二种方式:通过 ofNullable 方法,支持null
Optional<String> op3 = Optional.ofNullable("王天霸");
Optional<String> op4 = Optional.ofNullable(null);
//第三种方式:通过 empty 方法直接创建一个空的 Optional 对象
Optional<Object> op5 = Optional.empty();
}
4、Optional的常用方法
@Test
public void test04(){
Optional<String> op1 = Optional.of("zhangsan");
Optional<String> op2 = Optional.empty();
//获取 Optional 中的值
if(op1.isPresent()){
String s1 = op1.get();
System.out.println("s1=" + s1);
}
if(op2.isPresent()){
System.out.println("s2=" + op2.get());
}else{
System.out.println("op2是一个空Optional对象");
}
String s3 = op1.orElse("李四");
System.out.println("s3=" + s3);
String s4 = op1.orElse("王麻子");
System.out.println("s4=" + s4);
String s5 = op2.orElseGet(() -> {
return "Hello";
});
System.out.println("s5=" + s5);
//如果存在值就做什么
op1.ifPresent(s -> System.out.println("有值啊=" + s));
op1.ifPresent(System.out :: println);
}
运行结果:
s1=zhangsan
op2是一个空Optional对象
s3=zhangsan
s4=zhangsan
s5=Hello
有值啊=zhangsan
zhangsan
/**
* 需求:
* 自定义一个方法 getNameForOptional,将 Student 对象中的 name 转换为大写,并返回
*/
@Test
public void test03(){
//Student p = new Student(null,18);//打印:name=空值
Student p = new Student("zhangsan",18);//打印:name=ZHANGSAN
Optional<Student> op = Optional.of(p);
String name = getNameForOptional(op);
System.out.println("name=" + name);
}
/**
* 根据 Student 对象 将name转换为大写并返回
* 通过Optional方式实现
* @param op
* @return
*/
public String getNameForOptional(Optional<Student> op){
if(op.isPresent()){
String msg = op.map(Student::getName)
.map(String::toUpperCase)
.orElse("空值");
return msg;
}
return null;
}
八、新时间日期API
1、旧版日期时间的问题
在旧版本中JDK对于日期和时间这块的体验是非常差的,我们来看下示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest01 {
@Test
public void test01(){
//设计不合理
Date date = new Date(2022, 8, 16);
System.out.println("date=" + date);
//时间格式化和解析操作时线程不安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for(int i=0; i<50; i++){
new Thread(() -> {
try {
System.out.println(sdf.parse("2022-08-16"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
运行结果:
有如下缺陷:
- 设计不合理,在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间的,而java.sql.Date仅仅包含日期,此外用于格式化和解析的类在java.text包下
- 非线程安全,java.util.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一
- 时区处理麻烦,日期类并不提供国际化,没有时区支持
2、新日期时间API介绍
JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于java.time 包
中,下面是一些关键类。
- LocalDate :表示日期,包含年月日,格式为 2019-10-16
- LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
- LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
- DateTimeFormatter :日期时间格式化类
- Instant:时间戳,表示一个特定的时间瞬间
- Duration:用于计算2个时间(LocalTime,时分秒)的距离
- Period:用于计算2个日期(LocalDate,年月日)的距离
- ZonedDateTime :包含时区的时间
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天, 闰年是366天。
此外Java 8还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中华民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
2.1、日期时间的常见操作
LocalDate,LocalTime以及LocalDateTime的常见操作方式
LocalDate示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.LocalDate;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest02 {
//JDK8日期操作
@Test
public void test01(){
//创建指定的日期
LocalDate date1 = LocalDate.of(2022, 8, 16);
System.out.println("date1=" + date1);
//得到当前日期
LocalDate now = LocalDate.now();
System.out.println("now=" + now);
//根据 LocalDate 对象获取对应的日期信息
System.out.println("年:" + now.getYear());
System.out.println("月:" + now.getMonth().getValue());
System.out.println("日:" + now.getDayOfMonth());
System.out.println("星期:" + now.getDayOfWeek().getValue());
}
}
打印结果:
date1=2022-08-16
now=2022-08-16
年:2022
月:8
日:16
星期:2
LocalTime示例代码:
//JDK8时间操作
@Test
public void test02(){
//得到指定的时间
LocalTime time = LocalTime.of(9, 33, 33, 23145);
System.out.println("time=" + time);
//得到当前时间
LocalTime now = LocalTime.now();
System.out.println("now=" + now);
//获取时间信息
System.out.println("小时:" + now.getHour());
System.out.println("分钟:" + now.getMinute());
System.out.println("秒:" + now.getSecond());
System.out.println("纳米秒:" + now.getNano());
}
打印结果:
time=09:33:33.000023145
now=09:38:24.610
小时:9
分钟:38
秒:24
纳米秒:610000000
LocalDateTime示例代码:
//日期时间类型 LocalDateTime
@Test
public void test03(){
//得到指定的时间
LocalDateTime dateTime = LocalDateTime.of(2022, 8, 16, 9, 35, 25, 14154);
System.out.println("dateTime=" + dateTime);
//得到当前的日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now=" + now);
//得到日期时间信息
System.out.println("年:" + now.getYear());
System.out.println("月:" + now.getMonth().getValue());
System.out.println("日:" + now.getDayOfMonth());
System.out.println("星期:" + now.getDayOfWeek().getValue());
System.out.println("小时:" + now.getHour());
System.out.println("分钟:" + now.getMinute());
System.out.println("秒:" + now.getSecond());
System.out.println("纳米秒:" + now.getNano());
}
打印结果:
dateTime=2022-08-16T09:35:25.000014154
now=2022-08-16T09:42:37.852
年:2022
月:8
日:16
星期:2
小时:9
分钟:42
秒:37
纳米秒:852000000
2.2、日期时间的修改和比较
示例代码:
//日期时间的修改
@Test
public void test01(){
LocalDateTime now = LocalDateTime.now();
//修改日期时间
LocalDateTime localDateTime = now.withYear(1991);
System.out.println("now=" + now);
System.out.println("修改后的now:");
System.out.println("now=" + localDateTime);
System.out.println("月:" + now.withMonth(12));
System.out.println("天:" + now.withDayOfMonth(5));
System.out.println("小时:" + now.withHour(16));
System.out.println("分钟:" + now.withMinute(12));
//在当前日期时间的基础上,加上或者减去指定的时间
System.out.println("3天后" + now.plusDays(3));
System.out.println("10年后" + now.plusYears(10));
System.out.println("6个月后" + now.plusMonths(6));
System.out.println("5年前" + now.minusYears(5));
System.out.println("半年前" + now.minusMonths(6));
System.out.println("两周前" + now.minusDays(14));
}
运行结果:
now=2022-08-16T09:53:12.558
修改后的now:
now=1991-08-16T09:53:12.558
月:2022-12-16T09:53:12.558
天:2022-08-05T09:53:12.558
小时:2022-08-16T16:53:12.558
分钟:2022-08-16T09:12:12.558
3天后2022-08-19T09:53:12.558
10年后2032-08-16T09:53:12.558
6个月后2023-02-16T09:53:12.558
5年前2017-08-16T09:53:12.558
半年前2022-02-16T09:53:12.558
两周前2022-08-02T09:53:12.558
比较案例:
//日期时间的比较
@Test
public void test02(){
LocalDate now = LocalDate.now();//今天是2022.8.16
LocalDate date = LocalDate.of(2020, 1, 15);
//JDK8中实现日期的比较,用如下三个方法:
System.out.println(now.isAfter(date));//打印:true
System.out.println(now.isBefore(date));//打印:false
System.out.println(now.isEqual(date));//打印:false
}
注意:在进行日期时间修改的时候,原来的LocalDate对象是不会被修改,每次操作都是返回了一个新的LocalDate对象,所以在多线程场景下是数据安全的
2.3、格式化和解析操作
在JDK8中我们可以通过 java.time.format.DateTimeFormatter 类可以进行日期的解析和格式化操作
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest04 {
//日期格式化
@Test
public void test01(){
LocalDateTime now = LocalDateTime.now();
//指定格式:使用系统默认的格式
DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
//将日期时间转换为字符串
String format1 = now.format(isoLocalDateTime);
System.out.println("format1=" + format1);
//通过 ofPattern 方法来指定特定的格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format2 = now.format(dateTimeFormatter);
System.out.println("format2=" + format2);
//将字符串解析为一个 日期时间类型
LocalDateTime parse = LocalDateTime.parse("1991-05-13 22:22:22", dateTimeFormatter);
System.out.println("parse=" + parse);
}
}
运行结果:
format1=2022-08-16T10:29:01.799
format2=2022-08-16 10:29:01
parse=1991-05-13T22:22:22
2.4、Instant类
在JDK8中给我们新增一个Instant类(时间戳/时间线),内部保存了从1970年1月1日00:00:00以来的秒和纳秒
示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest05 {
@Test
public void test01() throws InterruptedException {
Instant now1 = Instant.now();
//获取从1970年1月1日 00:00:00到现在的纳秒值
System.out.println("now1=" + now1.getNano());
Thread.sleep(5);
Instant now2 = Instant.now();
System.out.println("now2=" + now2.getNano());
System.out.println("耗时:" + (now2.getNano() - now1.getNano()));
}
}
运行结果:
now1=768000000
now2=773000000
耗时:5000000
2.5、计算日期时间差
JDK8中提供了两个工具类Duration/Period:计算日期时间差
- Duration:用来计算两个时间差(LocalTime)
- Period:用来计算两个日期差(LocalDate)
示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.*;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest06 {
@Test
public void test01() throws InterruptedException {
//计算时间差
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(12, 18,30);
System.out.println("now=" + now);//打印结果:10:39:06.054
//通过 Duration 来计算时间差
Duration duration = Duration.between(now, time);
System.out.println(duration.toDays());//打印结果:0
System.out.println(duration.toHours());//打印结果:1
System.out.println(duration.toMinutes());//打印结果:99
System.out.println(duration.toMillis());//打印结果:5963946
//计算日期差
LocalDate nowDate = LocalDate.now();
System.out.println("nowDate=" + nowDate);//打印结果:nowDate=2022-08-16
LocalDate date = LocalDate.of(1991, 12, 12);
Period period = Period.between(date, nowDate);
System.out.println(period.getYears());//打印结果:30
System.out.println(period.getMonths());//打印结果:8
System.out.println(period.getDays());//打印结果:4
}
}
2.6、时间校正器
有时候我们可以需要如下调整:将日期调整到【下个月的第一天】等操作。这时我们通过时间校正器效果 可能会更好。
- TemporalAdjuster:时间校正器
- TemporalAdjusters:通过该类静态方法提供了大量的常用 TemporalAdjuster 的实现
示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.*;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest07 {
@Test
public void test01() {
LocalDateTime now = LocalDateTime.now();
//将当前的日期调整到下个月的一号
TemporalAdjuster adjuster = (temporal) -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
System.out.println("nextMonth=" + nextMonth);
return nextMonth;
};
LocalDateTime nextMonth1 = now.with(adjuster);
System.out.println("nextMonth1=" + nextMonth1);
//我们可以通过 TemporalAdjusters 来实现
LocalDateTime nextMonth2 = now.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println("nextMonth2=" + nextMonth2);
}
}
2.7、日期时间的时区
- Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime
- 其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等
- ZoneId:该类中包含了所有的时区信息
示例代码:
package cn.wujinagbo.demo;
import org.junit.Test;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 星期二
*/
public class DateTimeTest08 {
//时区操作
@Test
public void test01() {
//获取所有时区ID
//ZoneId.getAvailableZoneIds().forEach(System.out :: println);
//获取当前时间,中国使用的时区是东八区,比标准时间早8个小时
LocalDateTime now = LocalDateTime.now();
System.out.println("now=" + now);//打印结果:now=2022-08-16T11:11:41.521
//获取标准时间
ZonedDateTime zdt = ZonedDateTime.now(Clock.systemUTC());
System.out.println("zdt=" + zdt);//打印结果:zdt=2022-08-16T03:11:41.521Z
//使用计算机默认的时区,创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
System.out.println("now1=" + now1);//打印结果:now1=2022-08-16T11:11:41.521+08:00[Asia/Shanghai]
//使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
System.out.println("now2=" + now2);//打印结果:now2=2022-08-15T23:11:41.522-04:00[America/Marigot]
}
}
2.8、小结
JDK新的日期和时间API的优势:
- 新版日期时间API中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例
- 提供不同的两种方式,有效的区分了人和机器的操作
- TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整期
- 线程安全
九、其他新特性
1、重复注解
- 自从Java 5中引入 注解 以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是:在同一个地方不能多次使用同一个注解
- JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在JDK 8中使用@Repeatable注解定义重复注解
1.1、定义一个重复注解的容器
package cn.wujinagbo.demo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations{
MyAnnotation[] value();
}
1.2、定义一个可以重复的注解
package cn.wujinagbo.demo;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
1.3、配置多个重复的注解
package cn.wujinagbo.demo;
@MyAnnotation("test1")
@MyAnnotation("test2")
@MyAnnotation("test3")
public class AnnoTest01{
@MyAnnotation("func1")
@MyAnnotation("func2")
public void test01(){
//...
}
}
1.4、解析得到指定的注解
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 11:34 星期二
*/
public class AnnotationTest01 {
//解析重复注解
public static void main(String[] args) throws NoSuchMethodException {
MyAnnotation[] annotationsByType = AnnotationTest01.class.getAnnotationsByType(MyAnnotation.class);
for(MyAnnotation annotation : annotationsByType){
System.out.println(annotation.value());
}
//获取方法上标注的重复注解
MyAnnotation[] annotations = AnnoTest01.class.getMethod("test01").getAnnotationsByType(MyAnnotation.class);
for(MyAnnotation annotation : annotations){
System.out.println(annotation.value());//打印结果:func1和func2
}
}
}
2、类型注解
JDK 8为@Target元注解新增了两种类型: TYPE_PARAMETER , TYPE_USE 。
- TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中
- TYPE_USE :表示注解可以再任何用到类型的地方使用
TYPE_PARAMETER 示例:
package cn.wujinagbo.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE_PARAMETER)
public @interface TypeParam {
}
使用:
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 11:51 星期二
*/
public class TypeDemo01<@TypeParam T> {
public <@TypeParam k extends Object> k test001(){
return null;
}
}
TYPE_USE 示例:
package cn.wujinagbo.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE_USE)
public @interface NotNUll {
}
使用:
package cn.wujinagbo.demo;
/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-16 11:53 星期二
*/
public class TypeDemo02 {
public @NotNUll Integer age = 20;
public Integer sum(@NotNUll Integer i1, @NotNUll Integer i2){
return i1 + i2;
}
}