【JavaSE进阶】10-网络编程 & 11-Lambda表达式 &12-Stream API & 13-Java新特性

10 网络编程

在这里插入图片描述
在这里插入图片描述

10.1 网络编程概述

在这里插入图片描述

10.2 网络编程三要素

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.3 网络编程基础类

在这里插入图片描述

package com.powernode.javase.net;

import java.net.InetAddress;

/**
 * ClassName: InetAddressTest
 * Description:
 *      java.net.IntAddress类用来封装计算机的IP地址和DNS(没有端口信息),
 *      它包括一个主机名和一个IP地址,是java对IP地址的高层表示。大多数其它
 *      网络类都要用到这个类,包括Socket、ServerSocket、URL、
 *      DatagramSocket、DatagramPacket等
 * <p>
 * Datetime: 2024/2/1 10:08
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InetAddressTest {
    public static void main(String[] args) throws Exception{
        // 获取本机的IP地址和主机名的封装对象:InetAddress
        InetAddress ia = InetAddress.getLocalHost();

        // 获取本机的IP地址
        String hostAddress = ia.getHostAddress();
        System.out.println("本机IP地址:" + hostAddress); // 本机IP地址:192.168.137.101

        // 获取本机的主机名
        String hostName = ia.getHostName();
        System.out.println("本机的主机名:" + hostName); // 本机的主机名:MS-AQIGIVITIULB

        // 通过域名来获取InetAddress对象
        InetAddress ia2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ia2.getHostName()); // www.baidu.com
        System.out.println(ia2.getHostAddress()); // 182.61.200.7
    }
}

在这里插入图片描述

package com.powernode.javase.net;

/**
 * ClassName: URLTest01
 * Description:
 *      URL包括四部分:协议,IP地址,端口号,资源名称
 *      URL是网络中某个资源的地址。某个资源的唯一标识。
 *      通过URL是可以真实的定位到资源的。
 *      在Java中,java类库提供了一个URL类,来提供对URL的支持。
 *
 *      URL类的构造方法
 *          URL url = new URL("url");
 *
 *      URL类的常用方法
 *          url.getXxx();
 * <p>
 * Datetime: 2024/2/1 10:20
 * Author: 老杜@动力节点
 * Version: 1.0
 */
import java.net.URL;

public class URLTest01 {
    public static void main(String[] args) throws Exception{
        // 创建URL类型的对象
        URL url = new URL("http://www.baidu.com:8888/oa/index.html?name=zhangsan&password=123#tip");

        // 获取URL中的信息
        String protocol = url.getProtocol();
        System.out.println("协议:" + protocol);

        // 获取资源路径
        String path = url.getPath();
        System.out.println("资源路径:" + path);

        // 获取默认端口(HTTP协议的默认端口是80)
        int defaultPort = url.getDefaultPort();
        System.out.println("默认端口:" + defaultPort);

        // 获取当前的端口
        int port = url.getPort();
        System.out.println("当前端口号:" + port);

        // 获取URL中的IP地址
        String host = url.getHost();
        System.out.println("主机地址:" + host);

        // 获取URL准备传送的数据
        String query = url.getQuery();
        System.out.println("需要提交给服务器的数据:" + query);

        // 获取锚点
        String ref = url.getRef();
        System.out.println("获取锚点:" + ref);

        // 获取 资源路径 + 数据
        String file = url.getFile();
        System.out.println("资源路径+数据:" + file);
    }
}

在这里插入图片描述
网络爬虫

package com.powernode.javase.net;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

/**
 * ClassName: URLTest02
 * Description:
 * <p>
 * Datetime: 2024/2/1 15:53
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class URLTest02 {
    public static void main(String[] args) throws Exception{
        URL url = new URL("https://tianqi.qq.com/");
        InputStream inputStream = url.openStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));

        String s = null;
        while((s = br.readLine()) != null){
            System.out.println(s);
        }

        br.close();
    }
}

10.4 TCP与UDP协议

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.5 基于TCP协议的编程

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用Java中的Socket实现单向通信。基于TCP协议,属于TCP编程

package com.powernode.javase.net;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * ClassName: Server
 * Description:
 *      现在使用Java中的Socket实现单向通信。基于TCP协议。属于TCP编程。
 *      这个类充当的是服务器端。
 * <p>
 * Datetime: 2024/2/1 14:14
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Server {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket clientSocket = null;
        BufferedReader br = null;
        try {
            // 先启动服务器端,启动服务器端后,这个应用肯定要对应一个端口。
            // 创建服务器端套接字对象
            int port = 8888;
            serverSocket = new ServerSocket(port);

            System.out.println("服务器正在启动,请稍后....");
            System.out.println("服务器启动成功,端口号" + port + ",等待客户端的请求!");
            
            // 开始接收客户端的请求
            clientSocket = serverSocket.accept();

            // 后续代码怎么写一会再说!
            // 服务器端接收消息,所以服务器端应该获取输入流。
            br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

            // 开始读
            String s = null;
            while((s = br.readLine()) != null){
                System.out.println(s);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            // 关闭服务器端套接字
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
package com.powernode.javase.net;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

/**
 * ClassName: Client
 * Description:
 *      现在使用Java中的Socket实现单向通信。基于TCP协议。属于TCP编程。
 *      这个类充当的是客户端。
 * <p>
 * Datetime: 2024/2/1 14:14
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Client {
    public static void main(String[] args) {
        Socket clientSocket = null;
        BufferedWriter bw = null;
        try {
            // 创建客户端套接字对象
            // 需要指定服务器的IP地址,和端口号
            InetAddress ia = InetAddress.getLocalHost();
            int port = 8888;
            clientSocket = new Socket(ia, port);

            // 客户端给服务器端发送消息
            // 客户端你是输出流
            bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

            // 发送消息
            /*while(true){
                bw.write("你好,最近身体怎么样啊!");
                bw.write("\n");
                bw.write("你收到消息了吗?");
                // 刷新
                bw.flush();
                // 延迟效果
                Thread.sleep(1000);
            }*/

            Scanner scanner = new Scanner(System.in);
            while(true){
                System.out.print("请输入您要发送的消息:");
                // 从键盘上接收的消息
                String msg = scanner.next();
                // 把消息发送给服务器
                bw.write(msg);
                bw.write("\n");

                // 刷新
                bw.flush();
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

在这里插入图片描述
使用Java中的Socket实现双向通信。基于TCP协议,属于TCP编程

package com.powernode.javase.net;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * ClassName: TwoWayServer
 * Description:
 *          接收客户端发送过来的图片。
 *          回复消息给客户端。
 * <p>
 * Datetime: 2024/2/1 15:18
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class TwoWayServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket clientSocket = null;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        BufferedWriter bw = null;

        try {
            // 创建服务器套接字对象
            serverSocket = new ServerSocket(8888);
            System.out.println("服务器启动成功,正在接收客户端的请求!");

            // 开始接收客户端的请求
            clientSocket = serverSocket.accept();

            // 获取输入流
            bis = new BufferedInputStream(clientSocket.getInputStream());

            // 新建输出流
            bos = new BufferedOutputStream(new FileOutputStream("./dog.jpg"));

            // 开始读
            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = bis.read(bytes)) != -1){
                // 把客户端发送过来的图片,保存到本地服务器中。
                bos.write(bytes, 0, readCount);
            }

            // 刷新
            bos.flush();

            // 服务器接收完图片之后给客户端回个话
            bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

            bw.write("你发的图片我已经收到了,真可爱!\n");

            bw.flush();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
package com.powernode.javase.net;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
 * ClassName: TwoWayClient
 * Description:
 *          发送一个图片给服务器。
 *          接收服务器返回的消息。
 * <p>
 * Datetime: 2024/2/1 15:18
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class TwoWayClient {
    public static void main(String[] args) {
        Socket clientSocket = null;
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        BufferedReader br = null;
        try {
            // 创建客户端套接字对象
            clientSocket = new Socket(InetAddress.getLocalHost(), 8888);

            // 发送一张图片给服务器
            bos = new BufferedOutputStream(clientSocket.getOutputStream());

            // 开始写图片到服务器
            // 一边读一边写(读客户端本地硬盘上的图片)
            bis = new BufferedInputStream(new FileInputStream("c:/dog.jpg"));

            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = bis.read(bytes)) != -1){
                bos.write(bytes, 0, readCount);
            }

            // 刷新
            bos.flush();

            // 关闭输出(输出结束)
            clientSocket.shutdownOutput();

            // 接收服务器响应回来的消息
            br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            String s = null;
            while((s = br.readLine()) != null){
                System.out.println(s);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

10.6 基于UDP协议的编程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.powernode.javase.net;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * ClassName: Receive
 * Description:
 *          演示UDP编程,这个接收方。
 * <p>
 * Datetime: 2024/2/1 15:42
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Receive {
    public static void main(String[] args) throws Exception{

        DatagramSocket ds = new DatagramSocket(8888);

        byte[] bytes = new byte[64 * 1024];
        // 准备一个包,这个包接收发送方的信息。
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        // 程序执行到这里,停下来,等待发送方的发送。
        ds.receive(dp);

        // 程序执行到这里说明,已经完全将发送方发送的数据接收到了。
        // 从包中取出来数据。
        String msg = new String(bytes, 0, dp.getLength());
        System.out.println("接收到的发送方发过来的消息:" + msg);

        ds.close();
    }
}
package com.powernode.javase.net;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * ClassName: Send
 * Description:
 *      演示UDP编程,这个是发送方。
 * <p>
 * Datetime: 2024/2/1 15:42
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Send {
    public static void main(String[] args) throws Exception{
        DatagramSocket ds = new DatagramSocket();

        // 创建包
        byte[] bytes = "动力节点".getBytes();
        DatagramPacket dp = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getLocalHost(), 8888);

        // 发送消息
        ds.send(dp);

        ds.close();
    }
}

Java8新特性

接口的Java8和Java9的新特性

在这里插入图片描述

// 正确的函数式接口
@FunctionalInterface
public interface TestInterface {
 
    
    // 抽象方法
    public void sub();
 
    // java.lang.Object中的方法不是抽象方法
    public boolean equals(Object var1);
 
    // default不是抽象方法
    public default void defaultMethod(){
 
    }
 
    // static不是抽象方法
    public static void staticMethod(){
 
    }
}
package com.powernode.javase.oop29;

/**
 * 1. 接口(interface)在Java中表示一种规范或契约,它定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性。
 * 接口和类一样,也是一种引用数据类型
 * 2. 接口怎么定义?[修饰符列表] interface 接口名{}
 * 3. 抽象类是半抽象的,接口是完全抽象的。接口没有构造方法,也无法实例化
 * 4.(JDK8之前的语法规则) 接口中只能定义:常量+抽象方法。接口中的常量的static final可以省略。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的
 * 5.接口和接口之间可以多继承
 * 6.类和接口的关系我们叫做实现(这里的实现也可以等同看做继承)。使用implements关键字进行接口的实现。
 * 7.一个非抽象的类实现接口必须将接口中所有的抽象方法全部实现(强制要求的,必须的,要不然编译器报错。)
 * 8.一个类可以实现多个接口。语法是:class 类 implements 接口A,接口B{}
 * 9.Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)
 *      默认方法:引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。
 *              所有实现接口的类都必须实现这些抽象方法。这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时,
 *              这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。这就是所谓的"接口演变"问题
 *      静态方法:注意:java中规定,在JDK8之后,接口中可以一定静态方法,但是这个静态方法,只能通过“该接口名”去调用的。别的都无法调用。
 *              在JDK8之后引入接口可以定义静态方法,实际上想表达一个意思:接口也可以作为工具来使用了。
 * 10.JDK9之后允许接口中定义私有的实例方法(为默认方法服务的)和私有的静态方法(为静态方法服务的)
 * 11.所有的接口隐式的继承Object。因此接口也可以调用Object类的相关方法
 */
public interface MyInterface {

    //public MyInterface(){}

    public static final int num1 = 1;
    int num2 = 2;

    public abstract void m1();

    void m2();

    //void m3(){}

    // JDK9之后允许定义私有的实例方法。(给默认方法服务的。)
    private void privateMethod(){
        System.out.println("privateMethod执行了。");
    }

    // 默认方法
    default void defaultMethod(){
        System.out.println("接口中的默认方法defaultMethod执行了。");
        privateMethod();
    }

    // JDK9之后允许定义私有的静态方法。(给静态方法服务的)
    private static void privateStaticMethod(){
        System.out.println("privateStaticMethod执行了");
    }

    // 静态方法
    static void staticMethod(){
        System.out.println("接口的静态方法执行了");
        privateStaticMethod();
    }
}

interface A {
    void a();
}

interface B {
    void b();
}

interface C extends A,B{
    void c();
}

class MyInterfaceImpl implements MyInterface, C {

    /*@Override
    public void defaultMethod() {
        System.out.println("MyInterfaceImpl的默认方法执行了。");
    }*/

    @Override
    public void m1() {
        System.out.println("m1执行了");
    }

    @Override
    public void m2() {
        System.out.println("m2执行了");
    }

    @Override
    public void a() {
        System.out.println("a执行了");
    }

    @Override
    public void b() {
        System.out.println("b执行了");
    }

    @Override
    public void c() {
        System.out.println("c执行了");
    }
}

Java8的新日期API

在这里插入图片描述
时间戳:自1970年1月1日0时0分0秒到系统当前时间的总毫秒数

在这里插入图片描述

import java.time.LocalDateTime;

/**
 * java.time.LocalDateTime 日期时间
 */
public class DateTest01 {

    public DateTest01 m1(){
        System.out.println("m1...");
        return this;
    }

    public DateTest01 m2(){
        System.out.println("m2...");
        return this;
    }

    public DateTest01 m3(){
        System.out.println("m3...");
        return this;
    }

    public static void main(String[] args) {

        DateTest01 dateTest01 = new DateTest01();
        dateTest01.m1().m2().m3();

        // 获取系统当前时间,精确到纳秒级
        LocalDateTime now = LocalDateTime.now();
        System.out.println("系统当前时间:" + now);

        // 获取指定的日期时间
        LocalDateTime localDateTime = LocalDateTime.of(2008, 8, 8, 8, 8, 8, 8);
        System.out.println(localDateTime);

        // 加日期时间
        /*LocalDateTime localDateTime1 = localDateTime.plusYears(1);
        System.out.println(localDateTime1);
        LocalDateTime localDateTime2 = localDateTime1.plusSeconds(1);
        System.out.println(localDateTime2);*/

        // 对象的链式调用。
        LocalDateTime localDateTime1 = localDateTime.plusYears(1).plusSeconds(1);
        System.out.println(localDateTime1);

        // 减日期时间
        LocalDateTime localDateTime2 = localDateTime1.minusYears(2).minusMonths(1);
        System.out.println(localDateTime2);
    }
}

在这里插入图片描述

import java.time.Instant;

/**
 * 获取时间戳:自1970-1-1到当前系统时间的总毫秒数。
 */
public class DateTest02 {
    public static void main(String[] args) {
        long l = System.currentTimeMillis();
        System.out.println("时间戳:" + l);

        // Java8的API也可以获取时间戳。
        Instant now = Instant.now(); // 系统当前时间,基于UTC(全球标准时间。)
        //System.out.println(now);

        long epochMilli = now.toEpochMilli();
        System.out.println("时间戳:" + epochMilli);
    }
}

在这里插入图片描述

import java.time.Duration;
import java.time.LocalDateTime;

/**
 * 计算两个时间的差。java.time.Duration
 */
public class DateTest03 {
    public static void main(String[] args) {
        // 获取时间1
        LocalDateTime time1 = LocalDateTime.of(2008, 7, 8, 8, 8, 8);
        // 获取时间2
        LocalDateTime time2 = LocalDateTime.of(2008, 8, 8, 8, 8, 8);
        // 获取两个时间的差
        Duration between = Duration.between(time1, time2);
        // 看看差多少天
        System.out.println("相差天数:" + between.toDays());
        // 看看差多少个小时
        System.out.println("相差小时:" + between.toHours());
    }
}

在这里插入图片描述

import java.time.LocalDate;
import java.time.Period;

/**
 * 计算两个日期的差。java.time.Period
 */
public class DateTest04 {
    public static void main(String[] args) {
        // 获取日期1
        //LocalDate date1 = LocalDate.of(2007,7,7);
        LocalDate date1 = LocalDate.of(2007,7,15);
        // 获取日期2
        LocalDate date2 = LocalDate.of(2008,8,8);
        // 计算两个日期差
        Period between = Period.between(date1, date2);
        // 相差的年
        System.out.println(between.getYears());
        // 相差的月
        System.out.println(between.getMonths());
        // 相差的日
        System.out.println(between.getDays());
    }
}

在这里插入图片描述

import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;

/**
 * 时间矫正器
 */
public class DateTest05 {
    public static void main(String[] args) {
        // 获取系统当前时间
        LocalDateTime now = LocalDateTime.now();
        // 矫正时间
        LocalDateTime localDateTime1 = now.with(TemporalAdjusters.lastDayOfYear());
        System.out.println(localDateTime1);

        LocalDateTime localDateTime2 = now.with(TemporalAdjusters.firstDayOfMonth());
        System.out.println(localDateTime2);

        // 下周一
        LocalDateTime localDateTime3 = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
        System.out.println(localDateTime3);
    }
}

在这里插入图片描述

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 日期格式化
 */
public class DateTest06 {
    public static void main(String[] args) {

        // LocalDateTime -> String
        // 获取一个日期时间
        LocalDateTime now = LocalDateTime.now();
        // 创建格式化对象
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // 格式化
        String s = dateTimeFormatter.format(now);
        System.out.println(s);

        // String -> LocalDateTime
        LocalDateTime localDateTime = LocalDateTime.parse("2008-08-08 08:08:08", dateTimeFormatter);
        System.out.println(localDateTime);
    }
}

11 Lambda表达式

11.1 主要内容

  1. Lambda表达式的概述
  2. Lambda表达式的使用
  3. Lambda表达式的方法引用
  4. Lambda表达式的在集合中的使用

11.2 学习目标

知识点要求
Lambda表达式的概述理解
Lambda表达式的使用了解
Lambda表达式的方法引用掌握
Lambda表达式的在集合中的使用理解

11.3 Lambda表达式的概述

11.3.1 Lambda表达式的引入

Lambda表达式是JDK1.8的一个新特性,可以取代大部分的匿名内部类,以便写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
在以前的学习中,想要实现对List集合的“降序”排序操作,就需要使用匿名内部类来实现,这样的代码非常的复杂和繁琐,代码如下:

// 方式一:使用匿名内部类来实现
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});
System.out.println("排序后:" + list);

针对以上对List集合的的“降序”排序操作,除了使用匿名内部类来实现外,还可以使用Lambda表达式来实现,使用Lambda表达式的代码非常优雅,并且还非常的简洁,代码如下:

// 方式二:使用Lambda表达式来实现
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, (o1, o2) -> o2 - o1);
System.out.println("排序后:" + list);
package com.powernode.javase.lambda;

import java.util.Comparator;
import java.util.TreeSet;

/**
 * ClassName: LambdaTest01
 * Description: 先体会一下Java8的新特性:Lambda表达式
 * <p>
 * Datetime: 2024/2/2 8:38
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest01 {
    public static void main(String[] args) {
        // TreeSet集合中的元素是可以自动排序的。
        // TreeSet集合是怎么排序的? 两种方式
        // 第一种方式:如果比较规则固定不变,可以让TreeSet集合中的元素实现java.lang.Comparable接口。
        // 第二种方式:创建TreeSet集合的时候,给TreeSet集合传递一个比较器对象,比较器实现java.util.Comparator接口。
        // 以下是匿名内部类的方式
        /*TreeSet<User> users = new TreeSet<>(new Comparator<User>() {
            @Override
            public int compare(User o1, User o2) {
                return o1.getAge() - o2.getAge();
            }
        });*/

        //TreeSet<User> users1 = new TreeSet<>((User o1, User o2) -> { return o2.getAge() - o1.getAge(); });

        TreeSet<User> users1 = new TreeSet<>((o1, o2) ->  o2.getAge() - o1.getAge() );

        User user1 = new User(20);
        User user2 = new User(30);
        User user3 = new User(40);
        User user4 = new User(10);

        users1.add(user1);
        users1.add(user2);
        users1.add(user3);
        users1.add(user4);

        System.out.println(users1);
    }
}

class TT {
    public void doSome(){}
}

interface  A{

}
class B implements A{

}

class User {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                '}';
    }
}

11.3.2 函数式编程思想的概述

Java从诞生之日起就一直倡导“一切皆对象”,在Java语言中面向对象(OOP)编程就是一切,但是随着Python和Scala等语言的崛起和新技术的挑战,Java也不得不做出调整以便支持更加广泛的技术要求,即Java语言不但支持OOP还支持OOF(面向函数编程)。
JDK1.8引入Lambda表达式之后,Java语言也开始支持函数式编程,但是Lambda表达式不是Java语言最早使用的,目前C++、C#、Python、Scala等语言都支持Lambda表示。

  • 面向对象的思想
    • 做一件事情,找一个能解决这个事情的对象,然后调用对象的方法,最终完成事情。
  • 函数式编程思想
    • 只要能获得结果,谁去做的,怎么做的都不重要,重视的是结果,不重视实现过程。

在函数式编程语言中,函数被当成一等公民对待。在将函数当成一等公民的编程语言中,Lambda表达式的类型是函数,但是Lambda表达式却是一个对象,而不是函数,它们必须依附于一类特别的对象类型,也就是所谓的函数式接口。
简单点说,JDK1.8中的Lambda表达式就是一个函数式接口的实例,这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以使用Lambda表达式来表示

11.3.3 如何去理解函数式接口

能够使用Lambda表达式的一个重要依据是必须有相应的函数式接口,所谓的函数式接口,指的就是“一个接口中有且只能有一个抽象方法”。也就是说,如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口。
如果我们在接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,也就是该接口中有且只能定义一个抽象方法,如果该接口中定义了多个或0个抽象方法,则程序编译时就会报错。
【示例】定义一个函数式接口

@FunctionalInterface
public interface Flyable {
    // 在函数式接口中,我们有且只能定义一个抽象方法
    void showFly();
    // 但是,可以定义任意多个默认方法或静态方法
    default void show() {
        System.out.println("JDK1.8之后,接口还可以定义默认方法和静态方法");
    }
}

另外,从某种意义上来说,只要你保证你的接口中有且只有一个抽象方法,则接口中没有使用 @FunctionalInterface 注解来标注,那么该接口也依旧属于函数式接口。
在以下代码中,Flyable接口中没有使用@FunctionalInterface 注解,但是Flyable接口中只存在一个抽象方法,因此Flyable接口依旧属于函数式接口,那么使用Lambda表达式就可以表示Flyable 接口的实例,代码如下:

/**
 * 没有使用@FunctionalInterface标注的接口
 */
public interface Flyable {
    void showFly();
}
/**
 * 测试类
 */
public class Test01 {
    public static void main(String[] args) {
        // 使用lambda表示来表示Flyable接口的实例
        Flyable flyable = () -> {
            System.out.println("小鸟自由自在的飞翔");
        };
        // 调用Flyable接口的实例的showFly()方法
        flyable.showFly();
    }
}

11.3.4 Lambda和匿名内部类

  • 所需类型不同
    • 匿名内部类:可以是接口,抽象类,具体类。
    • Lambda表达式:只能是接口。
  • 使用限制不同
    • 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。
    • 如果接口中有多个抽象方法,则就只能使用匿名内部类,而不能使用Lambda表达式。
  • 实现原理不同
    • 匿名内部类:编译之后,会生成一个单独的.class字节码文件。
    • Lambda表达式:编译之后,没有生成一个单独的.class字节码文件。
package com.powernode.javase.lambda;

/**
 * ClassName: LambdaTest02
 * Description:
 *      Lambda表达式和匿名内部类的区别:
 *          所需类型不同:
 *              匿名内部类,可以是抽象类,也可以是接口。
 *              Lambda表达式,只能是接口。
 *          使用限制不同:
 *              Lambda表达式使用的接口中要求有且只有一个抽象方法。
 *              匿名内部类方式使用的接口可以有多个抽象方法。
 *          实现原理不同:
 *              采用匿名内部类的话,编译之后会生成一个.class文件。
 *              采用Lambda表达式的话,编译之后不会生成单独的.class文件。
 * <p>
 * Datetime: 2024/2/2 9:10
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest02 {

    public static void main(String[] args) {

        // 匿名内部类方式(匿名内部类可以是一个抽象类)
        LambdaTest02.test(new Animal() {
            @Override
            public void run() {
                System.out.println("Animal run....");
            }
        });

        // 尝试将上面的代码修改为Lambda表达式方式
        // 编译报错,原因是:只有接口才可以使用Lambda表达式
        //LambdaTest02.test(() -> { System.out.println("Animal run...."); });

        // 匿名内部类
        /*LambdaTest02.doFly(new Flyable() {
            @Override
            public void run() {
                System.out.println("run.....");
            }

            @Override
            public void fly() {
                System.out.println("fly.....");
            }
        });*/

        // 尝试使用Lambda表达式
        // Lambda表达式使用的接口必须是函数式接口。(必须是接口,而且接口中有且只有一个抽象方法。)
        //LambdaTest02.doFly(() -> { System.out.println("run....."); });


    }

    public static void test(Animal a){
        a.run();
    }

    public static void doFly(Flyable f){
        f.fly();
        f.run();
    }
}

abstract class Animal{
    public abstract void run();
}

interface Flyable {
    void run();
    void fly();
}

11.4 Lambda表达式的使用

11.4.1 Lambda表达式的语法

Lambda表达式本质就是一个匿名函数,在函数的语法中包含返回值类型、方法名、形参列表和方法体等,而在Lambda表达式中我们只需要关心形参列表和方法体即可。
在Java语言中,Lambda表达式的语法为“(形参列表) -> {方法体}”,其中“->”为 lambda操作符或箭头操作符,“形参列表”为对应接口实现类中重写方法的形参列表,“方法体”为对应接口实现类中重写方法的方法体。
接下来,我们就以匿名内部类为例,从而将匿名内部类演化为Lambda表达式,代码如下:

List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});
System.out.println("排序后:" + list);

在以上的匿名内部类中,黄色背景颜色标注的代码都属于不可变的固定代码,而红色背景颜色标注的代码,属于可变的并且是完成该功能的核心代码。因此,将此处的匿名内部类转化为Lambda表达式,我们只需保留红色部分的形参列表和方法体即可,对应的Lambda表达式代码实现如下:

List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, (Integer o1, Integer o2) -> {
    return o2 - o1;
});
System.out.println("排序后:" + list);

在以上代码中,黄色背景颜色标注的就是重写于Comparator接口中抽象方法的形参列表,而红色背景颜色标注的就是重写方法对应方法体的代码实现。因此Lambda本质上就是去掉了一堆没有意义的代码,只留下核心的代码逻辑,从而让代码看起来更加的简洁且优雅。


package com.powernode.javase.lambda;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * ClassName: LambdaTest03
 * Description:
 *          Lambda表达式的语法格式:
 *              (形式参数列表) -> {
 *                  方法体;
 *              }
 * <p>
 * Datetime: 2024/2/2 9:51
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest03 {
    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(100, 200, 300, 250);

        // 对List集合中的元素排序
        //Collections.sort(list);

        // 为了讲解这个Lambda表达式的基础语法,刻意绕弯了。
        // 这是匿名内部类的方式
        /*Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });*/

        // 改成Lambda表达式的形式
        //Collections.sort(list, (Integer a, Integer b) -> { return b - a; });

        Comparator<Integer> comparator = (Integer a, Integer b) -> { return a - b; };
        Collections.sort(list, comparator);

        // 输出排序后的
        System.out.println(list);
    }
}

11.4.2 Lambda表达式的使用

11.4.2.1 Lambda表达式的基本使用

接下来,我们以自定义的函数式接口为例,先从匿名对象的实现过程,慢慢演变为Lambda表达式的实现过程。另外,使用Lambda表达式的时候,则必须有上下文环境,才能推导出Lambda对应的接口类型。

11.4.2.1.1 无返回值函数式接口

情况一:无返回值无参数

// 情况一:无返回值无参数
interface NoParameterNoReturn {
    void test();
}

public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        NoParameterNoReturn obj1 = new NoParameterNoReturn() {
            @Override
            public void test() {
                System.out.println("无参无返回值");
            }
        };
        obj1.test();

        // 方式二:使用Lambda表达式来实现
        NoParameterNoReturn obj2 = () -> {
            System.out.println("无参无返回值");
        };
        obj2.test();
    }
}

package com.powernode.javase.lambda;

/**
 * ClassName: LambdaTest04
 * Description:
 *          Lambda表达式的使用:关于无返回值无参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:00
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest04 {
    public static void main(String[] args) {
        // 匿名内部类方式
        NoParameterNoReturn npnr = new NoParameterNoReturn() {
            @Override
            public void test() {
                System.out.println("无返回值无参数的test方法执行了。");
            }
        };
        npnr.test();

        // 改成Lambda表达式
        NoParameterNoReturn npnr2 = () -> { System.out.println("无返回值无参数的test方法执行了。"); };
        npnr2.test();

        // 精简
        NoParameterNoReturn npnr3 = () -> System.out.println("无返回值无参数的test方法执行了。");
        npnr3.test();
    }
}

@FunctionalInterface
interface NoParameterNoReturn {
    void test();
}

情况二:无返回值一个参数

// 情况二:无返回值一个参数
interface OneParameterNoReturn {
    void test(int num);
}

public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        OneParameterNoReturn obj1 = new OneParameterNoReturn() {
            @Override
            public void test(int num) {
                System.out.println("无返回值一个参数 --> " + num);
            }
        };
        obj1.test(10);

        // 方式二:使用Lambda表达式来实现
        OneParameterNoReturn obj2 = (int num) -> {
            System.out.println("无返回值一个参数 --> " + num);
        };
        obj2.test(20);
    }
}

/**
 * ClassName: LambdaTest05
 * Description:
 *          Lambda表达式的使用:关于无返回值一个参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:04
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest05 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        OneParameterNoReturn opnr = new OneParameterNoReturn() {
            @Override
            public void test(Integer value) {
                System.out.println("Integer-->" + value);
            }
        };
        opnr.test(1000);

        // Lambda表达式方式
        OneParameterNoReturn opnr2 = (Integer value) -> { System.out.println("Integer-->" + value); };
        opnr2.test(2000);

        // 精简
        OneParameterNoReturn opnr3 = value -> System.out.println("Integer-->" + value);
        opnr3.test(2000);
    }
}

@FunctionalInterface
interface OneParameterNoReturn {
    void test(Integer value);
}

情况三:无返回值多个参数

// 情况三:无返回值多个参数
interface MoreParameterNoReturn {
    void test(String str1, String str2);
}
public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        MoreParameterNoReturn obj1 = new MoreParameterNoReturn() {
            @Override
            public void test(String str1, String str2) {
                System.out.println(str1 + " : " + str2);
            }
        };
        obj1.test("hello", "world");

        // 方式二:使用Lambda表达式来实现
        MoreParameterNoReturn obj2 = (String str1, String str2) -> {
            System.out.println(str1 + " : " + str2);
        };
        obj2.test("你好", "世界");
    }
}

/**
 * ClassName: LambdaTest06
 * Description:
 *          Lambda表达式的使用:关于无返回值多个参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:06
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest06 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        MoreParameterNoReturn mpnr = new MoreParameterNoReturn() {
            @Override
            public void test(Integer value1, Integer value2) {
                System.out.println(value1 + value2);
            }
        };
        mpnr.test(100, 200);

        // Lambda表达式方式
        MoreParameterNoReturn mpnr2 = (Integer value1, Integer value2) -> { System.out.println(value1 + value2); };
        mpnr2.test(300, 400);

        // 精简
        MoreParameterNoReturn mpnr3 = (value1, value2) -> System.out.println(value1 + value2);
        mpnr3.test(300, 400);
    }
}

@FunctionalInterface
interface MoreParameterNoReturn {
    void test(Integer value1, Integer value2);
}
11.4.2.1.2 有返回值函数接口

情况一:有返回值无参数

// 情况一:有返回值无参数
interface NoParameterHasReturn {
    int test();
}

public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        NoParameterHasReturn obj1 = new NoParameterHasReturn() {
            @Override
            public int test() {
                return 520;
            }
        };
        System.out.println(obj1.test()); // 输出:520

        // 方式二:使用Lambda表达式来实现
        NoParameterHasReturn obj2 = () -> {
            return 1314;
        };
        System.out.println(obj2.test()); // 输出:1314
    }
}

/**
 * ClassName: LambdaTest07
 * Description:
 *          Lambda表达式的使用:关于有返回值无参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:09
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest07 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        NoParameterHasReturn nphr = new NoParameterHasReturn() {
            @Override
            public Integer test() {
                return 300;
            }
        };
        System.out.println(nphr.test());

        // Lambda表达式的方式
        NoParameterHasReturn nphr2 = () -> { return 500; };
        System.out.println(nphr2.test());

        // 精简
        NoParameterHasReturn nphr3 = () -> 500;
        System.out.println(nphr3.test());
    }
}

@FunctionalInterface
interface NoParameterHasReturn {
    Integer test();
}

情况二:有返回值一个参数

// 情况二:有返回值一个参数
interface OneParameterHasReturn {
    String test(double num);
}

public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        OneParameterHasReturn obj1 = new OneParameterHasReturn() {
            @Override
            public String test(double num) {
                return "传入的小数为:" + num;
            }
        };
        System.out.println(obj1.test(520.0));

        // 方式二:使用Lambda表达式来实现
        OneParameterHasReturn obj2 = (double num) -> {
            return "传入的小数为:" + num;
        };
        System.out.println(obj2.test(1314.0));
    }
}

/**
 * ClassName: LambdaTest08
 * Description:
 *          Lambda表达式的使用:关于有返回值一个参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:12
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest08 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        OneParameterHasReturn ophr = new OneParameterHasReturn() {
            @Override
            public Integer test(Integer value) {
                return value * 2;
            }
        };
        System.out.println(ophr.test(100));

        // Lambda表达式的方式
        OneParameterHasReturn ophr2 = (Integer value) -> { return value * 2; };
        System.out.println(ophr2.test(200));

        // 精简
        OneParameterHasReturn ophr3 = value -> value * 2;
        System.out.println(ophr3.test(200));
    }
}

@FunctionalInterface
interface OneParameterHasReturn {
    Integer test(Integer value);
}

情况三:有返回值多个参数

// 情况三:有返回值多个参数
interface MoreParameterHasReturn {
    String test(int num1, int num2);
}
public class Test01 {
    public static void main(String[] args) {
        // 方式一:使用匿名内部类来实现
        MoreParameterHasReturn obj1 = new MoreParameterHasReturn() {
            @Override
            public String test(int num1, int num2) {
                return "运算的结果为:" + (num1 + num2);
            }
        };
        System.out.println(obj1.test(10, 20));

        // 方式二:使用Lambda表达式来实现
        MoreParameterHasReturn obj2 = (int num1, int num2) -> {
            return "运算的结果为:" + (num1 + num2);
        };
        System.out.println(obj2.test(20, 30));
    }
}

/**
 * ClassName: LambdaTest09
 * Description:
 *          Lambda表达式的使用:关于有返回值多个参数的函数式接口。
 * <p>
 * Datetime: 2024/2/2 10:15
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest09 {
    public static void main(String[] args) {
        // 匿名内部类方式
        MoreParameterHasReturn mphr = new MoreParameterHasReturn() {
            @Override
            public Integer test(Integer value1, Integer value2) {
                return value1 + value2;
            }
        };
        System.out.println(mphr.test(1, 2));

        // Lambda表达式的方式
        MoreParameterHasReturn mphr2 = (Integer value1, Integer value2) -> { return value1 + value2; };
        System.out.println(mphr2.test(3, 4));

        // 精简
        MoreParameterHasReturn mphr3 = (a, b) -> a + b;
        System.out.println(mphr3.test(3, 4));

    }
}

@FunctionalInterface
interface MoreParameterHasReturn {
    Integer test(Integer value1, Integer value2);
}
11.4.2.2 Lambda表达式的语法精简

在以上代码中,虽然Lambda表达式的语法已经很简洁了,但是Lambda表达式的语法格式还可以更加的精简,从而写出更加优雅的代码,但是相应的代码可读性也会变差。
在以下的应用场景中,我们就可以对Lambda表达式的语法进行精简,场景如下:

  1. 形参类型可以省略,如果需要省略,则每个形参的类型都要省略。
  2. 如果形参列表中只存在一个形参,那么形参类型和小括号都可以省略。
  3. 如果方法体当中只有一行语句,那么方法体的大括号也可以省略。
  4. 如果方法体中只有一条return语句,那么大括号可以省略,且必须去掉return关键字。

接下来,我们就对以下的Lambda表达式代码进行精简,从而写出更加优雅的代码。

public class Test01 {
    public static void main(String[] args) {
        // (1)形参类型可以省略,如果需要省略,每个形参的类型都要省略。
        // 没有精简的Lambda表达式代码
        MoreParameterNoReturn obj1 = (String str1, String str2) -> {
            System.out.println(str1 + " : " + str2);
        };
        obj1.test("hello", "world");
        // 精简之后的Lambda表达式代码
        MoreParameterNoReturn obj2 = (str1, str2) -> {
            System.out.println(str1 + " : " + str2);
        };
        obj2.test("你好", "世界");

        // (2)如果形参列表中只有一个形参,那么形参类型和小括号都可以省略。
        // 没有精简的Lambda表达式代码
        OneParameterHasReturn obj3 = (double num) -> {
            return "传入的小数为:" + num;
        };
        System.out.println(obj3.test(520.0));
        // 精简之后的Lambda表达式代码
        OneParameterHasReturn obj4 = num -> {
            return "传入的小数为:" + num;
        };
        System.out.println(obj4.test(1314.0));

        // (3)如果方法体当中只有一行代码,那么方法体的大括号也可以省略。
        // 没有精简的Lambda表达式代码
        NoParameterNoReturn obj5 = () -> {
            System.out.println("无参无返回值");
        };
        obj5.test();
        // 精简之后的Lambda表达式代码
        NoParameterNoReturn obj6 = () -> System.out.println("无参无返回值");
        obj6.test();

        // (4)方法体中只有一条return语句,则大括号可以省略,且必须去掉return关键字
        // 没有精简的Lambda表达式代码
        MoreParameterHasReturn obj7 = (int a, int b) -> {
            return "运算的结果为:" + (a + b);
        };
        System.out.println(obj7.test(10, 20));
        // 精简之后的Lambda表达式代码
        MoreParameterHasReturn obj8 = (a, b) -> "运算的结果为:" + (a + b);
        System.out.println(obj8.test(20, 30));
    }
}

11.4.3 四个基本的函数式接口

名字接口名对应的抽象方法
消费Consumervoid accept(T t);
生产SupplierT get();
转换Function<T, R>R apply(T t);
判断Predicateboolean test(T t);

以上的函数式接口都在java.util.function包中,通常函数接口出现的地方都可以使用Lambda表达式,所以不必记忆函数接口的名字,这些函数式接口及子接口在后续学习中很常用。

11.5 Lambda表达式的方法引用

11.5.1 方法引用的概述

我们在使用Lambda表达式的时候,如果Lambda表达式的方法体中除了调用现有方法之外什么都不做,满足这样的条件就有机会使用方法引用来实现。
在以下的代码中,在重写的apply()方法中仅仅只调用了现有Math类round()方法,也就意味着Lambda表达式中仅仅只调用了现有Math类round()方法,那么该Lambda表达式就可以升级为方法引用,案例如下:

// 需求:实现小数取整的操作
// 方式一:使用匿名对象来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
    @Override
    public Long apply(Double aDouble) {
        return Math.round(aDouble);
    }
};
System.out.println(function1.apply(3.14));

// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));

// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));

对于方法引用,我们可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
在Lambda表达式的方法引用中,主要有实例方法引用、静态方法引用、特殊方法引用和构造方法引用、数组引用这五种情况,接下来我们就对这五种情况进行讲解。

11.5.2 实例方法引用

语法:对象 :: 实例方法
特点:在Lambda表达式的方法体中,通过“对象”来调用指定的某个“实例方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过对象调用某个实例方法的返回值类型和形参列表 保持一致。
【示例】实例化Consumer接口的实现类对象,并在重写的accept()方法中输出形参的值

// 方式一:使用匿名内部类来实现
Consumer<String> consumer1 = new Consumer<String>() {
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
};
consumer1.accept("hello world");

// 方式二:使用Lambda表达式来实现
Consumer<String> consumer2 = str -> System.out.println(str);
consumer2.accept("hello world");

// 方式三:使用方法引用来实现
Consumer<String> consumer3 = System.out :: println;
consumer3.accept("hello world");

【示例】实例化Supplier接口的实现类对象,并在重写方法中返回Teacher对象的姓名

Teacher teacher = new Teacher("ande", 18);
// 方式一:使用匿名内部类来实现
Supplier<String> supplier1 = new Supplier<String>() {
    @Override
    public String get() {
        return teacher.getName();
    }
};
System.out.println(supplier1.get());

// 方式二:使用Lambda表达式来实现
Supplier<String> supplier2 = () -> teacher.getName();
System.out.println(supplier2.get());

// 方式三:使用方法引用来实现
Supplier<String> supplier3 = teacher :: getName;
System.out.println(supplier3.get());

import java.util.function.Supplier;

/**
 * ClassName: LambdaTest10
 * Description: 实例方法引用
 *          语法格式:
 *              对象::实例方法名
 *
 *          满足什么条件的时候可以使用实例方法引用?
 *              函数式接口中的   返回值类型   和    形参
 *              与
 *              内部调用的方法的 返回值类型   和    形参
 *              一致。
 * <p>
 * Datetime: 2024/2/2 11:28
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest10 {
    public static void main(String[] args) {
        // 使用生产型接口:Supplier
        // 匿名内部类的方式
        Teacher teacher = new Teacher("老杜");
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return teacher.getName();
            }
        };
        System.out.println(supplier.get());

        // 以上是否符合“实例方法引用”的条件?
        // 先修改为Lambda表达式
        Supplier<String> supplier1 = () -> teacher.getName();
        System.out.println(supplier1.get());

        // 使用“实例方法引用”精简
        Supplier<String> supplier2 = teacher::getName;
        System.out.println(supplier2.get());

    }
}

class Teacher {
    private String name;

    public Teacher(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                '}';
    }
}

import java.util.function.Consumer;

/**
 * ClassName: LambdaTest11
 * Description: 实例方法引用
 * <p>
 * Datetime: 2024/2/2 11:36
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest11 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        // 使用消费型的函数式接口
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        consumer.accept("动力节点");

        // 修改为Lambda表达式
        Consumer<String> consumer1 = s -> System.out.println(s);
        consumer1.accept("动力节点");

        // 使用 实例方法引用 精简
        Consumer<String> consumer2 = System.out::println;
        consumer2.accept("动力节点");
    }
}

11.5.3静态方法引用

语法:类 :: 静态方法
特点:在Lambda表达式的方法体中,通过“类名”来调用指定的某个“静态方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过类名调用某个静态方法的返回值类型和形参列表保持一致。

【示例】实例化Function接口的实现类对象,并在重写的方法中返回小数取整的结果

// 方式一:使用匿名内部类来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
    @Override
    public Long apply(Double aDouble) {
        return Math.round(aDouble);
    }
};
System.out.println(function1.apply(3.14));

// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));

// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));

import java.util.function.Function;

/**
 * ClassName: LambdaTest12
 * Description: 静态方法引用
 *
 *          语法格式:
 *              类名::静态方法名
 *
 *          条件:
 *              函数式接口中的方法的     返回值类型     和    形参
 *              与
 *              内部调用静态方法的      返回值类型      和    形参
 *              一致。
 * <p>
 * Datetime: 2024/2/2 11:41
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest12 {
    public static void main(String[] args) {
        // 匿名内部类方式
        // 使用转换型函数式接口
        Function<Double, Long> function = new Function<Double, Long>() {
            @Override
            public Long apply(Double value) {
                return Math.round(value);
            }
        };
        System.out.println(function.apply(3.14));

        // Lambda表达式
        Function<Double, Long> function2 = value -> Math.round(value);
        System.out.println(function2.apply(5.67));

        // 静态方法引用改进
        Function<Double, Long> function3 = Math::round;
        System.out.println(function3.apply(5.67));
    }
}

11.5.4 特殊方法引用

语法:类名 :: 实例方法
特点:在Lambda表达式的方法体中,通过方法的第一个形参来调用指定的某个“实例方法”。
要求:把函数式接口中抽象方法的第一个形参作为方法的调用者对象,并且从第二个形参开始(或无参)可以对应到被调用实例方法的参数列表中,并且返回值类型保持一致。
【示例】使用Comparator比较器,来判断两个小数的大小

// 方式一:使用匿名内部类来实现
Comparator<Double> comparator1 = new Comparator<Double>() {
    @Override
    public int compare(Double o1, Double o2) {
        return o1.compareTo(o2);
    }
};
System.out.println(comparator1.compare(10.0, 20.0));

// 方式二:使用Lambda表达式来实现
Comparator<Double> comparator2 = (o1, o2) -> o1.compareTo(o2);
System.out.println(comparator2.compare(10.0, 20.0));

// 方式三:使用方法引用来实现
Comparator<Double> comparator3 = Double :: compareTo;
System.out.println(comparator3.compare(10.0, 20.0));

需求:实例化Function接口的实现类对象,然后获得传入Teacher对象的姓名。

// 方式一:使用匿名内部类来实现
Teacher teacher = new Teacher("ande", 18);
Function<Teacher, String> function1 = new Function<Teacher, String>() {
    @Override
    public String apply(Teacher teacher) {
        return teacher.getName();
    }
};
System.out.println(function1.apply(teacher));

// 方式二:使用Lambda表达式来实现
Function<Teacher, String> function2 = e -> e.getName();
System.out.println(function2.apply(teacher));

// 方式三:使用方法引用来实现
Function<Teacher, String > function3 = Teacher :: getName;
System.out.println(function3.apply(teacher));

import java.util.Comparator;

/**
 * ClassName: LambdaTest13
 * Description: 特殊方法引用
 *          语法格式:
 *              类名::实例方法
 *
 *          条件:
 *              1. 函数式接口中抽象方法的第一个参数作为内部方法调用对象。
 *              2. 从函数式接口的抽象方法的第二参数开始 与 内部调用方法时的参数类型  一致。
 *              3. 函数式接口中的抽象方法返回值类型  与  内部方法返回值类型  一致。
 * <p>
 * Datetime: 2024/2/2 11:52
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest13 {
    public static void main(String[] args) {
        // 匿名内部类
        Comparator<Double> comparator = new Comparator<Double>() {
            @Override
            public int compare(Double o1, Double o2) {
                return o1.compareTo(o2);
            }
        };
        System.out.println(comparator.compare(3.14, 5.6));

        // Lambda表达式
        Comparator<Double> comparator2 = (o1, o2) -> o1.compareTo(o2);
        System.out.println(comparator2.compare(3.14, 5.6));

        // 特殊方法引用
        Comparator<Double> comparator3 = Double::compareTo;
        System.out.println(comparator3.compare(3.14, 5.6));

    }
}

import java.util.function.Function;

/**
 * ClassName: LambdaTest14
 * Description: 特殊方法引用
 *          语法格式:
 *              类名::实例方法名
 * <p>
 * Datetime: 2024/2/2 14:02
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest14 {
    public static void main(String[] args) {
        // 转换型的函数式接口
        // 匿名内部类
        Function<Vip, String> function = new Function<Vip, String>() {
            @Override
            public String apply(Vip vip) {
                return vip.getName();
            }
        };

        Vip vip = new Vip("老杜");
        System.out.println(function.apply(vip));

        // Lambda表达式
        //Function<Vip, String> function2 = (Vip v) -> {return v.getName();};
        Function<Vip, String> function2 = v -> v.getName();
        System.out.println(function2.apply(vip));

        // 使用“特殊方法引用”来进行精简
        Function<Vip, String> function3 = Vip::getName;
        System.out.println(function3.apply(vip));
    }
}

class Vip {
    private String name;

    @Override
    public String toString() {
        return "Vip{" +
                "name='" + name + '\'' +
                '}';
    }

    public Vip(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

11.5.5 构造方法引用

语法:类名 :: new
特点:在Lambda表达式的方法体中,返回指定“类名”来创建出来的对象。
要求:创建对象所调用构造方法形参列表 和 函数式接口中的方法的形参列表 保持一致,并且方法的返回值类型和创建对象的类型保持一致。
【示例】实例化Supplier接口的实现类对象,然后调用重写方法返回Teacher对象

// 方式一:使用匿名内部类来实现
Supplier<Teacher> supplier1 = new Supplier<Teacher>() {
    @Override
    public Teacher get() {
        return new Teacher();
    }
};
System.out.println(supplier1.get());

// 方式二:使用Lambda表达式来实现
Supplier<Teacher> supplier2 = () -> new Teacher();
System.out.println(supplier2.get());

// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类的无参构造方法
Supplier<Teacher> supplier3 = Teacher :: new;
System.out.println(supplier3.get());

【示例】实例化Function接口的实现类对象,然后调用重写方法返回Teacher对象

// 方式一:使用匿名内部类来实现
Function<String, Teacher> function1 = new Function<String, Teacher>() {
    @Override
    public Teacher apply(String name) {
        return new Teacher(name);
    }
};
System.out.println(function1.apply("ande"));

// 方式二:使用Lambda表达式来实现
Function<String, Teacher> function2 = name -> new Teacher(name);
System.out.println(function2.apply("ande"));

// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类name参数的构造方法
Function<String, Teacher> function3 = Teacher :: new;
System.out.println(function3.apply("ande"));

import java.util.function.Supplier;

/**
 * ClassName: LambdaTest15
 * Description: 构造方法引用
 *      语法格式:
 *          类名::new
 *      条件:
 *          函数式接口中的方法的形式参数列表
 *          与
 *          构造方法上的形式参数列表
 *          一致。
 *          并且返回值类型相同。
 * <p>
 * Datetime: 2024/2/2 14:09
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest15 {
    public static void main(String[] args) {
        // 匿名内部类方式
        // 使用生产型的函数式接口
        Supplier<Bird> supplier = new Supplier<Bird>() {
            @Override
            public Bird get() {
                return new Bird();
            }
        };
        System.out.println(supplier.get());

        // Lambda表达式
        Supplier<Bird> supplier1 = () -> new Bird();
        System.out.println(supplier1.get());

        // 使用    构造方法引用    精简
        Supplier<Bird> supplier2 = Bird::new;
        System.out.println(supplier2.get());
    }
}

class Bird {

}

import java.util.function.Function;

/**
 * ClassName: LambdaTest16
 * Description: 构造方法引用
 * <p>
 * Datetime: 2024/2/2 14:13
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest16 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        // 使用转换型的函数式接口
        Function<String, Person> function = new Function<String, Person>() {
            @Override
            public Person apply(String name) {
                return new Person(name);
            }
        };
        System.out.println(function.apply("张三"));

        // Lambda表达式的形式
        Function<String, Person> function1 = name -> new Person(name);
        System.out.println(function1.apply("李四"));

        // 使用  构造方法引用  精简
        Function<String, Person> function2 = Person::new;
        System.out.println(function2.apply("李四"));
    }
}

class Person {
    private String name;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Person(String name) {
        this.name = name;
    }
}

11.5.6 数组引用

语法:数组类型 :: new
特点:在Lambda表达式的方法体中,创建并返回指定类型的“数组”。
要求:重写的方法有且只有一个整数型的参数,并且该参数就是用于设置数组的空间长度,并且重写方法的返回值类型和创建数组的类型保持一致。
【示例】实例化Function接口的实现类对象,并在重写方法中返回指定长度的int类型数组

// 方式一:使用匿名内部类来实
Function<Integer, int[]> function1 = new Function<Integer, int[]>() {
    @Override
    public int[] apply(Integer integer) {
        return new int[integer];
    }
};
System.out.println(Arrays.toString(function1.apply(10)));

// 方式二:使用Lambda表达式来实现
Function<Integer, int[]> function2 = num -> new int[num];
System.out.println(Arrays.toString(function2.apply(20)));

// 方式三:使用方法引用来实现
Function<Integer, int[]> function3 = int[] :: new;
System.out.println(Arrays.toString(function3.apply(30)));

import java.util.Arrays;
import java.util.function.Function;

/**
 * ClassName: LambdaTest17
 * Description: 数组引用
 *          语法格式:
 *              数组::new
 *          条件:
 *              1. 函数式接口中的方法只有一个整数型参数。
 *              2. 这个整数型参数正好是数组的长度。
 *              3. 返回值类型相同。
 * <p>
 * Datetime: 2024/2/2 14:18
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest17 {
    public static void main(String[] args) {
        // 匿名内部类的方式
        // 转换型函数式接口
        Function<Integer, int[]> function = new Function<Integer, int[]>() {
            @Override
            public int[] apply(Integer integer) {
                return new int[integer];
            }
        };
        int[] nums = function.apply(10);
        System.out.println(Arrays.toString(nums));

        // Lambda表达式
        Function<Integer, int[]> function1 = length -> new int[length];
        nums = function1.apply(20);
        System.out.println(Arrays.toString(nums));

        // 构造方法引用  精简
        Function<Integer, int[]> function2 = int[]::new;
        nums = function2.apply(30);
        System.out.println(Arrays.toString(nums));

    }
}

11.6 Lambda在集合当中的使用

为了能够让Lambda和Java的集合类集更好的一起使用,集合当中也新增了部分方法,以便与Lambda表达式对接,要用Lambda操作集合就一定要看懂源码。

11.6.1 forEach()方法

在Collection集合和Map集合中,都提供了forEach()方法用于遍历集合。
在Collection集合中,提供的forEach()方法的形参为Consumer接口(消费型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】遍历List集合中的元素

List<Integer> list = Arrays.asList(11, 22, 33, 44, 55);
// 方式一:使用匿名内部类来实现
list.forEach(new Consumer<Integer>() {
    /**
     * 获得遍历出来的元素
     * @param element 遍历出来的元素
     */
    @Override
    public void accept(Integer element) {
        System.out.println(element);
    }
});

// 方式二:使用Lambda表达式来实现
list.forEach(element -> System.out.println(element));

// 方式三:使用方法引用来实现
list.forEach(System.out :: println);

【示例】遍历Set集合中的元素

List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.forEach(new Consumer<String>() {
    /**
     * 获得遍历出来的元素
     * @param element 遍历出来的元素
     */
    @Override
    public void accept(String element) {
        System.out.println(element);
    }
});
// 方式二:使用Lambda表达式来实现
hashSet.forEach(element -> System.out.println(element));

// 方式三:使用方法引用来实现
hashSet.forEach(System.out :: println);

在Map集合中,提供的forEach()方法的形参为BiConsumer接口,而BiConsumer接口属于两个参数的消费型接口,通过该方法再配合Lambda表达式就可以遍历Map集合中的元素。
【示例】遍历Map集合中的元素

// 实例化Map集合并添加键值对
HashMap<String, String> map = new HashMap<>();
map.put("张三", "成都");
map.put("李四", "重庆");
map.put("王五", "西安");
// 方式一:使用匿名内部类来实现
map.forEach(new BiConsumer<String, String>() {
    /**
     * 获得遍历出来的key和value
     * @param key 键
     * @param value 值
     */
    @Override
    public void accept(String key, String value) {
        System.out.println("key:" + key + ",value:" + value);
    }
});
// 方式二:使用Lambda表达式来实现
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

/**
 * ClassName: LambdaTest18
 * Description:
 *          使用集合提供的forEach方法,结合Lambda表达式。
 *          遍历List集合。
 * <p>
 * Datetime: 2024/2/2 14:23
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest18 {
    public static void main(String[] args) {
        // List集合
        List<Integer> list = Arrays.asList(1,3,2,5,6,3,2,2,4,5,5,2);

        // 遍历List集合,调用forEach方法
        // forEach方法的参数是一个函数式接口:Consumer (消费型接口)
        // 匿名内部类方式
        list.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer elt) {
                // elt 代表的就是集合中的每个元素
                System.out.println(elt);
            }
        });

        System.out.println("========================");

        // Lambda表达式改进
        list.forEach(elt -> System.out.println(elt));

        System.out.println("========================");

        // 使用方法引用精简
        list.forEach(System.out::println);

    }
}

import java.util.TreeSet;

/**
 * ClassName: LambdaTest19
 * Description:
 *          使用集合提供的forEach方法,结合Lambda表达式。
 *          遍历Set集合。
 * <p>
 * Datetime: 2024/2/2 14:28
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest19 {
    public static void main(String[] args) {

        TreeSet<Integer> treeSet = new TreeSet<>();

        treeSet.add(100);
        treeSet.add(100);
        treeSet.add(200);
        treeSet.add(300);
        treeSet.add(400);
        treeSet.add(5);

        // 遍历set集合
        treeSet.forEach(System.out::println);
    }
}

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

/**
 * ClassName: LambdaTest20
 * Description:
 *          使用集合提供的forEach方法,结合Lambda表达式。
 *          遍历Map集合。
 * <p>
 * Datetime: 2024/2/2 14:30
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest20 {
    public static void main(String[] args) {

        Map<Integer, String> map = new HashMap<>();

        map.put(1, "jack");
        map.put(2, "lucy");
        map.put(3, "wangwu");
        map.put(4, "zhangsan");
        map.put(5, "lisi");

        // 遍历
        // 匿名内部类方式
        map.forEach(new BiConsumer<Integer, String>() {
            @Override
            public void accept(Integer key, String value) {
                System.out.println(key + "=" + value);
            }
        });

        // 使用Lambda表达式改进
        map.forEach((k,v) -> System.out.println(k + "=" + v));
    }
}

11.6.2 removeIf()方法

在Collection集合中,提供的removeIf()方法的形参为Predicate接口(判断型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】删除List集合中的某个元素

// 创建List集合并添加元素
List<String> list = new ArrayList<>(Arrays.asList("aa", "bb", "cc", "dd"));
// 方式一:使用匿名内部类来实现
list.removeIf(new Predicate<String>() {
    /**
     * 删除指定的某个元素
     * @param element 用于保存遍历出来的某个元素
     * @return 返回true,代表删除;返回false,代表不删除
     */
    @Override
    public boolean test(String element) {
        return "bb".equals(element);
    }
});
System.out.println(list); // 输出:[aa, cc, dd]

// 方式二:使用Lambda表达式来实现
list.removeIf("cc" :: equals);
System.out.println(list); // 输出:[aa, dd]

【示例】删除Set集合中的某个元素

List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.removeIf(new Predicate<String>() {
    /**
     * 删除指定的某个元素
     * @param element 用于保存遍历出来的某个元素
     * @return 返回true,代表删除;返回false,代表不删除
     */
    @Override
    public boolean test(String element) {
        return "bb".equals(element);
    }
});
System.out.println(hashSet); // 输出:[aa, cc, dd]

// 方式二:使用Lambda表达式来实现
hashSet.removeIf("cc" :: equals);
System.out.println(hashSet); // 输出:[aa, dd]

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

/**
 * ClassName: LambdaTest21
 * Description:
 *          集合的removeIf方法。结合Lambda表达式,删除符合条件的元素。
 *          遍历过程中删除符合条件的元素。
 *          使用的函数式接口是:判断型。(返回boolean类型的方法。)
 * <p>
 * Datetime: 2024/2/2 17:58
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class LambdaTest21 {
    public static void main(String[] args) {
        // 创建List集合
        List<String> list = new ArrayList<>();

        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");

        // 删除 cc
        // 匿名内部类的方式
        /*list.removeIf(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return "cc".equals(s);
            }
        });*/

        // 使用Lambda表达式方式
        //list.removeIf(s -> "cc".equals(s));

        // 使用方法引用继续精简
        list.removeIf("cc"::equals);

        System.out.println(list);

        // Set集合中的元素在遍历过程中删除符合条件的元素
        Set<String> set = new HashSet<>();

        set.add("100");
        set.add("200");
        set.add("300");
        set.add("400");

        set.removeIf("300"::equals);

        System.out.println(set);
    }
}

12 Stream API

12.1 主要内容

  1. Stream API的概述
  2. 创建Stream的方式
  3. Stream的中间操作
  4. Stream的终止操作

12.2 学习目标

知识点要求
Stream API的概述理解
创建Stream的方式掌握
Stream的中间操作掌握
Stream的终止操作掌握

12.3 Stream API的概述

12.3.1 什么是StreamAPI呢?

从JDK1.8开始,Java语言引入了一个全新的流式Stream API,StreamAPI把真正的函数式编程风格运用到Java语言中,使用StreamAPI可以帮我们更方便地操作集合,允许开发人员在不改变原始数据源的情况下对集合进行操作,这使得代码更加简洁、易读和可维护。
使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询,也可以使用Stream API来并行执行的操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。

12.3.2 Stream和Collection的区别

Collection:是静态的内存数据结构,强调的是数据。
Stream API:是跟集合相关的计算操作,强调的是计算。
总结:Collection面向的是内存,存储在内存中;StreamAPI面向的是CPU,通过CPU来计算。

12.3.3 Stream API的操作步骤

  1. 第一步:创建Stream
    1. 通过数据源(如:集合、数组等)来获取一个Stream对象 。
  2. 第二步:中间操作
    1. 对数据源的数据进行处理,该操作会返回一个Stream对象,因此可以进行链式操作。
  3. 第三步:终止操作
    1. 执行终止操作时,则才会真正执行中间操作,并且并返回一个计算完毕后的结果。

12.3.4 Stream API的重要特点

  1. Stream自己不会存储元素,只能对元素进行计算。
  2. Stream不会改变数据对象,反而可能会返回一个持有结果的新Stream。
  3. Stream上的操作属于延迟执行,只有等到用户真正需要结果的时候才会执行。
  4. Stream一旦执行了终止操作,则就不能再调用其它中间操作或终止操作了。

12.4 创建 Stream的方式

12.4.1 通过Collection接口提供的方法

通过Collection接口提供的stream()方法来创建Stream流。

List<String> list = Arrays.asList("aa", "bb", "cc");
Stream<String> stream = list.stream();

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest01
 * Description:
 *          获取Stream流对象的第一种方式:通过Collection接口的stream()方法来获取Stream对象
 * <p>
 * Datetime: 2024/2/3 8:53
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest01 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        // 注意,这个流属于顺序流,本质是单线程的。数据量如果不是很多,采用这种方式。
        Stream<Integer> stream = list.stream();
        // java.util.stream.ReferencePipeline$Head@b4c966a
        // 通过Stream对象可以对集合中的元素进行计算。
        System.out.println(stream);

        // 这是一个并行流(底层自动启动多线程,你不需要管,程序员不需要干涉)
        // 在计算的时候自动会启动多线程去运算。
        // 什么时候用?如果数据量非常庞大。
        Stream<Integer> parallelStream = list.parallelStream();
        // java.util.stream.ReferencePipeline$Head@2f4d3709
        System.out.println(parallelStream);
    }
}

12.4.2 通过Arrays类提供的方法

通过Arrays类提供的stream()静态方法来创建Stream流。

String[] arr1 = {"aa", "bb", "cc"};
Stream<String> stream = Arrays.stream(arr1);

int[] arr2 = {11, 22, 33, 44};
IntStream intStream = Arrays.stream(arr2);

long[] arr3 = {11, 22, 33, 44};
LongStream longStream = Arrays.stream(arr3);

double[] arr4 = {1.0, 2.0, 3.0};
DoubleStream doubleStream = Arrays.stream(arr4);

注意:Stream、IntStream、LongStream和DoubleStream都继承于BaseStream接口。


import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest02
 * Description:
 *          获取Stream对象的第二种方式:通过Arrays数组工具类的stream()方法。
 * <p>
 * Datetime: 2024/2/3 8:59
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest02 {
    public static void main(String[] args) {
        
        String[] names = {"zhangsan", "lisi", "wangwu"};
        Stream<String> stream = Arrays.stream(names);
        System.out.println(stream);

        int[] nums = {1,2,3,4};
        IntStream stream1 = Arrays.stream(nums);
        System.out.println(stream1);

        long[] lnums = {1L,2L,3L};
        LongStream stream2 = Arrays.stream(lnums);
        System.out.println(stream2);

    }
}

12.4.3 使用Stream接口提供的方法

通过Stream接口提供的of(T… values)静态方法来创建Stream流。

Stream<String> stringStream = Stream.of("aa", "bb", "cc");
Stream<Integer> integerStream = Stream.of(11, 22, 33, 44);

import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest03
 * Description:
 *          获取Stream对象的第三种方式:使用Stream接口本身的of(可变长度参数)方法
 * <p>
 * Datetime: 2024/2/3 9:03
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest03 {
    public static void main(String[] args) {

        // java.util.stream.ReferencePipeline$Head@b4c966a
        // stream是一个顺序流,单线程
        Stream<String> stream = Stream.of("abc", "def", "xyz");
        System.out.println(stream);

        System.out.println(stream.isParallel()); // false

        Stream<String> parallel = stream.parallel();
        System.out.println(parallel);

        System.out.println(stream == parallel); // true

        System.out.println(parallel.isParallel()); // true

        // java.util.stream.ReferencePipeline$Head@2f4d3709
        Stream<Integer> integerStream = Stream.of(1, 2, 3, 4);
        System.out.println(integerStream);
    }
}

12.4.4 顺序流和并行流的理解

在前面获得Stream对象的方式,我们都称之为“顺序流”,顺序流对Stream元素的处理是单线程的,即一个一个元素进行处理,处理数据的效率较低。
如果Stream流中的数据处理没有顺序要求,并且还希望可以并行处理Stream的元素,那么就可以使用“并行流”来实现,从而提高处理数据的效率。
一个普通Stream转换为可以并行处理的Stream非常简单,只需要用调用Stream提供的parallel()方法进行转换即可,这样就可以并行的处理Stream的元素。那么,我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
【示例】把顺序流转化为并行流

// 创建一个“顺序流”Stream对象
Stream<String> stream = Stream.of("aa", "bb", "cc");
// 验证:stream是否为并行流
System.out.println(stream.isParallel());         // 输出:false
// 将Stream对象转化为“并行流”
// 注意:parallel()方法返回的就是“方法的调用者对象”
Stream<String> parallelStream = stream.parallel();
System.out.println(stream == parallelStream);    // 输出:true
// 验证:stream是否为并行流
System.out.println(stream.isParallel());         // 输出:true

在Collection接口中,还专门提供了一个parallelStream()方法,用于获得一个并行流。
【示例】使用parallelStream()方法获得一个并行流

List<String> list = Arrays.asList("aa", "bb", "cc");
// 创建一个“并行流”Stream对象
Stream<String> stream = list.parallelStream();
// 验证:stream是否为并行流
System.out.println(stream.isParallel()); // 输出:true

12.5 Stream API的中间操作

中间操作属于惰式执行,直到执行终止操作才会真正的进行数据的计算,此处调用中间操作只会返回一个标记了该操作的新Stream对象,因此可以进行链式操作。
在后续的操作中,我们调用StudentData类的getStudentList()静态方法,则就能获得一个存储Student对象的List集合,其代码实现如下:

public class Student {
    private String name;
    private int age;
    private String sex;
    private String city;

    public Student() {}
    public Student(String name, int age, String sex, String city) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.city = city;
    }
    /*setter和getter方法省略*/
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}
public class StudentData {
    /**
     * 获得一个存储Student对象的List集合
     */
    public static List<Student> getStudentList() {
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("张三", 21, "男", "武汉"));
        list.add(new Student("李四", 18, "女", "重庆"));
        list.add(new Student("王五", 25, "女", "成都"));
        list.add(new Student("赵六", 22, "男", "武汉"));
        list.add(new Student("王麻子", 16, "女", "成都"));
        return list;
    }
}

12.5.1 筛选(filter)

筛选(filter),按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。该操作使用了Stream接口提供的“Stream filter(Predicate<? super T> predicate);”方法来实现。
【示例】使用筛选的案例

// 需求:筛选出年龄大于20的学生对象
Stream<Student> stream1 = StudentData.getStudentList().stream();
stream1.filter(stu -> stu.getAge() > 20).forEach(System.out :: println);
// 需求:筛选出字符串长度大于3的元素
Stream<String> stream2 = Stream.of("hello", "too", "like", "ande");
stream2.filter(str -> str.length() > 3).forEach(System.out :: println);

import java.util.Objects;

/**
 * ClassName: Student
 * Description:
 * <p>
 * Datetime: 2024/2/3 9:13
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private String gender;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }

    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 String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Student(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name) && Objects.equals(gender, student.gender);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, gender);
    }

    @Override
    public int compareTo(Student o) {
        return this.getAge() - o.getAge();
    }
}
import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: StudentService
 * Description:
 *          学生业务类
 * <p>
 * Datetime: 2024/2/3 9:13
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StudentService {
    public static List<Student> getStudents(){
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("zhangsan", 20, "男"));
        studentList.add(new Student("lisi", 21, "女"));
        studentList.add(new Student("wangwu", 22, "男"));
        studentList.add(new Student("zhaoliu", 18, "女"));
        studentList.add(new Student("qianqi", 19, "女"));
        /*studentList.add(new Student("qianqi", 19, "男"));
        studentList.add(new Student("qianqi", 19, "男"));
        studentList.add(new Student("qianqi", 19, "男"));*/
        return studentList;
    }
}
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest04
 * Description:
 *          Stream中间操作之:filter
 * <p>
 * Datetime: 2024/2/3 9:15
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest04 {
    public static void main(String[] args) {
        // 筛选出年龄大于20的学生对象
        // filter 属于中间操作,过滤
        // forEach 属于终止操作,遍历
        // filter和forEach都是Stream接口中的方法。
        // 由于Stream支持链式调用,所以可以一直"."
        // 匿名内部类的方式
        StudentService.getStudents().stream().filter(new Predicate<Student>() {
            @Override
            public boolean test(Student student) {
                return student.getAge() > 20;
            }
        }).forEach(new Consumer<Student>() {
            @Override
            public void accept(Student student) {
                System.out.println(student);
            }
        });

        // Lambda表达式方式
        StudentService.getStudents().stream().filter(student -> student.getAge() > 20).forEach(System.out::println);

        // 筛选出字符串长度大于3的元素
        Stream<String> stream = Stream.of("zhangsan", "lisi", "wangwu", "abc");
        stream.filter(s -> s.length() > 3).forEach(System.out::println);

        // 筛选出学生名字长度大于4的学生
        StudentService.getStudents().stream().filter(student  -> student.getName().length() > 4).forEach(System.out::println);
    }
}

12.5.2 映射(map)

映射(map),将一个流的元素按照一定的映射规则映射到另一个流中。该操作使用了Stream接口提供的“ Stream map(Function<? super T, ? extends R> mapper);”方法来实现。
【示例】使用映射的案例

// 需求:把字符串中的字母全部转化为大写
Stream<String> stream1 = Stream.of("hello", "too", "like", "ande");
// stream1.map(str -> str.toUpperCase()).forEach(System.out :: println);
stream1.map(String :: toUpperCase).forEach(System.out :: println);

// 需求:获得集合中所有学生的名字
Stream<Student> stream2 = StudentData.getStudentList().stream();
// stream2.map(stu -> stu.getName()).forEach(System.out :: println);
stream2.map(Student :: getName).forEach(System.out :: println);

// 需求:获得集合中性别为男的学生名字
// 思路:先筛选,后映射
Stream<Student> stream3 = StudentData.getStudentList().stream();
stream3.filter(stu -> stu.getSex().equals("男")).map(Student :: getName).forEach(System.out :: println);

在Stream接口中,可以实现“将多个集合中的元素映射到同一个流中”,该操作使用了Stream接口提供的“ Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);”方法来实现。
【示例】将多个集合中的元素映射到同一个流中

// 需求:将两个集合中的元素映射到同一个流中
List<String> list1 = new ArrayList<>();
list1.add("aa");
list1.add("bb");
list1.add("cc");

List<String> list2 = new ArrayList<>();
list2.add("dd");
list2.add("ee");
list2.add("ff");

Stream<List<String>> stream = Stream.of(list1, list2);
stream.flatMap(List<String>::stream).forEach(System.out::println);

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest05
 * Description:
 *          Stream的中间操作之:map
 *          map指的是映射操作,本质上就是:转换操作。
 * <p>
 * Datetime: 2024/2/3 9:41
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest05 {
    public static void main(String[] args) {
        // 把字符串中的字母全部转化为大写
        // 匿名内部类的方式
        Stream.of("abc", "def", "xyz").map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.toUpperCase();
            }
        }).forEach(System.out::println);

        // Lambda表达式方式
        Stream.of("abc", "def", "xyz").map(String::toUpperCase).forEach(System.out::println);

        // 获得集合中所有学生的名字
        StudentService.getStudents().stream().map(Student::getName).forEach(System.out::println);

        System.out.println("==============");

        // 需求:获得集合中性别为男的学生名字
        // 思路:先筛选,后映射
        // 先filter,然后map
        StudentService.getStudents().stream().filter(student -> student.getGender().equals("男")).map(Student::getName).forEach(System.out::println);

        // 将多个集合中的数据合并到一个流Stream当中
        // flatMap的方法作用是什么?将多个集合中的所有元素全部放到一个Stream中。
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);

        List<Integer> list2 = new ArrayList<>();
        list1.add(4);
        list1.add(5);
        list1.add(6);

		// 此时的twoListStream中存放的是两个对象list1和list2,并没有将list1和list2中的数据拿出来合并到一个Stream中
        Stream<List<Integer>> twoListStream = Stream.of(list1, list2);
        // 匿名内部类方式
        /*twoListStream.flatMap(new Function<List<Integer>, Stream<?>>() {
            @Override
            public Stream<?> apply(List<Integer> integers) {
                return integers.stream();
            }
        }).forEach(System.out::println);*/

        // Lambda表达式方式
        twoListStream.flatMap(List<Integer>::stream).forEach(System.out::println);
    }
}

12.5.3 除重(distinct)

除重(distinct),也就是除去重复的元素,底层使用了hashCode()和equals(Object obj)方法来判断元素是否相等。该操作使用了Stream接口提供的“Stream distinct();”方法来实现。
【示例】演示除重的操作

// 需求:除去重复的元素
Stream.of(11, 22, 33, 44, 33).distinct().forEach(System.out :: println);

// 需求:除去重复的学生(除重后输出学生对象)
StudentData.getStudentList().stream().distinct().forEach(System.out :: println);

// 需求:除去年龄相同的学生(除重后输出学生年龄)
// 思路:先映射,后除重
StudentData.getStudentList().stream().map(Student :: getAge).distinct().forEach(System.out :: println);

import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest06
 * Description:
 *          Stream中间操作之:distinct
 *          去除重复记录
 * <p>
 * Datetime: 2024/2/3 9:55
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest06 {
    public static void main(String[] args) {
        // 除去重复的元素
        Stream.of(1,1,1,1,1,1,1,2).distinct().forEach(System.out::println);

        // 除去重复的学生(除重后输出学生对象)
        // 去除重复记录是基于 hashCode + equals方法的。记得重写。
        StudentService.getStudents().stream().distinct().forEach(System.out::println);

        // 需求:除去年龄相同的学生(除重后输出学生年龄)
        // 思路:先映射,后除重
        StudentService.getStudents().stream().map(Student::getAge).distinct().forEach(System.out::println);
    }
}

12.5.4 排序(sorted)

排序(sorted),也就是对元素执行“升序”或“降序”的排列操作。在Stream接口中提供了“Stream sorted();”方法,专门用于对元素执行“自然排序”,使用该方法则元素对应的类就必须实现Comparable接口。
【示例】使用自然排序的案例

// 需求:对元素执行“升序”排序
Stream.of(4, 1, 3, 6, 2, 5).sorted().forEach(System.out :: println);

// 需求:按照学生的年龄执行“升序”排序(排序后输出学生对象)
StudentData.getStudentList().stream().sorted().forEach(System.out :: println);

// 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
StudentData.getStudentList().stream().map(Student :: getAge).sorted().forEach(System.out :: println);

在Stream接口中还提供了“Stream sorted(Comparator<? super T> comparator);”方法,专门用于对元素执行“指定排序”,这样就能对某一个类设置多种排序规则。
【示例】使用指定排序的案例

// 需求:对元素执行“升序”排序
Stream.of(4, 1, 3, 6, 2, 5).sorted(Integer :: compare).forEach(System.out :: println);

// 需求:按照学生的年龄执行“降序”排序(排序后输出学生对象)
StudentData.getStudentList().stream().sorted((stu1, stu2) -> stu2.getAge() - stu1.getAge()).forEach(System.out :: println);

// 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
StudentData.getStudentList().stream().map(Student :: getAge).sorted(Integer :: compare).forEach(System.out :: println);

import java.util.Comparator;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest07
 * Description:
 *          Stream中间操作之:sorted
 *          排序
 * <p>
 * Datetime: 2024/2/3 10:02
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest07 {
    public static void main(String[] args) {
        // 需求:对元素执行“升序”排序
        Stream.of(1,2,3,4,100,0,-1).sorted().forEach(System.out::println);
        Stream.of("ccc", "bbb", "abc", "aaa").sorted().forEach(System.out::println);

        // 需求:按照学生的年龄执行“升序”排序(排序后输出学生对象)
        // 注意:这里的排序是对学生对象进行排序,排序规则需要指定,Student实现java.lang.Comparable接口。
        StudentService.getStudents().stream().sorted().forEach(System.out::println);

        // 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
        // 先映射,再排序
        StudentService.getStudents().stream().map(Student::getAge).sorted().forEach(System.out::println);

        System.out.println("==================");

        // 需求:对元素执行“升序”排序
        Stream.of(10, 20, 30, 18, 15).sorted(Integer::compareTo).forEach(System.out::println);

        // 需求:按照学生的年龄执行“降序”排序(排序后输出学生对象)
        StudentService.getStudents().stream().sorted((o1,o2) -> o2.getAge() - o1.getAge()).forEach(System.out::println);

        // 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
        // 先映射,再排序
        StudentService.getStudents().stream().map(Student::getAge).sorted().forEach(System.out::println);
        StudentService.getStudents().stream().map(Student::getAge).sorted(Integer::compareTo).forEach(System.out::println);
    }
}

12.5.5 合并(concat)

合并(concat),也就是将两个Stream合并为一个Stream,此处使用Stream接口提供的“public static Stream concat(Stream<? extends T> a, Stream<? extends T> b)”静态方法来实现。
【示例】将两个Stream合并为一个Stream。

Stream<String> stream1 = Stream.of("aa", "bb", "cc");
Stream<String> stream2 = Stream.of("11", "22", "33");
Stream.concat(stream1, stream2).forEach(System.out :: println);

import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest08
 * Description:
 *          Stream中间操作之:concat
 *          合并。
 * <p>
 * Datetime: 2024/2/3 10:16
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest08 {
    public static void main(String[] args) {
        Stream<Integer> stream1 = Stream.of(1, 2, 3);
        Stream<Integer> stream2 = Stream.of(4, 5, 6);

        Stream.concat(stream1, stream2).forEach(System.out::println);
    }
}

12.5.6 截断和跳过

跳过(skip),指的就是跳过n个元素开始操作,此处使用Stream接口提供的“Stream skip(long n);”方法来实现。
截断(limit),指的是截取n个元素的操作,此处使用Stream接口提供的“Stream limit(long maxSize);”方法来实现。
【示例】从指定位置开始截取n个元素

// 需求:从索引为2的位置开始截取3个元素
Stream.of(11, 22, 33, 44, 55, 66).skip(2).limit(3).forEach(System.out :: println);

import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest09
 * Description:
 *          Stream中间操作之:skip + limit
 *          skip 是跳过
 *          limit 是截取
 *
 *          他们两个组合起来可以达到什么效果?
 *              取集合的一部分。
 * <p>
 * Datetime: 2024/2/3 10:19
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest09 {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9,10).skip(3).limit(3).forEach(System.out::println);
    }
}

12.6 Stream API的终止操作

触发终止操作时才会真正执行中间操作,终止操作执行完毕会返回计算的结果,并且终止操作执行完毕那么操作的Stream就失效,也就是不能再执行中间操作或终止操作啦。

12.6.1 遍历(forEach)

遍历(forEach),使用Stream接口提供的“void forEach(Consumer<? super T> action);”方法来遍历计算的结果。
【示例】遍历操作的案例

List<Student> list = StudentData.getStudentList();
// 遍历所有的元素
list.stream().forEach(System.out :: println);
// 遍历学生年龄大于20的元素
list.stream().filter(stu -> stu.getAge() > 20).forEach(System.out :: println);

12.6.2 匹配(match)

匹配(match),就是判断Stream中是否存在某些元素,Stream接口提供的匹配方法如下:

  1. boolean allMatch(Predicate<? super T> predicate); 检查是否匹配所有的元素
  2. boolean anyMatch(Predicate<? super T> predicate); 检查是否至少匹配一个元素
  3. boolean noneMatch(Predicate<? super T> predicate); 检查是否一个元素都不匹配
  4. Optional findFirst(); 获得第一个元素

注意:此处的Optional是一个值的容器,可以通过get()方法获得容器的值。
【示例】匹配操作的案例

List<Student> list = StudentData.getStudentList();
// 需求:匹配学生名字是否都为“王五”
boolean all = list.stream().allMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否匹配所有的元素:" + all);
// 需求:匹配学生名字是否至少有一个为“王五”
boolean any = list.stream().anyMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否至少匹配一个元素:" + any);
// 需求:匹配学生名字中是否全部都没有“王五”
boolean none = list.stream().noneMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否一个元素都不匹配:" + none);
// 需求:获得第一个学生
Student firstStu = list.stream().findFirst().get();
System.out.println(firstStu);
// 需求:获得第四个学生
// 思路:跳过前面3个学生,然后再获得第一个元素
Optional<Student> skipStu = list.stream().skip(3).findFirst();
System.out.println(skipStu);

import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest10
 * Description:
 *          Stream终止操作之:match
 * <p>
 * Datetime: 2024/2/3 11:06
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest10 {
    public static void main(String[] args) {
        // 匹配集合中元素是否都是3
        // allMatch 匹配所有
        System.out.println(Stream.of(1, 2, 3, 4).allMatch(value -> value.equals(3))); // false
        System.out.println(Stream.of(3, 3, 3, 3).allMatch(value -> value.equals(3))); // true

        // 匹配集合中元素是否包含3
        // anyMatch 匹配其中一个
        System.out.println(Stream.of(1, 2, 3, 4).anyMatch(value -> value.equals(3))); // true

        // 匹配集合中元素有没有3
        // noneMatch 匹配不上
        System.out.println(Stream.of(1, 2, 3, 4).noneMatch(value -> value.equals(3))); // false
        System.out.println(Stream.of(1, 2, 3, 4).noneMatch(value -> value.equals(100))); // true

        // 获取流中第一个元素
        Optional<Integer> firstOptional = Stream.of(1, 2, 3, 4).findFirst();
        System.out.println(firstOptional.get());

        System.out.println(Stream.of(1, 2, 3, 4).findFirst().get());

        // 需求:匹配学生名字是否都为“zhangsan”
        System.out.println(StudentService.getStudents().stream().allMatch(student -> student.getName().equals("zhangsan"))); // false

        // 需求:匹配学生名字是否至少有一个为“zhangsan”
        System.out.println(StudentService.getStudents().stream().anyMatch(student -> student.getName().equals("zhangsan"))); // true

        // 需求:匹配学生名字中是否全部都没有“lucy”
        System.out.println(StudentService.getStudents().stream().noneMatch(student -> student.getName().equals("lucy"))); // true

        // 需求:获得第一个学生
        System.out.println(StudentService.getStudents().stream().findFirst().get());
        Optional<Student> first = StudentService.getStudents().stream().findFirst();
        /*if (first.isPresent()) {
            System.out.println(first.get());
        }*/
        first.ifPresent(System.out::println);

        // 需求:获得第四个学生
        // 思路:跳过前面3个学生,然后再获得第一个元素
        System.out.println(StudentService.getStudents().stream().skip(3).findFirst().get());

    }
}

12.6.3 归约(reduce)

归约(reduce),将所有元素按照指定的规则合并成一个结果。在Stream接口中,常用的归约方法如下:

  1. Optional reduce(BinaryOperator accumulator);
  2. T reduce(T identity, BinaryOperator accumulator);

【示例】归约操作的案例

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 需求:获得集合中所有元素“相加”的结果
// Integer sum = list.stream().reduce((x, y) -> x + y).get();
Integer sum = list.stream().reduce(Integer :: sum).get();
System.out.println(sum);
// 需求:获得集合中所有元素“相乘”的结果
Integer result = list.stream().reduce((x, y) -> x * y).get();
System.out.println(result);
// 需求:获得最大长度的元素
String str = Stream.of("I", "love", "you", "too").reduce((str1, str2) -> str1.length() > str2.length() ? str1 : str2).get();
System.out.println(str);
// 需求:获得所有学生的总年龄
Integer sumAge = StudentData.getStudentList().stream().map(Student::getAge).reduce((age1, age2) -> age1 + age2).get();
System.out.println(sumAge);
// 需求:获得10和集合中所有元素“相加”的结果
Integer sum1 = list.stream().reduce(10, Integer :: sum);
System.out.println(sum1);

reduce操作可以实现从一组元素中生成一个值,而max()、min()、count()等方法都属于reduce操作,将它们单独设为方法只是因为常用,在Stream接口中这些方法如下:
long count(); 获得元素的个数
Optional max(Comparator<? super T> comparator); 获得最大的元素
Optional min(Comparator<? super T> comparator); 获得最小的元素

【示例】获得最大、最小和元素的个数

List<Student> list = StudentData.getStudentList();
// 需求:获得元素的个数
long count = StudentData.getStudentList().stream().count();
System.out.println(count);

// 需求:获得年龄“最大”的学生
Student maxStu = list.stream().max((stu1, stu2) -> stu1.getAge() - stu2.getAge()).get();
System.out.println(maxStu);
// 需求:获得学生的“最大”年龄
Integer maxAge = list.stream().map(Student::getAge).max(Integer::compare).get();
System.out.println(maxAge);

// 需求:获得年龄“最小”的学生
Student minStu = list.stream().min((stu1, stu2) -> stu1.getAge() - stu2.getAge()).get();
System.out.println(minStu);
// 需求:获得学生的“最小”年龄
Integer minAge = list.stream().map(Student::getAge).min(Integer::compare).get();
System.out.println(minAge);

import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest11
 * Description:
 *          Stream终止操作之:reduce(规约/归纳/总结)
 *          作用:将流中的所有数据,按照指定的规则,最终计算出一个结果。
 * <p>
 * Datetime: 2024/2/3 11:23
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest11 {
    public static void main(String[] args) {

        // 将集合中的所有数字求和
        System.out.println(Stream.of(1, 2, 3, 4).reduce((x, y) -> x + y).get());

        // 使用数学工具类(方法引用)
        //System.out.println(Math.addExact(10, 20));
        System.out.println(Stream.of(1, 2, 3, 4).reduce((x, y) -> Math.addExact(x, y)).get());
        System.out.println(Stream.of(1, 2, 3, 4).reduce(Math::addExact).get());

        // 需求:获得集合中所有元素“相乘”的结果
        System.out.println(Stream.of(1, 2, 3, 4).reduce(Math::multiplyExact).get());

        // 需求:获得最大长度的元素
        System.out.println(Stream.of("abc", "def", "hello", "helloworld").reduce((s1, s2) -> s1.length() > s2.length() ? s1 : s2).get());

        // 需求:获得所有学生的总年龄
        System.out.println(StudentService.getStudents().stream().map(Student::getAge).reduce(Math::addExact).get());

        // 需求:获得10和集合中所有元素“相加”的结果
        System.out.println(Stream.of(1, 2, 3, 4).reduce(10, Math::addExact));
    }
}

/**
 * ClassName: StreamAPITest12
 * Description:
 *          Stream终止操作之:reduce
 *          stream对象中的 count()、max()、min()方法等,底层实际上调用的还是 reduce() 方法。
 *          这里只是为了编程方便,专门又提供了一些API。
 * <p>
 * Datetime: 2024/2/3 11:39
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest12 {
    public static void main(String[] args) {
        // 需求:获得元素的个数
        System.out.println(StudentService.getStudents().stream().count());
        System.out.println(StudentService.getStudents().size());

        // 需求:获得年龄“最大”的学生
        System.out.println(StudentService.getStudents().stream().max((s1, s2) -> s1.getAge() - s2.getAge()).get());

        // 需求:获得学生的“最大”年龄
        System.out.println(StudentService.getStudents().stream().map(Student::getAge).max(Integer::compareTo).get());

        // 需求:获得年龄“最小”的学生
        System.out.println(StudentService.getStudents().stream().min((s1, s2) -> s1.getAge() - s2.getAge()).get());

        // 需求:获得学生的“最小”年龄
        System.out.println(StudentService.getStudents().stream().map(Student::getAge).min(Integer::compareTo).get());
    }
}

12.6.4 收集(collect)

收集(collect),可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。
调用Stream接口提供的“<R, A> R collect(Collector<? super T, A, R> collector);”方法来实现收集操作,并且参数中的Collector对象大都是直接通过Collectors工具类获得,实际上传入的Collector决定了collect()的行为。

12.6.4.1 归集(toList/toSet/toMap)

因为Stream流不存储数据,那么在Stream流中的数据完成处理后,如果需要把Stream流的数据存入到集合中,那么就需要使用归集的操作。在Collectors提供的toList、toSet和toMap比较常用,另外还有Collectors提供的toCollection等比较复杂一些的用法。
【示例】演示toList、toSet和toMap的实现

List<String> stringList = Arrays.asList("I", "love", "you", "too");
// 需求:将Stream转化为List集合
List<String> list = stringList.stream().collect(Collectors.toList());
System.out.println(list);
// 需求:将Stream转化为Set集合
Set<String> set = stringList.stream().collect(Collectors.toSet());
System.out.println(set);
// 需求:将Stream转化为Map集合
// 明确:每个元素以“:”来分割,左边的为key,右边的为value
Stream<String> stream = Stream.of("张三:成都", "李四:武汉", "王五:重庆");
Map<String, String> map = stream.collect(Collectors.toMap(str -> str.substring(0, str.indexOf(":")), str -> str.substring(str.indexOf(":") + 1)));
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));

在以上的代码中,我们将Stream流中计算的数据转化为List和Set集合时,此时并没有明确存储数据对应集合的具体类型,想要明确存储数据对应集合的具体类型,则就需要使用toCollection来实现。
【示例】演示toCollection的实现

List<String> list = Arrays.asList("I", "love", "you", "too");
// 需求:将Stream转化为ArrayList集合
ArrayList<String> arrayList = list.stream().collect(Collectors.toCollection(ArrayList::new));
System.out.println(arrayList);
// 需求:将Stream转化为LinkedList集合
LinkedList<String> linkedList = list.stream().collect(Collectors.toCollection(LinkedList::new));
System.out.println(linkedList);
// 需求:将Stream转化为HashSet集合
HashSet<String> hashSet = list.stream().collect(Collectors.toCollection(HashSet::new));
System.out.println(hashSet);
// 需求:将Stream转化为TreeSet集合
TreeSet<String> treeSet = list.stream().collect(Collectors.toCollection(TreeSet::new));
System.out.println(treeSet);

【示例】获得年龄大于20岁的女同学,然后返回按照年龄进行升序排序后的List集合

List<Student> list = StudentData.getStudentList();
ArrayList<Student> arrayList =
 list.stream().filter(stu -> stu.getAge() > 18). // 过滤年龄小于等于18的学生
 filter(stu -> stu.getSex().equals("女")). // 过滤男性学生
 sorted(Comparator.comparing(Student::getAge)). // 按照年龄执行升序排序
 collect(Collectors.toCollection(ArrayList::new)); // 转化为ArrayList存储
arrayList.forEach(System.out :: println);

在归集的知识点中,我们实现了将Stream中计算的数据转化为集合或Map,那么能否将Stream中计算的数据转化为数组呢?答案是可以的,我们可以使用Stream提供的toArray静态方法来实现。
【示例】将Stream中计算的数据转化为数组

List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
// 需求:将Stream转化为数组
Object[] array = list.stream().toArray();
System.out.println(Arrays.toString(array));
// 需求:将Stream转化为“指定类型”的数组
String[] stringArray = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(stringArray));

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest13
 * Description:
 *      Stream终止操作之:收集collect
 *      归集:toList/toSet/toMap
 * Datetime: 2024/2/19 12:02
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest13 {
    public static void main(String[] args) {
        // 将流Stream中的数据全部收集到一个集合中。
        // 收集为List集合。(具体是哪种List集合,在这里是不知道的。)
        List<String> list = Stream.of("zhangsan", "lisi", "wangwu").collect(Collectors.toList());
        System.out.println(list);

        // 收集为Set集合。(具体是哪种Set集合,在这里是不知道的。)
        Set<String> set = Stream.of("zhangsan", "lisi", "wangwu", "wangwu").collect(Collectors.toSet());
        System.out.println(set);

        // 收集为Map集合。
        // 匿名内部类的方式
        /*Map<String, String> map = Stream.of("1:zhangsan", "2:lisi", "3:wangwu").collect(Collectors.toMap(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.substring(0, s.indexOf(":"));
            }
        }, new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.substring(s.indexOf(":") + 1);
            }
        }));*/

        // Lambda表达式方式
        Map<String, String> map = Stream.of("1:zhangsan", "2:lisi", "3:wangwu")
                .collect(Collectors.toMap(s -> s.substring(0, s.indexOf(":")), s -> s.substring(s.indexOf(":") + 1)));

        // 遍历Map集合
        map.forEach((k, v) -> System.out.println(k + "===>" + v));
    }
}

import java.util.HashSet;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.ArrayList;

/**
 * ClassName: StreamAPITest14
 * Description:
 *          Stream终止操作之:收集collect
 *          归集:toCollection
 *          以指定的集合类型来进行收集
 * Datetime: 2024/2/19 12:17
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest14 {
    public static void main(String[] args) {
        // 注意:ArrayList::new 是构造方法引用
        // 以ArrayList集合进行收集
        ArrayList<String> arrayList = Stream.of("zhangsan", "lisi", "wangwu", "wangwu").collect(Collectors.toCollection(ArrayList::new));
        System.out.println(arrayList);

        // 以LinkedList集合进行收集
        LinkedList<String> linkedList = Stream.of("zhangsan", "lisi", "wangwu", "wangwu").collect(Collectors.toCollection(LinkedList::new));
        System.out.println(linkedList);

        // 以HashSet集合收集
        HashSet<Integer> hashSet = Stream.of(1, 1, 1, 1, 100, 300, 200, 50, 50).collect(Collectors.toCollection(HashSet::new));
        System.out.println(hashSet);

        // 以TreeSet集合收集
        TreeSet<Integer> treeSet = Stream.of(1, 1, 1, 1, 100, 300, 200, 50, 50).collect(Collectors.toCollection(TreeSet::new));
        System.out.println(treeSet);
    }
}


import java.util.ArrayList;
import java.util.stream.Collectors;

/**
 * ClassName: StreamAPITest15
 * Description:
 *          获得年龄大于20岁的女同学,然后返回按照年龄进行升序排序后的List集合
 * Datetime: 2024/2/19 12:23
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest15 {
    public static void main(String[] args) {

        ArrayList<Student> students = StudentService.getStudents().stream()
                .filter(student -> student.getGender().equals("女"))
                .filter(student -> student.getAge() > 18)
                .sorted((s1, s2) -> s1.getAge() - s2.getAge())
                .collect(Collectors.toCollection(ArrayList::new));

        System.out.println(students);
    }
}

import java.util.Arrays;
import java.util.stream.Stream;

/**
 * ClassName: StreamAPITest16
 * Description:
 *      Stream终止操作之:收集collect
 *      归集:转换成数组形式。
 * Datetime: 2024/2/19 12:27
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest16 {
    public static void main(String[] args) {
        // 没有指定数组的类型时,默认转换成Object[]数组。
        Object[] array = Stream.of(1, 2, 3, 4).toArray();
        System.out.println(Arrays.toString(array));

        // 转换为指定类型的数组
        Integer[] array1 = Stream.of(1, 2, 3, 4).toArray(Integer[]::new);
        System.out.println(Arrays.toString(array1));

        String[] array2 = Stream.of("a", "b", "c").toArray(String[]::new);
        System.out.println(Arrays.toString(array2));
    }
}
12.6.4.2 统计(counting/averaging)

Collectors提供了一系列用于数据统计的静态方法:

  1. 计数:counting
  2. 平均值:averagingInt、averagingLong、averagingDouble
  3. 最值:maxBy、minBy
  4. 求和:summingInt、summingLong、summingDouble
  5. 统计以上所有:summarizingInt、summarizingLong、summarizingDouble

【示例】对学生的年龄进行统计

List<Student> list = StudentData.getStudentList();
// 需求:获得元素的个数
Long count = list.stream().collect(Collectors.counting());
System.out.println(count);
// 需求:获得学生的平均年龄
Double averAge = list.stream().collect(Collectors.averagingDouble(Student::getAge));
System.out.println(averAge);
// 需求:获得最大年龄的学生
Student stu = list.stream().collect(Collectors.maxBy((stu1, stu2) -> stu1.getAge() - stu2.getAge())).get();
System.out.println(stu);
// 需求:获得所有学生年龄之和
Long sum = list.stream().collect(Collectors.summingLong(Student::getAge));
System.out.println(sum);
// 需求:获得年龄的所有的信息
IntSummaryStatistics collect = list.stream().collect(Collectors.summarizingInt(Student::getAge));
System.out.println(collect);

import java.util.DoubleSummaryStatistics;
import java.util.stream.Collectors;

/**
 * ClassName: StreamAPITest17
 * Description:
 *      Stream的终止操作之:收集collect
 *      收集中的统计相关:
 *          计数
 *          平均值
 *          最值
 *          求和
 *          ...
 * Datetime: 2024/2/19 9:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest17 {
    public static void main(String[] args) {
        // 需求:获得元素的个数
        // 可以使用reduce
        long count = StudentService.getStudents().stream().count();
        System.out.println("学生总数:" + count);

        // 也可以使用collect
        Long count2 = StudentService.getStudents().stream().collect(Collectors.counting());
        System.out.println("学生总数:" + count2);

        // 需求:获得学生的平均年龄
        Double avgAge = StudentService.getStudents().stream().collect(Collectors.averagingDouble(Student::getAge));
        System.out.println("学生平均年龄:" + avgAge);

        // 需求:获得最大年龄的学生
        Student student = StudentService.getStudents().stream().collect(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge())).get();
        System.out.println("最大年龄的学生:" + student);

        // 需求:获得所有学生年龄之和
        Integer ageSum = StudentService.getStudents().stream().collect(Collectors.summingInt(Student::getAge));
        System.out.println("所有学生年龄的和:" + ageSum);

        // 需求:获得年龄的所有的信息
        DoubleSummaryStatistics collect = StudentService.getStudents().stream().collect(Collectors.summarizingDouble(Student::getAge));
        System.out.println(collect);

        System.out.println(collect.getAverage());
        System.out.println(collect.getMax());
        System.out.println(collect.getMin());
        System.out.println(collect.getSum());

    }
}
12.6.4.3 分组(groupingBy)

分组(groupingBy),将Stream按条件分为两个Map,比如按照学生年龄分为两个Map集合。
【示例】按照学生性别分为两个Map集合

List<Student> list = StudentData.getStudentList();
// 需求:按照学生性别进行分组
Map<String, List<Student>> map = list.stream().collect(Collectors.groupingBy(Student::getSex));
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * ClassName: StreamAPITest18
 * Description:
 *          Stream终止操作之:收集collect
 *          分组:groupingBy
 * Datetime: 2024/2/19 12:33
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest18 {
    public static void main(String[] args) {
        // 按照性别分组
        Map<String, List<Student>> map = StudentService.getStudents().stream().collect(Collectors.groupingBy(Student::getGender));
        map.forEach((k , v) -> System.out.println(k + "===>" + v));
    }
}
12.6.4.4 接合(joining)

接合(joining),把Stream计算的数据按照一定的规则进行拼接。
【示例】获得所有学生的名字拼接成一个字符

List<Student> list = StudentData.getStudentList();
// 需求:将所有学生的姓名连接成一个字符串,每个名字之间以“,”连接
String allName = list.stream().map(Student::getName).collect(Collectors.joining(", "));
System.out.println(allName);

import java.util.stream.Collectors;

/**
 * ClassName: StreamAPITest19
 * Description:
 *      Stream终止操作之:收集collect
 *      接合:joining
 * Datetime: 2024/2/19 12:33
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class StreamAPITest19 {
    public static void main(String[] args) {
        // 需求:将所有学生的姓名连接成一个字符串,每个名字之间以“,”连接
        String s = StudentService.getStudents().stream()
                .map(Student::getName)
                .collect(Collectors.joining(","));

        System.out.println(s);
    }
}

13 Java新特性

13.1 主要内容

  1. 新特性的概述
  2. 新语法的结构
  3. API层面的变化

13.2 学习目标

知识点要求
新特性的概述了解
新语法方面的变化理解
API层面的变化理解

13.3 新特性的概述

纵观Java这几年的版本变化,在Java被收入Oracle之后,Java以小步快跑的迭代方式,在功能更新上迈出了更加轻快的步伐。基于时间发布的版本,可以让Java研发团队及时获得开发人员的反馈,因此可以看到最近的Java版本,有很多语法层面简化的特性。同时,Java在支持容器化场景,提供低延迟的GC方面(ZGC等)也取得了巨大的进步。

注意一个新特性的出现通常会经过以下阶段:

  1. 孵化器(Incubator)阶段:这是新特性最早的开发和试验阶段,此时新特性只能作为一个单独的模块或库出现,而不会包含在Java SE中。在这个阶段,特性的设计可能会有些不稳定,而且会经常调整和变更。
  2. 预览(Preview)阶段:在经过了孵化器阶段的验证和修改后,新特性进入了预览阶段,这是一种在Java SE内部实现的,开发人员可以使用并对其提供反馈的渠道。此时特性可能被包含在Java SE版本中,但是它默认是未开启的,需要通过特定的命令行参数或其他方式进行启用。
  3. 正式版(General Availability,GA)阶段:在经过了预览阶段的反复测试和修复后,新特性最终会在Java SE的稳定版本中发布。此时,特性被默认开启,成为Java SE的一部分,并可以在各个Java应用程序中使用。

需要注意的是,上述阶段并非一成不变,并不是所有JEP(Java Enhancement Proposal:Java增强方案)都需要经过孵化器阶段和预览阶段,这取决于特定的提案和规划。但是,Java SE领导小组通常会遵循这些阶段的流程,以确保新特性可以经过充分的评估和测试,以便能够稳定和可靠地使用在Java应用程序中。

在以下的内容中,我们对Java9到Java21新特性做一个简单的概述。

13.3.1 Java9新特性

Java9经过4次推迟,历经曲折的Java9最终在2017年9月21日发布,提供了超过150项新功能特性。

  • JEP 261: Module System
    • JDK 9 开始引入的一种全新的模块化编程方式。JPMS 的目的是为了更好地支持大型应用程序的开发和维护,同时也可以使 Java 程序在更为动态、可移植和安全的环境下运行。
  • JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)
    • 一种交互式的 Java Shell,可以在命令行上快速地进行 Java 代码的编写、验证和执行,从而提高开发者的生产力。
  • JEP 213: Milling Project Coin(细化工程改进,该计划旨在引入小型语言特性来提高代码的简洁性和可读性)
    • 在Java 9中,@SafeVarargs注解可以用于一个私有实例方法上。在Java 7和Java 8中,@SafeVarargs注解只能用于静态方法、final实例方法和构造函数。
    • 在Java 9中,可以将效果等同于final变量作为try-with-resources语句块中的资源来使用。在Java 7/8中,try-with-resources语句块中的资源必须是显式的final或事实上的final(即变量在初始化后未被修改),否则编译器会报错。这个限制限制了Java程序员使用try-with-resources语句块的能力,特别是在涉及lambda表达式、匿名类或其他读取外部变量的代码段时。
    • Java 9允许在匿名类实例化时使用钻石操作符(<>)来简化代码,但参数类型必须是具体的、可推导的类型。
    • 从Java9开始,不能使用一个单一的“_”作为标识符了。
    • 从Java9开始,接口中支持定义私有方法。
  • JEP 224: HTML5 Javadoc
    • 从Java9开始,javadoc开始支持HTML5的语法。
  • JEP 254: Compact Strings
    • 一种新的字符串表示方式,称为紧凑型字符串,以提高Java应用程序的性能和内存利用率。通过String源码得知:char[] 变成了 byte[]。
  • JEP 269: Convenience Factory Methods for Collections
    • 更加方便的创建只读集合:List.of(“abc”, “def”, “xyz”);
  • JEP 269:对Stream API进行了增强
    • 其中最显著的是引入了四个新的方法,分别是 takeWhile(), dropWhile(), ofNullable()iterate()
  • JEP 110:一个新的HTTP客户端API,名为HttpClient,它是一种基于异步和事件驱动的方式,更加高效和灵活的HTTP客户端。

13.3.2 Java10新特性

2018年3月21日,Oracle官方宣布JAVA10正式发布。JAVA10一共定义了109个新特性,其中包含JEP,对开发人员来说,真正的新特性也就一个,还有一些新的API和JVM规范以及JAVA语言规范上的改动。

  • JEP 286:局部变量类型推断
  • JEP 296:将 JDK 森林合并到单个存储库中
  • JEP 304:垃圾收集器接口
  • JEP 307:G1 的并行完整 GC
  • JEP 310:应用程序类数据共享
  • JEP 312:线程局部握手
  • JEP 313:删除本机头生成工具 (javah)
  • JEP 314:附加 Unicode 语言标签扩展
  • JEP 316:替代内存设备上的堆分配
  • JEP 317:基于 Java 的实验性 JIT 编译器
  • JEP 319:根证书
  • JEP 322:基于时间的发布版本控制

13.3.3 Java11新特性

2018年9月26日,Oracle官方发布JAVA11。这是JAVA大版本周期变化后的第一个长期支持版本,官方支持到2026年。

  • JEP 181:基于 Nest 的访问控制
  • JEP 309:动态类文件常量
  • JEP 315:改进 Aarch64 内部函数
  • JEP 318:Epsilon:无操作垃圾收集器
  • JEP 320:删除 Java EE 和 CORBA 模块
  • JEP 321:HTTP 客户端(标准)
  • JEP 323:本地变量语法LAMBDA参数
  • JEP 324:与Curve25519密钥协商和Curve448
  • JEP 327:Unicode的10
  • JEP 328:飞行记录器
  • JEP 329:ChaCha20和Poly1305加密算法
  • JEP 330:启动单文件源代码程序
  • JEP 331:低开销堆纹
  • JEP 332:传输层安全性 (TLS) 1.3
  • JEP 333:ZGC:可扩展的低延迟垃圾收集器(实验性)
  • JEP 335:弃用 Nashorn JavaScript 引擎
  • JEP 336:弃用 Pack200 工具和 API

13.3.4 Java12新特性

2019年3月19日,java12正式发布。

  • JEP 189:Shenandoah:一个低暂停时间的垃圾收集器(实验性)
  • JEP 230:微基准套件
  • JEP 325:switch表达式(预览)
  • JEP 334:JVM 常量 API
  • JEP 340:一个 AArch64 端口
  • JEP 341:默认 CDS 档案
  • JEP 344:G1 支持可中断的 Mixed GC
  • JEP 346:及时从 G1 返回未使用的已提交内存

13.3.5 Java13新特性

  • JEP 350:动态 CDS 档案
  • JEP 351:ZGC:取消提交未使用的内存
  • JEP 353:重新实现旧的 Socket API
  • JEP 354:开关表达式(预览)
  • JEP 355:文本块(预览)

13.3.6 Java14新特性

  • JEP 305:instanceof 的模式匹配(预览)
  • JEP 343:包装工具(孵化器)
  • JEP 345:G1 的 NUMA 感知内存分配
  • JEP 349:JFR 事件流
  • JEP 352:非易失性映射字节缓冲区
  • JEP 358:有用的空指针异常
  • JEP 359:记录(预览)
  • JEP 361: switch表达式(标准)
  • JEP 362:弃用 Solaris 和 SPARC 端口
  • JEP 363:删除并发标记清除 (CMS) 垃圾收集器
  • JEP 364:macOS 上的 ZGC
  • JEP 365:Windows 上的 ZGC
  • JEP 366:弃用 ParallelScavenge + SerialOld GC 组合
  • JEP 367:删除 Pack200 工具和 API
  • JEP 368:文本块(第二次预览)
  • JEP 370:外部内存访问 API(孵化器)

13.3.7 Java15新特性

  • JEP 339:爱德华兹曲线数字签名算法 (EdDSA)
  • JEP 360:密封类(预览)
  • JEP 371:隐藏类
  • JEP 372:删除 Nashorn JavaScript 引擎
  • JEP 373:重新实现旧版 DatagramSocket API
  • JEP 374:禁用和弃用偏向锁定
  • JEP 375:instanceof 的模式匹配(第二次预览,无改动)
  • JEP 377:ZGC:可扩展的低延迟垃圾收集器(确定正式版)
  • JEP 378:文本块(确定正式版)
  • JEP 379:Shenandoah:一个低暂停时间的垃圾收集器(确定正式版)
  • JEP 381:删除 Solaris 和 SPARC 端口
  • JEP 383:外内存访问API(第二孵化器)
  • JEP 384:记录(第二次预览)
  • JEP 385:弃用 RMI 激活以进行删除

13.3.8 Java16新特性

  • JEP 338:Vector API(孵化器)
  • JEP 347:启用 C++14 语言功能
  • JEP 357:从 Mercurial 迁移到 Git
  • JEP 369:迁移到 GitHub
  • JEP 376:ZGC:并发线程栈处理
  • JEP 380:Unix 域套接字通道
  • JEP 386:Alpine Linux 端口
  • JEP 387:弹性元空间
  • JEP 388:Windows/AArch64 端口
  • JEP 389:外链 API(孵化器)
  • JEP 390:基于值的类的警告
  • JEP 392:打包工具
  • JEP 393:外内存访问API(第三孵化器)
  • JEP 394:instanceof 的模式匹配
  • JEP 395:记录
  • JEP 396:默认情况下强封装JDK内部
  • JEP 397:密封类(第二次预览)

13.3.9 Java17新特性

2021年9月14日,java17正式发布(LTS)。长期支持版,支持到2029年。Oracle 宣布,从JDK17开始,后面的JDK都全部免费提供。

  • JEP 306:恢复始终严格的浮点语义
  • JEP 356:增强型伪随机数发生器
  • JEP 382:新的 macOS 渲染管线
  • JEP 391:macOS/AArch64 端口
  • JEP 398:弃用 Applet API 以进行删除
  • JEP 403:强封装JDK内部
  • JEP 406:switch模式匹配(预览)
  • JEP 407:删除 RMI 激活
  • JEP 409:密封类(正式确定)
  • JEP 410:删除实验性 AOT 和 JIT 编译器
  • JEP 411:弃用安全管理器以进行删除
  • JEP 412:外部函数和内存 API(孵化器)
  • JEP 414:Vector API(第二孵化器)
  • JEP 415:上下文特定的反序列化过滤器

13.3.10 Java18新特性

2022年3月22日发布。非长期支持版本。

  • JEP 400:从JDK18开始,UTF-8是Java SE API的默认字符集。
  • JEP 408:从JDK18开始,引入了jwebserver这样一个简单的WEB服务器,它是一个命令工具。
  • JEP 416:使用方法句柄重新实现核心反射
  • JEP 418:互联网地址解析SPI
  • JEP 413:Java API文档中的代码段(javadoc注释中使用
    括起来的代码段会原模原样的生成到帮助文档中)
  • JEP 417:Vector API(第三孵化器)
  • JEP 419:Foreign Function & Memory API(第二孵化器)
  • JEP 420:switch 的模式匹配(第二次预览)
  • JEP 421:Object中的finalize()方法被移除

13.3.11 Java19新特性

2022年9月20日发布。非长期支持的版本。直到 2023 年 3 月它将被 JDK 20 取代。

  • JEP 425:虚拟线程(预览版)
    • 一种新的线程模型,即虚拟线程;“虚拟线程” 指的是一种轻量级线程,可以通过 JVM 进行管理和调度,而不需要操作系统进行支持
  • JEP 428:结构化并发(孵化器)
    • 一组新的API和规范,用于优化并简化Java程序的并发编程
  • JEP 405:Record模式 (预览版)
  • JEP 427:switch语句中的模式匹配(第三次预览版)
    • "switch语句中的模式匹配"表示该特性是针对 switch 语句的改进,可以使用模式匹配的方式处理 switch 语句中的分支
  • JEP 424:外部函数和内存API(预览版)
    • “外部函数”指的是在Java程序中调用非Java语言编写的函数,比如C/C++函数
    • “内存API”指的是在Java程序中直接操作内存的API
  • JEP 426:向量API(第四版孵化器)
    • 一组专用于向量化处理的API,允许在Java程序中轻松高效地执行向量化计算

13.3.12 Java20新特性

2023年3月21日发布。非长期支持版本。直到 2023 年 9月它将被 JDK 21 取代。

  • JEP 432: Record模式(第二次预览版)
  • JEP 433: switch的模式匹配 (第四次预览版)
  • JEP 434: 外部函数和内存API(第二次预览版)
  • JEP 438: 向量API (第五版孵化器)
  • JEP 429: Scoped Values (Incubator)
  • JEP 436: 虚拟线程(第二次预览版)
  • JEP 437: 结构化并发(第二版孵化器)

13.3.13 Java21新特性

2023年9月19日发布。长期支持版本。

  • JEP 440:Record模式(正式确定)
  • JEP 441:switch的模式匹配(正式确定)
  • JEP 430:String Templates (Preview)
  • JEP 443:Unnamed Patterns and Variables (Preview)
  • JEP 445:Unnamed Classes and Instance Main Methods (Preview)
  • JEP 444:Virtual Threads(正式确定)
  • JEP 431:Sequenced Collections(正式确定)
  • JEP 452:Key Encapsulation Mechanism API
  • JEP 442:Foreign Function & Memory API (Third Preview)
  • JEP 453:Structured Concurrency (Preview)
  • JEP 446:Scoped Values (Preview)
  • JEP 448:Vector API (Sixth Incubator)
  • JEP 439:Generational ZGC
  • JEP 451:Prepare to Disallow the Dynamic Loading of Agents

13.4 新语法方面的变化

13.4.1 jShell命令

jShell命令是Java9引进的新特性,像Python和Scala之类的语言早就有交互式编程环境REPL (read-evaluate-print-loop),以交互式的方式对语句和表达式进行求值。开发者只需要输入一些代码,就可以在编译前获得对程序的反馈。而之前的Java 版本要想执行代码,必须创建文件、声明类、提供测试方法方可实现。
我们打开DOS命令窗口,然后输入jshell,就能进入交互式编程环境REPL,如下图所示:
图片1.png
通过jShell命令,我们能够定义一些变量,并执行相关的运算操作,如下图所示:
图片2.png
通过jShell命令,我们能够定义方法,并执行调用方法的操作,如下图所示:
图片3.png
想要查看JShell提供的所有指令,则直接输入“/help”即可,如下图所示:
图片4.png
想要查看书写的所有代码,则直接输入“/list”指令即可,如下图所示:
图片5.png
想要查看定义的所有变量,则直接输入“/vars”指令即可,如下图所示:
图片6.png
想要查看定义的所有方法,则直接输入“/methods”指令即可,如下图所示:
图片7.png
想要将输入的历史代码片段保存到文件中,就需要使用“ /save”指令,如下图所示:
图片8.png

13.4.2 try-with-resources

众所周知,所有被打开的系统资源,比如流、文件、Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。
在Java7以前,我们想要关闭资源就必须的finally代码块中完成。
【示例】Java7之前资源的关闭的方式

public void copyFile1(File srcFile, File destFile) {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        // 实例化IO流(输入流和输出流)
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);
        // 拷贝文件(存储和读取)
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 关闭资源
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Java7及以后关闭资源的正确姿势:try-with-resource,该语法格式为:

try(/*实例化需要关闭资源的对象或引用需要关闭资源的对象*/){
	// 书写可能出现异常的代码
} catch(Exception e) {
	// 处理异常
}

使用try-with-resource来自动关闭资源,则需要关闭资源的对象对应的类就必须实现java.lang.AutoCloseable接口,该接口中提供了一个close()的抽象方法,而自动关闭资源默认调用的就是实现于java.lang.AutoCloseable接口中的close()方法。
因为FileInputStream类和FileOutputStream类都属于java.lang.AutoCloseable接口的实现类,因此此处文件拷贝的操作就可以使用try-with-resource来自动关闭资源。
【示例】Java7之后资源的关闭的方式

public void copyFile(File srcFile, File destFile) {
    // 实例化IO流(输入流和输出流)
    try (FileInputStream fis = new FileInputStream(srcFile);
         FileOutputStream fos = new FileOutputStream(destFile)) {
        // 拷贝文件(存储和读取)
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

通过try-with-resource来关闭放资源,即使资源很多,代码也可以写的很简洁,如果用Java7之前的方式去关闭资源,那么资源越多,用finally关闭资源时嵌套也就越多。
在Java9之后,为了避免在try后面的小括号中去实例化很多需要关闭资源的对象(复杂),则就可以把需要关闭资源的多个对象在try之前实例化,然后在try后面的小括号中引用需要关闭资源的对象即可,从而提高了代码的可读性。
【示例】Java9之后的使用方式

public void copyFile(File srcFile, File destFile) throws FileNotFoundException {
    // 实例化IO流(输入流和输出流)
    FileInputStream fis = new FileInputStream(srcFile);
    FileOutputStream fos = new FileOutputStream(destFile);
    // 拷贝文件(存储和读取)
    try (fis; fos) {
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在以上代码中,表达式中引用了fis和fos,那么在fis和fos就自动变为常量啦,也就意味着在try代码块中不能修改fis和fos的指向,从而保证打开的资源肯定能够关闭。


import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * ClassName: NewFun01
 * Description:
 *      Java7新特性:try-with-resources
 * Datetime: 2024/2/19 10:46
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class NewFun01 {
    public static void main(String[] args) throws Exception {
        // java7之前:完成文件复制
        /*FileInputStream in = null;
        FileOutputStream out = null;
        try {
            in = new FileInputStream("E:\\powernode\\02-JavaSE\\video\\上\\001-课程内容介绍.avi");
            out = new FileOutputStream("E:\\001-课程内容介绍.avi");

            // 一边读一边写
            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = in.read(bytes)) != -1){
                out.write(bytes, 0, readCount);
            }

            out.flush();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }*/

        // Java7之后(包括Java7),新特性:try-with-resources
        /*try(FileInputStream in = new FileInputStream("E:\\powernode\\02-JavaSE\\video\\上\\001-课程内容介绍.avi");
            FileOutputStream out = new FileOutputStream("E:\\001-课程内容介绍.avi")){

            // 一边读一边写
            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = in.read(bytes)) != -1){
                out.write(bytes, 0, readCount);
            }
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }*/

        // Java9之后,对try-with-resources进行了再次的改进
        FileInputStream in = new FileInputStream("E:\\powernode\\02-JavaSE\\video\\上\\001-课程内容介绍.avi");
        FileOutputStream out = new FileOutputStream("E:\\001-课程内容介绍.avi");
        try(in; out){
            // 一边读一边写
            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = in.read(bytes)) != -1){
                out.write(bytes, 0, readCount);
            }
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

13.4.3 局部变量类型判断

在Java10中,新增了局部变量类型判断。在方法体或代码块中,对于可以在编译期确定的类型,可以使用var来定义。这个特性并不意味着java是弱类型的语言,仅是提供了更简洁的书写方式。对于编译期无法确定的类型,依然要写清楚类型。
【示例】局部变量类型判断案例

// 使用var来作为变量的引用声明
var num = 123;
var str = "hello world";
var arr = new int[] {11, 22, 33};
var arrayList = new ArrayList<String>();
var calendar = Calendar.getInstance();
// 以下为不可以声明为var的情况
// 1.使用var必须要求变量必须初始化
// var userName;
// 2.不能给变量赋null值
// var userName = null;
// 3.lambda表达式不可以声明为var
// var function = (num) -> Math.round(3.51);
// 4.方法引用不可以声明为var
// var method = System.out :: println;
// 5.数组静态初始化不可以声明为var
// var arr = {"aa", "bb", "cc"};
// 6.类的成员变量不可以使用var类型推断
// 7.所有参数声明,返回值类型,构造方法参数都不可以

/**
 * ClassName: NewFun02
 * Description:
 *          Java10的新特性:局部变量类型判断
 * Datetime: 2024/2/19 10:54
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class NewFun02 {

    String name = "zhangsan";

    // 报错
    //var myName = "lisi";

    public static void doSome(int i){

    }

    // 报错
    /*public static void doOther(var i){

    }*/

    public static void main(String[] args) {
        /*int i = 100;
        double d = 3.14;
        boolean flag = false;*/

        // 使用Java10的新特性:局部变量类型推断
        var i = 100;
        var d = 3.14;
        var flag = false;

        System.out.println(i);
        System.out.println(d);
        System.out.println(flag);

    }
}

13.4.4 instanceof的模式匹配

在JDK14中新增instanceof模式匹配增强(预览),在JDK16中转正。通过instanceof模式匹配增强,我们就可以直接在模式匹配的括号内声明对应类型的局部变量。
【示例】执行向下转型的操作,从而调用show()方法

/**
 * 以前的代码实现方式
 */
@Test
public void testOld() {
    // 父类引用指向子类对象(多态)
    Animal animal = new Dog();
    // 判断animal是否为Dog类的实例
    if (animal instanceof Dog) {
        // 指向向下转型的操作
        Dog dog = (Dog) animal;
        // 调用Dog类特有的show()方法
        dog.show();
    }
}
/**
 * 使用instanceof模式匹配增强的实现方式
 */
public void testNew() {
    // 父类引用指向子类对象(多态)
    Animal animal = new Dog();
    // 如果animal是Dog类的实例,则向下转型后就命名为dog
    if (animal instanceof Dog dog) {
        // 调用Dog类特有的show()方法
        dog.show();
    }
}

【示例】重写equals(),判断成员变量是否相等

public class Tiger {
    String name;
    int age;

    /**
     * 以前的代码实现方式
     */
    /*@Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        // 如果obj属于Tiger类型,则就执行向下转型的操作
        if (obj instanceof Tiger) {
            // 执行向下转型的操作,恢复对象的实际类型
            Tiger tiger = (Tiger) obj;
            // 如果成员变量都相等,则返回true,否则返回false
            return age == tiger.age && Objects.equals(name, tiger.name);
        }
        // 如果obj不属于Tiger类型,则返回false即可
        return false;
    }*/

    /**
     * 使用instanceof模式匹配增强的实现方式
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        // 如果obj属于Tiger类型并且成员变量值都相等,那么返回true
        if (obj instanceof Tiger tiger) {
            return age == tiger.age && Objects.equals(name, tiger.name);
        }
        // 如果obj不属于Tiger类型,则返回false即可
        return false;
    }
}

/**
 * ClassName: Animal
 * Description:
 * Datetime: 2024/2/19 12:43
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Animal {
}
/**
 * ClassName: Cat
 * Description:
 * Datetime: 2024/2/19 12:43
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Cat extends Animal{

    // 这个方法是子类特有的。父类没有。
    public void catchMouse(){
        System.out.println("猫抓老鼠");
    }
}
/**
 * ClassName: NewFun03
 * Description:
 *      Java14的新特性:instanceof的模式匹配。
 *      Java16发布的正式版本。
 *
 *      作业:自己尝试重写一下某个类的equals方法,请使用新特性:instanceof的模式匹配。
 * Datetime: 2024/2/19 12:44
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class NewFun03 {
    public static void main(String[] args) {
        // 以前
        Animal a = new Cat();
        if(a instanceof Cat) {
            Cat cat = (Cat) a;
            cat.catchMouse();
        }

        // 使用instanceof模式匹配之后
        if(a instanceof Cat c){
            c.catchMouse();
        }
    }
}

13.4.5 switch表达式

目前switch表达式的问题:

  1. 匹配自上而下,若无break,后面的case语句都会执行
  2. 不同的case语句定义的变量名不能相同
  3. 不能在一个case后面写多个值
  4. 整个switch不能作为表达式的返回值

在Java12中对switch表达式做了增强(预览),能够使用更加简洁的代码来解决这些问题。
【示例】switch表达式使用的案例

/**
 * 需求:根据月份输出对应季节的特点
 * 方案一:使用以前的技术来实现
 */
public static void normalSwitch(int month) {
    // 定义一个变量,用于保存季节的特点
    String season;
    // 判断month的取值,从而知晓对应的季节
    switch (month) {
        case 12:
        case 1:
        case 2:
            season = "白雪皑皑";
            break;
        case 3:
        case 4:
        case 5:
            season = "春意盎然";
            break;
        case 6:
        case 7:
        case 8:
            season = "夏日炎炎";
            break;
        case 9:
        case 10:
        case 11:
            season = "秋高气爽";
            break;
        default:
            throw new RuntimeException("没有该月份。。。");
    }
    // 输出month对应季节的特点
    System.out.println(season);
}

/**
 * 需求:根据月份输出对应季节的特点
 * 方案二:使用Java12的新特性来实现
 */
public static void newSwitch(int month) {
    // 判断month的取值,获得对应季节的特点
    String season = switch (month) {
        case 12, 1, 2 -> "白雪皑皑";
        case 3, 4, 5 -> "春意盎然";
        case 6, 7, 8 -> "夏日炎炎";
        case 9, 10, 11 -> "秋高气爽";
        default -> throw new RuntimeException("没有该月份。。。");
    };
    // 输出month对应季节的特点
    System.out.println(season);
}

在Java13中,增加关键字yield关键字(预览), 用于在switch表达式中返回结果。到Java14版本中,Java12和Java13中关于switch的新特性都确定为正式版本。
【示例】switch表达式中的yield关键字

/**
 * 需求:根据月份输出对应季节的特点
 * 演示:Java13版本中新增的yield新特性
 */
public static void yieldSwitch1(int month) {
    // 判断month的取值,获得对应季节的特点
    String season = switch (month) {
        case 12, 1, 2:
            yield "白雪皑皑";
        case 3, 4, 5:
            yield "春意盎然";
        case 6, 7, 8:
            yield "夏日炎炎";
        case 9, 10, 11:
            yield "秋高气爽";
        default:
            throw new RuntimeException("没有该月份。。。");
    };
    // 输出month对应季节的特点
    System.out.println(season);
}

13.4.6 文本块

在Java语言中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。
在Java12版本中,新增了文本块(预览)。文本块就是指多行字符串,例如一段格式化后的xml、json等。而有了文本块以后,用户不需要转义,Java能自动搞定。因此,文本块将提高Java程序的可读性和可写性。
【示例】演示文本块的使用

// 使用以前拼接的方式
String html1 = "<html>\n" +
        "      <body>\n" +
        "            <p>Hello, world</p>\n" +
        "      </body>\n" +
        "</html>";
System.out.println(html1);
// 使用文本块的方式
String html2 = """
        <html>
              <body>
                    <p>Hello, world</p>
              </body>
        </html>
        """;
System.out.println(html2);

在Java14版本中,针对文本块又新增两个特性(阅览)。1)在一行的结尾增加“\”可以取消改行的换行符;2)可以通过“\s”增加空格。
【示例】演示文本块新增特性

// 取消换行(\)
String json1 = """
        {
            "username":"ande",\
            "age":18
        }
        """;
System.out.println(json1);
// 添加空格(\s)
String json2 = """
        {
            "username"\s:\s"ande",
            "age"\s:\s18
        }
        """;
System.out.println(json2);

/**
 * ClassName: NewFun05
 * Description:
 *          Java12的新特性:文本块
 * Datetime: 2024/2/19 14:02
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class NewFun05 {
    public static void main(String[] args) {
        // 需求:输出一段HTML代码到控制台。并且要求展示的格式如下:
        /*
            <html>
                <head>
                    <title>first page</title>
                </head>
                <body>
                    <h1>my first html page!</h1>
                </body>
            </html>
         */
        String htmlCode = "<html>\n" +
                          "    <head>\n" +
                          "        <title>first page</title>\n" +
                          "    </head>\n" +
                          "    <body>\n" +
                          "        <h1>my first html page!</h1>\n" +
                          "    </body>\n" +
                          "</html>";

        System.out.println(htmlCode);

        // 使用文本块
        String htmlCode2 = """
                <html>
                	<head>
                		<title>first page</title>\
                	</head>
                	<body>
                		<h1>my first html pag\s\s\s\s\s\se!</h1>
                	</body>
                </html>
                """ ;
        System.out.println(htmlCode2);

    }
}

13.4.7 Record

早在2019年2月份,Java语言架构师Brian Goetz就吐槽了Java语言,他和很多程序员一样抱怨“Java太啰嗦”或有太多的“繁文缛节”,他提到:开发人员想要创建纯数据载体类,通常都必须编写大量低价值、重复的、容易出错的代码。例如:构造方法、getter/setter、equals()、hashCode()以及toString()等。
以至于很多人选择使用IDE的功能来自动生成这些代码。还有一些开发会选择使用一些第三方类库,如Lombok等来生成这些方法,从而会导致了令人吃惊的表现和糟糕的可调试性。
那么,Brian Goetz大神提到的纯数据载体到底指的是什么呢?我们举了一个简单的例子:

public final class Tiger {
    private final String name;
    private final int age;

    public Tiger(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (Tiger) obj;
        return Objects.equals(this.name, that.name) &&
                this.age == that.age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Tiger[" +
                "name=" + name + ", " +
                "age=" + age + ']';
    }
}

这里面的Tiger其实就是一个纯数据载体,Tiger类中提供了name和age两个私有常量,并且只提供了全参构造方法和常量名相同的getter方法,以及一些equals、hashCode和toString等方法。于是,BrianGoetz大神提出一种想法,他提到,Java完全可以对于这种纯数据载体通过另外一种方式表示。
在Java14版本中,新增了Record类型。Record是Java的一种新的类型,Record使数据类型变得非常简洁,一般可以帮助我们定义一些简单的用于纯数据载体的实体类。

Record类的特点:
状态声明中的每个属性,都是默认采用了private和final修饰,则属性值就不可修改
在Record类中,默认已经重写了Object类提供的equals(),hashcode(),toString()方法
在Record类中,默认提供全参的构造方法,并且提供的getter方法名和属性名保持一致。
Record类采用了final修饰,并且显示的继承于java.lang.Record类,因此就不能继承别的父类。
【示例】将以上的Tiger类转化为Record类

public record Tiger(String name, int age)  {

}

在以上的Record类中,Tiger类默认采用了final修饰,并且显示的继承于java.lang.Record抽象类,因此Tiger类就不能继承于别的父类。在Tiger类中,提供了name和age两个私有常量,并且还提供了一个public修饰的全参构造方法,提供的getter方法的名字和属性名保持一致,但是并没有提供setter方法。并且,在Tiger类中还重写了Object类提供的equals(),hashcode(),toString()方法。
在Record类中,我们还可以新增静态属性、无参构造方法、成员方法和静态方法,但是创建对象时不能调用无参构造方法,而是通过全参构造方法创建对象的时候,默认就会调用Record类中的无参构造方法。
【示例】在Record类中添加的内容

public record Tiger(String name, int age)  {
    // 新增静态属性
    static double score;
    // 新增无参构造方法
    // 注意:通过全参构造方法创建对象,默认就会调用此处的无参构造方法
    public Tiger {
        System.out.println("无参构造方法");
    }
    // 新增成员方法
    void show() {
        System.out.println("show. ..");
    }
    // 新增静态方法
    static void method() {
        System.out.println("method ...");
    }
}

import java.util.Objects;

/**
 * ClassName: User
 * Description:
 *          这个类中的属性是final修饰的,并且只提供了属性的读取方法。没有提供属性的修改方法。
 *          很明显,这个类其实起到的作用就是:纯数据的载体。
 * Datetime: 2024/2/19 14:03
 * Author: 老杜@动力节点
 * Version: 1.0
 */
/*public final class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name(){
        return name;
    }

    public int age(){
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}*/

// 如果在开发中,你需要这样一个类:专门只做纯数据的载体。那么可以将这种类型定义为Record类型。
public record User(String name, int age) {
    // 自动带有两个实例变量,并且被final修饰,一个是name,一个是age。
    // 自动带有toString方法
    // 自动带有hashCode + equals方法
    // 自动带有 name() 方法
    // 自动带有 age() 方法
    // 自动带有全参数的构造方法

    // Record类型中可以添加什么?
    // 无参数的构造方法
    public User{
        System.out.println("Record类型的无参数构造方法");
    }
    // 静态变量
    static int i = 100;

    // 静态方法
    public static void m1(){
        System.out.println("m1....");
    }
    // 实例方法
    public void m2(){
        System.out.println("m2...");
    }
}

// 测试类
class UserTest {
    public static void main(String[] args) {
        User user = new User("zhangsan", 20);
        System.out.println(user.name());
        System.out.println(user.age());
        System.out.println(user.toString());

        System.out.println("静态变量:" + User.i);
        User.m1();
        user.m2();
    }
}

/**
 * ClassName: Customer
 * Description:
 *          所有的Record本质上还是一个class。
 *          所有的Record类型继承 java.lang.Record。
 *          所有的Record类型都是final的。
 *          Record类型中的属性也都是final修饰的。
 * Datetime: 2024/2/19 14:14
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public record Customer(String name, int age, boolean sex) {
}

13.4.8 密封类

Java中的密封类是一种新的类修饰符,它可以修饰类和接口,可以控制哪些类可以扩展或实现该类或接口。下面是密封类的一些主要用途:

  1. 维护类层次结构的封闭性

密封类的一个主要用途是确保类层次结构的封闭性。这意味着,如果您想保护一组类,而不希望其他类继承或实现它们,可以使用密封类来实现这一目标。这对于确保代码的安全性和稳定性非常有用。

  1. 预防代码的意外扩展

密封类可以防止其他程序员意外地扩展一个类。在进行类设计时,您可能希望自己或其他程序员只能在特定的类中实现或继承指定的类。在这种情况下,您可以将类标记为“密封”,强制限制其他程序员可以实现或继承的类的范围。

  1. 增强代码的可读性和可维护性

密封类可以增强代码的可读性和可维护性。由于密封类明确规定了哪些类可以扩展或实现它,因此其他开发人员可以更清晰地看到代码的结构并理解它们的关系。这使得代码更易于维护和修改。
总之,密封类是一种灵活而有用的类修饰符,可以帮助您维护类的封闭性、预防代码的意外扩展、增强代码的可读性和可维护性。

在Java15版本中,新增了密封类和密封接口(预览)。
使用sealed关键字修饰的类,我们就称之为密封类。密封类必须是一个父类,我们可以使用permits关键字来指定哪些子类可以继承于密封类,并且密封类的子类必须使用sealed、final或non-sealed来修饰。
【示例】密封类的演示

// 密封类必须被继承,并且使用permits来指定哪些子类可以被继承
sealed class Animal permits Dog, Bird, Tiger { }
// 注意:密封类的子类必须使用sealed、final或non-sealed来修饰
// final关键字修饰的子类,则该子类不能被继承
final class Tiger extends Animal { }
// non-sealed修饰的子类,则该子类就是一个普通类
non-sealed class Bird extends Animal { }
// sealed修饰的子类,则该类就必须被继承,否则就会编译错误
sealed class Dog extends Animal {}
non-sealed class SmallDog extends Dog {}

使用sealed关键字修饰的接口,我们就称之为密封接口。密封接口必须使用permits关键字来指定实现类或子接口。针对密封接口的实现类,则必须使用sealed、final或non-sealed来修饰;针对密封接口的子接口,则必须使用sealed或non-sealed来修饰。
【示例】密封接口的演示

// 使用sealed修饰的接口,则必须使用permits来指定实现类或子接口。
public sealed interface InterA permits Student, InterB { }
// 密封接口的实现类,必须使用sealed、final或non-sealed来修饰
non-sealed /*final*/ /*sealed*/ class Student implements InterA { }
// 密封接口的子接口,必须使用sealed或non-sealed来修饰
non-sealed /*sealed*/ interface InterB extends InterA {}

sealed与record:
因为Record类默认采用了final关键字修饰,因此Record类就可以作为密封接口的实现类。
【示例】密封接口和Record类

// 密封接口
sealed interface Flyable permits SuperMan { }
// 让Record类作为密封接口的实现类
record SuperMan(String name, int age) implements Flyable { }

/**
 * ClassName: SealedTest
 * Description:
 *      在Java15版本中,新增了密封类和密封接口
 * Datetime: 2024/2/19 14:29
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class SealedTest {
}

// 现在定义的这个T类是可以被所有子类继承的。没有任何限制。
class T {}
class T1 extends T{}
class T2 extends T{}
class T3 extends T{}
class T4 extends T{}

// 需求:定义一个Person类,只允许 Teacher、Student、Worker这三个类继承。
// 怎么定义密封类:在class关键字前面添加 sealed 关键字。
// 添加了 sealed 之后,该类成为密封类,必须使用 permits 关键字指定哪些子类可以继承我。
sealed class Person permits Teacher, Student, Worker {}

// 当一个类继承了密封类之后,该类必须使用 final、sealed、non-sealed 这三个关键字中的一个关键字进行修饰。
// final 最终的
// sealed 密封的
// non-sealed 非密封的
final class Teacher extends Person {}
non-sealed class Student extends Person {}
sealed class Worker extends Person permits OtherWorker {}
non-sealed class OtherWorker extends Worker{}
//class T5 extends Person{}

// 密封接口
// 只允许A B C三个类实现这个接口
sealed interface MyInterface permits A,B,C {}
final class A implements MyInterface{}
non-sealed class B implements MyInterface{}
sealed class C implements MyInterface permits D{}
non-sealed class D extends C {}
//class E implements MyInterface{}


// Record 和 Sealed的关系。
sealed interface ProductService permits Product {}

// Record类型默认被final修饰。
// Record类型可以做密封接口的实现类。
record Product(String name, double price) implements ProductService { }

13.5 API层面的变化

13.5.1 String存储结构改变

在Java8及其之前,String底层采用char类型数组来存储字符;在Java9及其以后,String底层采用byte类型的数组来存储字符。将char[]转化为byte[],其目的就是为了节约存储空间。
图片9.png

13.5.2 String 新增的方法

在Java11版本中,对String类新增了一些方法,新增的方法如下:

// 空格,制表符,换行等都认为是空的
boolean blank = "\t \n".isBlank();
System.out.println(blank); // 输出:true

String source = "\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000www.baidu.com\u3000\u3000\u3000\u3000\u3000";
// 去除“前后”的中文空格
System.out.println(source.strip());
// 去除“开头”的中文空格
System.out.println(source.stripLeading());
// 去除“末尾”的中文空格
System.out.println(source.stripTrailing());

// 把字符串内容重复n份
String repeat = "xixi".repeat(3);
System.out.println(repeat); // 输出:xixixixixixi

// 按照换行来分割字符串,返回的结果是Stream对象
Stream<String> lines = "a\nb\nc\n".lines();
System.out.println(lines.count()); // 输出:3

在Java12版本中,对String类新增了一些方法,新增的方法如下:

// 在字符串前面添加n个空格
String result2 = "Java Golang".indent(4);
System.out.println(result2);

import java.util.stream.Stream;

/**
 * ClassName: NewStringTest
 * Description:
 * Datetime: 2024/2/19 15:21
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class NewStringTest {
    public static void main(String[] args) {
        // 1. 在java9之后(包括java9在内),String启用了压缩机制,以前是char[],变成了 byte[] 数组。

        // 2. java11新增了 isBlank() 方法,判断某个字符串是否为空,“制表符,空格,换行符”统一都被认为是空字符串。
        System.out.println("\t      \n".isBlank()); // true

        // 3. java11新增了 strip()、stripLeading()、stripTrailing() 去除所有类型的空格。包括中文空格。
        // 4. java11新增了 repeat() 方法
        String repeat = "haha".repeat(3);
        System.out.println(repeat);

        // 5. java11新增了 lines() 方法,将字符串以换行符进行分割,生成一个流对象
        Stream<String> stream = "abc\ndef\nxyz\n".lines();
        //stream.forEach(System.out::println);
        System.out.println("行数:" + stream.count());

        // 6. java12新增了 indent() 方法
        String str = "hello world".indent(4);
        System.out.println(str);

    }
}

13.5.3 接口支持私有方法

在Java8版本中,接口中支持“公开”的静态方法和公开的默认方法;在Java9版本中,接口中还允许定义“私有”的静态方法和成员方法,但是不能定义私有的默认方法。
【示例】演示接口中的私有静态方法和成员方法

/**
 * 接口(JDK1.9)
 */
public interface Flyable {
    // 私有的静态方法
    private static void staticMethod() {
        System.out.println("static method ...");
    }
    // 私有的成员方法
    private void method() {
        System.out.println("default method ...");
    }
}

13.5.4 标识符命名的变化

在Java8及其之前,标识符可以独立使用“_”来命名。

String _ = "hello";
System.out.println(_);

但是,在Java9中规定“_”不能独立命名标识符了,如果使用就会报错:
图片10.png

13.5.5 简化编译运行程序

在我们的认知里面,要运行一个Java源代码必须先编译(javac命令),再运行(java命令),两步执行动作。而在Java 11版本中,通过一个java命令就直接搞定了。
需要执行的程序:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

执行java命令进行运行,如下图所示:
图片11.png

13.5.6 创建不可变集合

在Java9版本中,我们可以通过List、Set和Map接口提供的of(E… elements)静态方法来创建不可变集合。通过此方式创建的不可变集合,我们不但不能添加或删除元素,并且还不能修改元素。
【示例】创建不可变集合

// 创建不可变List集合
List<Integer> list = List.of(1, 2, 3, 4, 5);
System.out.println(list);
// 创建不可变Set集合
// 注意:如果Set集合中有相同的元素,则就会抛出IllegalArgumentException异常。
Set<Integer> set = Set.of(1, 2, 3, 4, 5, 4);
System.out.println(set);
// 创建不可变Map集合
Map<Integer, String> map = Map.of(123, "武汉", 456, "成都");
System.out.println(map);

Arrays.asList与List.of的区别:
List.of:不能向集合中添加或删除元素,也不能修改集合中的元素。
Arrays.asList:不能向集合中添加或删除元素,但是可以修改集合中的元素。

【示例】Arrays.asList与List.of的区别

// 通过Arrays.asList()方法创建不可变集合
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
// list1.add(6); // 抛出UnsupportedOperationException异常
// list1.remove(2); // 抛出UnsupportedOperationException异常
list1.set(2, 33); // 没有问题
System.out.println(list1); // 输出:[1, 2, 33, 4, 5]

// 通过List.of()方法创建不可变集合
List<Integer> list2 = List.of(1, 2, 3, 4, 5);
// list2.add(6); // 抛出UnsupportedOperationException异常
// list2.remove(2); // 抛出UnsupportedOperationException异常
// list2.set(2, 33); // 抛出UnsupportedOperationException异常

import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * ClassName: ReadOnlyCollection
 * Description:
 *          创建只读集合(不可变集合)。
 *          这种集合不可以添加元素/修改元素/删除元素。
 *
 * Datetime: 2024/2/19 15:32
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class ReadOnlyCollection {
    public static void main(String[] args) {
        // 创建一个不可变的集合对象
        List<Integer> list = List.of(1, 2, 3, 4, 5);
        System.out.println(list);

        // 以下的增删改,都是不支持的。
        //list.add(100);
        //list.remove(0);
        //list.set(1, 100);

        // 创建一个不可变的Set集合对象
        Set<String> set = Set.of("abc", "def", "xyz");
        System.out.println(set);

        // Arrays.asList() 这个方法也可以返回一个只读的集合
        List<Integer> list1 = Arrays.asList(100, 200, 300);
        System.out.println(list1);

        // 不可以
        //list1.add(600);
        // 不可以
        //list1.remove(0);

        // 可以(这个就是一个小区别 和 List.of)
        list1.set(0, 666);
        System.out.println(list1);
    }
}

13.5.7 Optional API

在Java8以前,Java程序员操作对象时,为了避免错误引用null造成的空指针异常,往往需要一系列繁杂冗余的判空操作,增加了许多重复代码,降低了代码可读性,于是Java 8引入Optional类,优雅简洁的对null值进行处理,从而避免出现空指针异常(NullPointerException)。
本质上,Optional 类是一个包含有可选值的包装类,这意味着 Optional 类中既可以含有对象也可以为null。


import java.util.Optional;

/**
 * ClassName: OptionalAPI
 * Description:
 * Datetime: 2024/2/19 15:38
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class OptionalAPI {
    public static void main(String[] args) {
        // Optional API是Java8引入的。(专门做空处理的。避免空指针异常的)
        //有一场商业表演,原计划让“刘亦菲”来表演,如果“刘亦菲”不能参加,则就换“佟丽娅”来表演
        //String name = "刘亦菲";
        /*String name = null;
        if(name == null) {
            name = "佟丽娅";
        }
        System.out.println("最终参与商业表演的演员是:" + name);*/

        // 以上代码采用Optional API可以代替。
        String name = null;
        // 获取 Optional 对象(将可能为空的name封装成Optional对象)
        Optional<String> optional = Optional.ofNullable(name);
        // 原理:
        // 如果 optional 对象中封装的数据是 null,则选择 参数 作为返回值。
        // 如果 optional 对象中封装的数据不是 null,则选择 封装的数据 作为返回值。
        String finalName = optional.orElse("佟丽娅");
        System.out.println("最终参与商业表演的演员是:" + finalName);
    }
}
13.5.7.1 创建Optional对象

使用Optional类提供的of()和ofNullable() 静态方法来创建包含值的Optioanal实例。
如果将null当作参数传进去of()会抛出空指针异常,如果将null当作参数传进去 ofNullable() 就不会抛出空指针异常。
因此当对象可能存在或者不存在,应该使用 ofNullable()方法来创建Optional实例。
【示例】创建一个Optional实例

// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
13.5.7.2 Optional类的方法

想要获得Optional实例中包含的值,那么就可以使用以下两个方法来实现。

方法名描述
public T get()如果值不为null,则直接取出该值;如果值为null,则抛出空指针异常。
public T orElse(T other)如果值不为null,则直接取出该值;如果值为null,则取出的就是参数other的值。

开发中,我们获取Optional中存储的值,一般都是采用orElse(T other)方法来实现。
【示例】演示get()方法

// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
Object obj1 = optional1.get(); // 抛出空指针异常
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
String str = optional2.get();
System.out.println(str); // 输出:hello

【示例】演示orElse(T other)方法

// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
Object str1 = optional1.orElse("world");
System.out.println(str1); // 输出:world
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
String str2 = optional2.orElse("world");
System.out.println(str2); // 输出:hello
13.5.7.3 Optional的使用案例

需求:有一场商业表演,原计划让“刘亦菲”来表演,如果“刘亦菲”不能参加,则就换“佟丽娅”来表演,该需求的实现代码如下:

// 定义一个变量,用于保存表演者的名字
// String name = "刘亦菲"; // 原计划
String name = null; // 刘亦菲不能参加的情况
// 使用Optional来封装表演者的名字
Optional<String> optional = Optional.ofNullable(name);
// 获得实际参与表演对应人的名字
// 如果name的值为null,则就换为“佟丽娅”参与表演
String finalName = optional.orElse("佟丽娅");
// 输出实际表演者的名字
System.out.println(finalName);
  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开五档的蒙奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值