Java SE 06

目录

  • Java SE 06
  • 一、网络编程
  • 两种软件结构
  • 网络通信协议
  • 网络通信协议分类
  • IP地址
  • 端口号
  • TCP通信概述
  • TCP通信的客户端实现
  • TCP通信的服务器端实现
  • 文件上传C/S实例
  • 模拟B/S的服务器端实例
  • 二、函数式接口
  • 函数式接口的概念和定义
  • 函数式接口的作用
  • 常用的函数式接口
  • 三、Stream流
  • 流式思想概述
  • 从数组、集合中获取流
  • Stream流中的常用方法
  • 四、方法引用
  • 方法引用的概念
  • 通过对象名引用成员方法
  • 通过类名引用静态成员方法
  • 通过super引用父类成员方法
  • 通过this引用本类的成员方法
  • 类的构造方法引用
  • 数组的构造方法引用
  • 五、Junit
  • 测试概述
  • Junit使用步骤
  • 相关注解:@Before和@After
  • 六、反射
  • 类的加载过程和类加载器
  • 反射概述
  • 获取字节码Class对象的三种方式
  • Class对象功能(使用Class对象)
  • 反射案例
  • 七、Enum类和使用enum定义的枚举类
  • 枚举类概述
  • 自定义枚举类(使用普通类来模拟枚举类)
  • 使用enum关键字定义的枚举类
  • 枚举类与switch(xxx)
  • 枚举类的方法
  • 枚举类实现接口
  • 八、注解
  • 注解概述
  • JDK内置注解
  • 自定义注解
  • 解析注解
  • 注解案例

一、网络编程

两种软件结构

  • 软件结构
  1. C/S结构:全称Client/Server结构,是指客户端和服务器结构
    常见案例:迅雷、QQ、网盘
  2. B/S结构:全称Browser/Server结构,是指浏览器和服务器结构
  • 两种架构各有优势,但是无论是哪种架构,都离不开网络的支持
  • 网络编程:在一定的协议下实现两台设备通信

网络通信协议

  • TCP/IP协议:传输控制协议/因特网互联协议(Transmission Control Protocol/Internet Protocol)

网络通信协议分类

  • UDP:用户数据报协议(User Datagram Protocol),UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。当一台计算机向另一台计算机发送数据的时候,发送端不会确认接收端是否存在,就会发送数据;同样接收端在收到数据时,也不会向发送端反馈是否收到数据
  • UDP协议的特点:消耗资源小,通信效率高,通常用于视频、音频和普通数据的传输,因为这种场景下即使偶尔丢失一两个数据包,也不会对接收结果产生太大的影响

但是在使用UDP传输数据的时候,由于UDP是面向无连接的,不能保证数据的完整性,因此在传输重要数据时,不建议使用UDP协议

  • TCP协议:传输控制协议(Transmission Control Protocol),TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输
  • 在TCP连接中必须要明确客户端和服务器端,由客户端向服务器端发送连接请求,每次连接的创建都需要经过“三次握手”
    “三次握手”:TCP协议中,在发送数据的准备阶段,客户端和服务器之间的三次交互,以保证连接的可靠
  • 第一次握手:客户端向服务端发送连接请求,等待服务器确认
  • 第二次握手:服务端向客户端回送一个响应,通知客户端收到了连接请求
  • 第三次握手:客户端再次向服务端发送确认信息,确认连接

完成“三次握手”,连接建立之后,客户端和服务器就可以开始进行数据的传输了,由于这种面向连接的特性,TCP协议可以保证传输数据的安全,通常用于下载文件、浏览网页

IP地址

  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP
  • IP地址的分类
  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示为a.b.c.d,其中a,b,c,d都是0~255之间的十进制整数,最多可以表示42亿个
  • IPv6:采用128位地址长度,每16个字节一组,分成8组16进制数,每组16进制数可以表示0~65535,总共有65536^8个地址
  • 特殊的IP地址
    本机的IP地址:127.0.0.1localhost

端口号

  • 两台计算机进行通信,必须保证数据能准确无误的发送到对方的计算机软件上,使用IP地址加端口号,就可以保证数据准确无误的发送到对方计算机的指定软件上了
  • 端口号:是一个逻辑端口,无法直接看到,可以使用一些软件查看端口号
    当我们使用网络软件时,网络软件在打开的时候会向系统要指定的端口号,
    或者操作系统会为网络软件分配一个随机的端口号
  • 端口号是由两个字节组成的,取值范围是0~65535(2^16)之间

注意:

  1. 1024之前的端口号已经被系统分配给已知的的网络软件
  2. 网络软件的端口号不能重复
  • 常用的端口号:
  1. 80端口:网络端口(打开一个网址默认自动分配了80端口(若改成其他端口号,则无法访问),例如www.baidu.com:80)
  2. 数据库:
    mysql:3306
    oracle:1521
  3. Tomcat服务器:8080

TCP通信概述

  • TCP通信:是面向连接的通信,客户端和服务器端必须经过3次握手,建立逻辑连接,才能通信
  • 通信的步骤:
    服务器端先启动,服务器端不会主动请求客户端,必须使用客户端请求服务器端,客户端和服务器端就会建立一个逻辑连接,
    在这个连接中包含一个对象,这个对象就是IO对象,客户端和服务器端就可以使用IO对象进行通信,通信的数据不仅仅是字符,
    所以IO对象是字节流对象
  • 服务器端必须明确两件事情:
  1. 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行交互
    在服务器端有个方法accept客户端获取到请求的客户端对象
  2. 多个客户端同时和服务器进行交互,就需要使用多个IO流对象
    服务器端是没有IO流的,服务器可以获取到请求的客户端对象Socket,使用每个客户端Socket中提供的IO流和客户端交互

服务器使用客户端的字节输入流读取客户端发送的数据

服务器使用客户端的字节输出流给客户端回写数据

TCP通信的客户端实现

  • TCP通信的客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
  • 表示客户端的类:
  • java.net.Socket:此类实现客户端套接字。套接字是两台机器间通信的端点
    套接字:包含了IP地址和端口号的网络单位
  • 构造方法:
    Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号
    参数:
    String host:服务器主机的名称/服务器的IP地址
    int port:服务器的端口号
  • 成员方法:
    OutputStream getOutputStream() 返回此套接字的输出流
    InputStream getInputStream() 返回此套接字的输入流
    void close() 关闭此套接字
  • 实现步骤:
  1. 创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
  2. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
  3. 使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
  4. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
  5. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
  6. 释放资源(Socket)
  • 注意:
  1. 客户端和服务端进行交互,必须使用Socket中提供的网络流,不能使用自己创建的流对象
  2. 当创建客户端对象Socket的时候,就会去请求服务器和服务器经过3次握手建立连接通路
    这是服务器没有启动,就会抛出异常ConnectException
    如果服务器已经启动,就可以进行交互了
  • 示例Code
public class TCPClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 6666);

        OutputStream os = socket.getOutputStream();
        os.write("Hello, this is from  TCP client".getBytes());

        InputStream is = socket.getInputStream();
        byte[] arr = new byte[1024];
        int len = is.read(arr);
        System.out.println(new String(arr, 0, len));

        socket.close();
    }
}

TCP通信的服务器端实现

  • TCP通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
  • 表示服务器的类:
  • java.net.ServerSocket:此类实现服务器套接字
  • 构造方法:
    ServerSocket(int port) 创建绑定到特定端口的服务器套接字
  • 服务器端必须明确一件事情,必须知道是哪个客户端请求的服务器,所以可以使用accept方法获取到请求的客户端对象Socket
  • 成员方法:
    Socket accept() 侦听并接收到此套接字的连接
  • 实现步骤:
  1. 创建服务器ServerSocket对象和传入系统指定的端口号
  2. 使用ServerSocket对象中的方法accept,获取到请求的客户端对象socket
  3. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
  4. 使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
  5. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
  6. 使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
  7. 释放资源(Socket, ServerSocket)
  • 示例Code
public class TCPServer {
    public static void main(String[] args) throws IOException {
      ServerSocket serverSocket = new ServerSocket(6666);
        Socket socket = serverSocket.accept();

        InputStream is = socket.getInputStream();
        byte[] arr = new byte[1024];
        int len = is.read(arr);
        System.out.println(new String(arr, 0, len));

        OutputStream os = socket.getOutputStream();
        os.write("server side has received!".getBytes());

        socket.close();
        serverSocket.close();
    }
}

文件上传C/S实例

  • 客户端示例Code
public class FileClient {
    public static void main(String[] args) throws IOException {
        
 /*
上传本地文件到服务器端(与拷贝文件操作方法类似)
*/
        
        //创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream(new File("G:\\a.txt"));
        //创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 6565);
        //使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();

        //使用本地字节输入流对象中的read方法读取本地文件
        int len = 0;
        byte[] arr = new byte[1024];
        while((len = fis.read(arr)) != -1) {
            //使用网络字节输出流对象中的方法把读取的文件上传到服务器上(写入)
            os.write(arr, 0, len);
        }
        //发送TCP的正常连接终止序列,防止在server端的InputStream的read阻塞
        socket.shutdownOutput();

/*
从服务器端获取发送的信息
*/     
        
        //使用Socket中的方法getInputStream,获取网络字节输入流的对象
        InputStream is = socket.getInputStream();
        //使用网络字节输入流对象中的read方法,把服务器端写入的内容读出
        while((len = is.read(arr)) != -1) {
            System.out.println(new String(arr, 0, len));
        }

/*
释放资源(只要释放主要的两个流对象fis、socket,不用释放衍生的对象os、is)
*/  
        fis.close();
        socket.close();
    }
}
  • 服务器端示例Code
public class FileServer {
    public static void main(String[] args) throws IOException {
      //创建一个服务器ServerSocket对象,和系统指定的端口号
        ServerSocket serverSocket = new ServerSocket(6565);
        //使用ServerSocket对象中的方法accept,获取到请求的客户端Scoket对象
        Socket socket = serverSocket.accept();

        //判断“G:\\test2”文件夹是否存在,不存在则创建
        File currentFile = new File("G:\\test2");
        if(!currentFile.exists()) {
            currentFile.mkdir();
        }

        //使用Socket对象中方法getInputStream,获取到网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();
        //创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
        FileOutputStream fos = new FileOutputStream(new File(currentFile, "\\b.txt"));
        int len = 0;
        byte[] arr = new byte[1024];
        while((len = is.read(arr)) != -1) {
            //使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘中
            fos.write(arr, 0, len);
        }

        //使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
        //使用网络字节输出流OutputStream对象中的方法write,给客户端回写信息
        OutputStream os = socket.getOutputStream();
        os.write("File Server side has finished save file!".getBytes());

        
        //释放资源(FileOutputStream, Socket,ServerSocket)
        fos.close();
        socket.close();
        serverSocket.close();
    }
}
  • 文件上传中服务器端的read方法的阻塞问题
  • 问题原因:read方法读取的内容是来自于client端OutputStream写入的内容,在这个过程中client就算写完所有的上传文件字节也不会写入文件结束标记符-1,在服务端中InputStream的read方法会接收不到读完的-1标记,所以一直死循环等待结束标记,这就是阻塞问题
  • 解决方法:在client端结束write时手动终止write
    使用Socket类的方法void shutdownOutput();

void shutdownOutput();禁用此套接字的输出流

对于TCP套接字,任何以前写入的数据都将被发送,并且后跟TCP的正常连接终止序列

  • 文件上传中服务端程序的优化(文件防止重名+循环接收+多线程提高效率)
public class FileServer2 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(6565);
        while(true) {
            Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {

                        File currentFile = new File("G:\\test2");
                        if(!currentFile.exists()) {
                            currentFile.mkdir();
                        }

                        String filename = "\\newImage" + System.currentTimeMillis() + new Random().nextInt(9999) + ".txt";
						//这里访问对象socket,相当于是局部内部类访问方法的局部变量
                        InputStream is = socket.getInputStream();//从Java8开始,只要局部变量事实不变,那么final关键字可以省略
                        FileOutputStream fos = new FileOutputStream(new File(currentFile, filename));
                        int len = 0;
                        byte[] arr = new byte[1024];
                        while((len = is.read(arr)) != -1) {
                            fos.write(arr, 0, len);
                        }

                        OutputStream os = socket.getOutputStream();
                        os.write("File Server side has finished save file!".getBytes());

                        fos.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

模拟B/S的服务器端实例

  • 服务器端循环接收+多线程+异常处理模板代码块
ServerSocket serverSocket = new ServerSocket(xxxx);

while(true) {
    
    Socket socket = serverSocket.accept();
    
    new thread(new Runnable(){
        @Override
        public void run() {
            try {
                
            } catch () {
                
            }
        }
    }).start();
}
  • 实例Code
//首先将browser的请求信息读取出来,经过字符串处理,在服务端的硬盘上读出并写入网络字节输出流(返回html文件给browser)
//浏览器解析服务器回写的html页面,页面中如果有图片,那么浏览器会单独开启一个线程,读取服务器的图片,
//我们就让服务器一直处于监听的状态,客户端请求一次,服务器就回写一次

//浏览器输入本地的路径,是从idea当前项目的根目录开始
//http://127.0.0.1:8020/BHJavaDevelopment/src/JavaAdvance/Day11NetWorks/web/index.html
public class BSServer2 {
    public static void main(String[] args) throws IOException {
        //创建一个服务器ServerSocket,和系统要指定的端口号
        ServerSocket serverSocket = new ServerSocket(8020);

        while(true) {
			//用accept方法获取到请求的客户端(浏览器)对象
            Socket socket = serverSocket.accept();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
                        InputStream is = socket.getInputStream();
						//把is网路字节输入流对象,转换为字符缓冲输入流
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));
                        //把客户端请求信息的第一行读出来
                        String firstLine = br.readLine();
                        //把读取出来的字符串切割只要中间的部分
                        String[] strArr = firstLine.split(" ");
                        //把路径前的/去掉,只保留后面的部分
                        String objPath = strArr[1].substring(1);
                        System.out.println(firstLine);
						//使用Socket中的getOutputStream获取网络字节输出流
                        OutputStream os = socket.getOutputStream();
                        //写入HTTP协议响应头,这个是固定的写法
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type:text/html\r\n".getBytes());
                        //必要时写入空行,否则不解析
                        os.write("\r\n".getBytes());

						//创建一个本地字节输入流,构造方法中绑定要读取的html路径
                        FileInputStream fis = new FileInputStream(objPath);
                        int len = 0;
                        byte[] arr = new byte[1024];
                        while((len = fis.read(arr)) != -1) {
                            os.write(arr, 0, len);
                        }
						//释放资源
                      fis.close();
                        socket.close();
                        
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }
}

二、函数式接口

函数式接口的概念和定义

  • 函数式接口在Java中是指:有且仅有一个抽象方法的接口
  • 函数式接口,适用于函数式编程场景的接口,Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口,只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导
  • “语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,for-each这就是一种语法糖。从应用层面讲,Java中的Lambda可以被当作是匿名内部类的语法糖,但是二者在原理上是不同的
  • 函数式接口的格式:
    确保接口中只有一个抽象方法即可
修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(方法参数)
    //其他的非抽象方法内容
}
  • 函数式接口:有且仅有一个抽象方法的接口,称之为函数式接口
    接口中可以包含其他的方法(默认,静态,私有)
  • 使用@FunctionalInterface注解
    作用:可以检测接口是否是一个函数式接口
    是:编译成功
    否:编译失败(接口中没有抽象方法,抽象方法的个数多于1个)
//加了FunctionalInterface注解,接口中没有抽象方法或者抽象方法有多个就会报错

@FunctionalInterface
public interface Demo01 {
    //这两个抽象方法同时只能存在一个,否则报错
    public abstract void method01();
    //void method02();//接口中的抽象方法可以省略public abstract
}

函数式接口的作用

  • 函数式接口的使用:一般可以作为方法的参数和返回值类型
  • 函数式接口作为方法的参数
@FunctionalInterface
public interface MyFunctionalInterface {
    public abstract void printInfo();
}
public class MyFunctionalInterfaceImpl implements MyFunctionalInterface{
    @Override
    public void printInfo() {
        System.out.println("Method 1");
    }
}
public class Demo02 {
    public static void main(String[] args) {
        //1.直接使用接口实现类的对象作为参数传入
        printInfo(new MyFunctionalInterfaceImpl());
		//2.传入接口的匿名内部类对象
        printInfo(new MyFunctionalInterface() {
            @Override
            public void printInfo() {
                System.out.println("method 2");
            }
        });
		//3.传入使用Lambda表达式重写接口中的方法(Lambda的返回值是接口类型的对象)
        printInfo(() -> {
            System.out.println("method 3");
        });
		//4.传入使用Lambda表达式的结果简化版
        printInfo(() -> System.out.println("method 4"));
    }

    public static void printInfo(MyFunctionalInterface mfi) {
        mfi.printInfo();
    }
}
/*output:
Method 1
method 2
method 3
method 4
*/

另一个案例

public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" starts!");
    }
}
//Runnable是一个函数式接口
public class Demo01 {
    public static void main(String[] args) {
        //使用实现类的对象
        MyThread(new RunnableImpl());
		//使用匿名内部类对象
        MyThread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" starts!");
            }
        });
		//使用Lambda
        MyThread(() -> System.out.println(Thread.currentThread().getName()+" starts!"));
    }

    public static void MyThread(Runnable runnable) {
        new Thread(runnable).start();
    }
}
  • 使用函数式接口作为方法参数用于日志打印的优化案例
  • Lambda的延迟执行:有些场景的代码执行后,结果不一定会被使用,从而造成性能的浪费。而Lambda表达式是延迟执行的,正好可以提升性能
  • Lambda的特点:延迟加载
    Lambda的使用前提:必须存在函数式接口
  • 打印日志的原版:要先完成字符串拼接,传入参数判断若不是匹配的level,则这次的字符串拼接就浪费了
public class OriginalVersion {
    public static void main(String[] args) {
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "War II";

        printLog(1, msg1 + msg2 + msg3);
        printLog(2, msg1 + msg2 + msg3);
        printLog(3, msg1 + msg2 + msg3);
    }

    public static void printLog(int level, String msg) {
        if(level == 1) {
            System.out.println(msg);
        }
    }
}
  • 使用Lambda的改进版本:先传入接口类型的对象,若level匹配,就调用接口类型对象的方法
@FunctionalInterface
public interface makeString {
    public abstract String make();
}
public class LambdaVersion {
    public static void main(String[] args) {
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "War II";

        printLog(1, () -> msg1+msg2+msg3);
        printLog(2, () -> msg1+msg2+msg3);
        printLog(3, () -> msg1+msg2+msg3);
    }

    public static void printLog(int level, makeString ms) {
        if(level == 1) {
            System.out.println(ms.make());
        }
    }
}
  • 函数式接口作为方法的返回值
  • 如果一个方法的返回值类型是一个函数式接口,那么就可以返回一个Lambda表达式
    当需要通过一个方法来获取java.util.Comparator接口类型的对象作为排序器时,就可以调用该方法
  • 示例Code
public class Demo01 {
    public static void main(String[] args) {
        String[] arr = new String[] {"aa", "vvvv", "b"};
        System.out.println(Arrays.toString(arr));
        Arrays.sort(arr, getComparator1());
        //Arrays.sort(arr, getComparator2());
        System.out.println(Arrays.toString(arr));
    }
    //定义一个方法,方法的返回值类型使用函数式接口Comparator
    public static Comparator<String> getComparator1() {
        //方法的返回值类型是一个接口,那么我们就可以返回这个接口的匿名内部类
        return new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                //按照字符串长度的降序排序
                return o2.length() - o1.length();
            }
        };
    }
	//方法的返回值类型是一个函数式接口,可以返回一个Lambda表达式
    public static Comparator<String> getComparator2() {
        return ((o1, o2) -> o2.length() -o1.length());
    }
}
/*output:
[aa, vvvv, b]
[vvvv, aa, b]
*/

常用的函数式接口

  • java.util.function.Supplier
  • 该接口仅包含一个无参的方法:T get()用来获取一个泛型参数指定类型的对象数据
  • Supplier接口被称之为生成型接口,指定接口的泛型是什么类型,那么接口中的get方法就会产生什么数据类型
  • 示例Code
public class Demo03 {
    public static void main(String[] args) {
        //匿名内部类中重写Supplier<T>中的get方法
        String str = new Supplier<String>() {
            @Override
            public String get() {
                return "Hello 01";
            }
        }.get();
        System.out.println(str);
		//使用Lambda重写Supplier<T>中的get方法
        Supplier<String> supplier = () -> "Hello 02";
        System.out.println(supplier.get());
    }
}
/*output:
Hello 01
Hello 02
*/
  • 案例:获取数组中的最大值
public class Demo04 {
    public static void main(String[] args) {
        int[] arr = new int[] {11, 34, -34, 24, 101, 55};
		//匿名内部类中重写Supplier<T>中的get方法
        int maxNum = getMax(new Supplier<Integer>() {
            int max = arr[0];
            @Override
            public Integer get() {
                for(int i : arr) {
                    if(i > max) {
                        max = i;
                    }
                }
                return max;
            }
        });
        System.out.println("maxNum of arr is " + maxNum);

        System.out.println("==========================");
		//使用Lambda重写Supplier<T>中的get方法
        int maxNum2 = getMax(()-> {
            int max = arr[0];
            for(int i : arr) {
                if(i > max) {
                    max = i;
                }
            }
            return max;
        });
        System.out.println("maxNum of arr is " + maxNum2);
    }

    public static int getMax(Supplier<Integer> supplier) {
        return supplier.get();
    }
}
/*output:
maxNum of arr is 101
==========================
maxNum of arr is 101
*/
  • java.util.function.Consumer
  • 这个接口正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定
  • Consumer接口中包含抽象方法void accept(T t),用来消费一个指定的泛型数据
  • Consumer接口是一个消费型接口,泛型执行什么类型,就可以使用accept方法消费什么类型的数据,至于怎么消费(使用),需要自定义(输出、计算......)
  • 示例Code
public class Demo05 {
    public static void main(String[] args) {
    consStr("Hello", (str) -> System.out.println(str));

        consStr("World", (str) -> {
            String newStr = new StringBuilder(str).reverse().toString();
            System.out.println(newStr);
        });
    }

    public static void consStr(String str, Consumer<String> consumer) {
        consumer.accept(str);
    }
}
/*output:
Hello
dlroW
*/
  • Consumer接口的默认方法andThen
  • 作用:需要两个Consumer接口,可以把两个Consumer接口组合在一起,对数据进行消费
  • 举例:
//两个Consumer接口,依次执行accept方法
Consumer<String> con1;
Consumer<String> con2;
String s = "hello";
con1.accept(s);
con2.accept(s);

//andThen方法:连接两个Consumer接口,再进行消费
//使用andThen默认方法,写在前面的先被消费
con1.andThen(con2).accept(s);
  • 示例Code
public class Demo06 {
    public static void main(String[] args) {
        updateStr("Hello World", (str) -> {
            String newStr = str.toLowerCase();
            System.out.println(newStr);
        }, (str) -> {
            String newStr = str.toUpperCase();
            System.out.println(newStr);
        });


    }

    public static void updateStr(String str, Consumer<String> consumer1, Consumer<String> consumer2) {
//        consumer1.accept(str);
//        consumer2.accept(str);

        //使用andThen可替换上面两句
        consumer1.andThen(consumer2).accept(str);
    }
}
  • 拼接字符串案例
//将给定字符串数组的内容按"name is xxx.gender is xxx."格式拼接并打印
public class Demo07 {
    public static void main(String[] args) {
        String[] arr = new String[] {"Tom, male", "Lucy, female", "Jeff, male"};

        for(String str : arr) {
            makeStr(str, (username) -> {
                String name = str.split(",")[0];
                System.out.print("name is " + name + ".");
            }, (userGender) -> {
                String gender = str.split(" ")[1];
                System.out.println("gender is " + gender + ".");
            });
        }
    }

    public static void makeStr(String str, Consumer<String> consumer1, Consumer<String> consumer2) {
        consumer1.andThen(consumer2).accept(str);
    }
}
/*output:
name is Tom.gender is male.
name is Lucy.gender is female.
name is Jeff.gender is male.
*/
  • java.util.function.Predicate接口
  • 作用:对某种数据类型的数据进行判断,结果返回一个boolean值
  • Predicate接口中包含一个抽象方法
    boolean test(T t)用来对指定数据类型数据进行判断的方法
    符合条件:返回true
    不符合条件:返回false
  • 示例Code
public class Demo08 {
    public static void main(String[] args) {
        boolean result = checkString("Hello", (str) -> str.length() > 5);
        System.out.println(result);
    }

    public static boolean checkString(String str, Predicate<String> predicate) {
       return predicate.test(str);
    }
}
/*output:
false
*/
  • Predicate接口中有一个方法and,表示并且关系,也可以用于连接两个判断条件
  • 在方法内部的两个判断条件,也是使用&&运算符连接起来的
default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> this.test(t) && other.test(t);
}
  • 示例Code
//判断一个字符串是否长度大于5并且包含"a"
public class Demo09 {
    public static void main(String[] args) {
        boolean result = checkString("Helloa", (str) -> str.length() > 5, (str) -> str.contains("a"));
        System.out.println(result);
    }

    public static boolean checkString(String str, Predicate<String> predicate1, Predicate<String> predicate2) {
        //使用and方法等同于下面一句的写法
        return predicate1.and(predicate2).test(str);
        //return predicate1.test(str) && predicate2.test(str);
    }
}
/*output:
true
*/
  • Predicate接口中有一个方法or,表示或者关系,也可以用于连接两个判断条件
  • 在方法内部的两个判断条件,也是使用||运算符连接起来的
default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> this.test(t) || other.test(t);
}
  • 示例Code
//判断一个字符串是否长度大于5或者包含"a"
public class Demo10 {
    public static void main(String[] args) {
        boolean result = checkString("Hallo", (str) -> str.length() > 5, (str) -> str.contains("a"));
        System.out.println(result);
    }

    public static boolean checkString(String str, Predicate<String> predicate1, Predicate<String> predicate2) {
        //使用or方法等同于下面一句的写法
        return predicate1.or(predicate2).test(str);
        //return predicate1.test(str) || predicate2.test(str);
    }
}
/*output:
true
*/
  • Predicate接口中有一个方法negate,表示取反
  • 内部实现
default Predicate<T> negate() {
    return (t) -> !test(t);
}
  • 示例Code
//判断一个字符串长度是否大于5,若大于5返回false,小于等于返回5返回true
public class Demo11 {
    public static void main(String[] args) {
        boolean result = checkString("Hello", (str) -> str.length() > 5);
        System.out.println(result);
    }

    public static boolean checkString(String str, Predicate<String> predicate) {
        //使用negate方法等同于下面一句的写法
        return predicate.negate().test(str);
        //return !predicate.test(str);
    }
}
/*output:
true
*/
  • 信息筛选案例
  • 示例Code
//筛选名字长度大于3且性别为男用户,add进入集合,将集合遍历打印
public class Demo12 {
    public static void main(String[] args) {
        String[] arr = new String[] {"Jeff,male", "Mike,male", "Lucy,female"};

        ArrayList<String> list = checkString(arr, (str) -> str.split(",")[0].length() > 3, (str) -> str.split(",")[1].equals("male"));

        for(String ele : list) {
            System.out.println(ele);
        }
    }

    public static ArrayList<String> checkString(String[] arr, Predicate<String> predicate1, Predicate<String> predicate2) {
        ArrayList<String> list = new ArrayList<>();

        for(String str : arr) {
            if(predicate1.and(predicate2).test(str)) {
                list.add(str);
            }
        }

        return list;
    }
}
/*output:
Jeff,male
Mike,male
*/
  • java.util.function.Function<T, R>
  • 作用:接口用来根据一个类型的数据得到另一个类型的数据
    前者为前置条件,后者为后置条件
  • Function接口中最主要的抽象方法为R apply(T t),根据类型T的参数获取类型R的结果
    使用场景:将String类型转换为Integer类型
  • 示例Code
public class Demo13 {
    public static void main(String[] args) {
        changeValue("9856", (str) -> Integer.parseInt(str));
    }

    public static void changeValue(String str, Function<String, Integer> function) {
        int num = function.apply(str);
        System.out.println(num);
    }
}
/*output:
9856
*/
  • Function接口中的默认方法andThen:用来进行组合操作
  • 使用方法:
    定义两个Function<T, R> func1, Function<R, T> func2
    func1.andThen(func2).apply(T t)
    func1先调用apply方法,把T转换为R
    func2再调用apply方法,把R转换为T
  • 示例Code
//将字符串转换为整型数字再加10后,转换为字符串
public class Demo14 {
    public static void main(String[] args) {
        changeValueType("155", (str) -> Integer.parseInt(str) + 10, (num) -> num + "" + 13);
    }

    public static void changeValueType(String str, Function<String, Integer> function1, Function<Integer, String> function2) {
        String newStr = function1.andThen(function2).apply(str);
        System.out.println(newStr);
    }
}
/*output:
16513
*/
  • andThen连续使用案例
//先取出字符串中的数字部分,将字符串转换为对应的数字,将数字加100
public class Demo15 {
    public static void main(String[] args) {
        changeValue("Jeff,45", (str) -> str.split(",")[1], (str) -> Integer.parseInt(str), (num) -> num + 100);
    }

    public static void changeValue(String str, Function<String, String> function1, Function<String, Integer> function2, Function<Integer, Integer> function3) {
        int num = function1.andThen(function2).andThen(function3).apply(str);
        System.out.println(num);
    }
}
/*output:
145
*/

三、Stream流

流式思想概述

  • 流式思想类似于工厂车间的“生产流水线”
  • 当需要对多个元素进行操作的时候(多个步骤:filter, map, skip, count),考虑到性能和便利性,我们应该首先拼好一个模型步骤方案,然后再按照方案去执行它
  • Stream流是一个集合元素的函数模型,它并不是集合,也不是数据结构,本身不存储任何的元素(或其地址值)
  • Stream流有两个基础的特征:
  1. Pipelining:中间的操作都会返回流对象本身。
  2. 内部迭代:以前对集合的遍历都是通过Iterator或者增强for的方式,显式的在集合外部进行迭代,这叫外部迭代。Stream提供了内部迭代的方式,流可以直接调用遍历方法

从数组、集合中获取流

  • java.util.stream.Stream是Java 8新加入的最常用的流接口(并不是一个函数式接口)
  • 获取流的两种方式:
  1. 所有的Collection集合都可以通过默认方法stream获取流
    default Stream<E> stream()
public class Demo01 {
    public static void main(String[] args) {
        //List和Set可以直接通过默认方法stream获取流
        List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        Stream<String> stream2 = list.stream();

        //Map不是Collection,需要间接获取流,通过keySet
        Map<String, Integer> map = new HashMap<>();
        Set<String> keySet = map.keySet();
        Stream<String> stream3 = keySet.stream();
		//Map不是Collection,需要间接获取流,通过values
        Collection<Integer> values = map.values();
        Stream<Integer> stream4 = values.stream();
		//Map不是Collection,需要间接获取流,通过entrySet
        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
        Stream<Map.Entry<String, Integer>> stream5 = entrySet.stream();
    }
}
  1. Stream接口的静态方法of可以获取数组对应的流
    static <T> Stream<T> of (T...values)参数是一个可变参数,我们就可以传递一个数组
public class Demo03 {
    public static void main(String[] args) {
        Stream<Integer> stream1 = Stream.of(0, 1, 3, 5);

        Integer[] arr = new Integer[] {2, 4, 6, 8};
        Stream<Integer> stream2 = Stream.of(arr);
        
        String[] arr2 = new String[] {"a", "b", "c", "d"};
        Stream<String> stream3 = Stream.of(arr2);
    }
}

Stream流中的常用方法

  • Stream流中常用的方法分为两种:
  • 延迟方法:返回值仍然是Stream接口自身类型的方法,因此支持链式调用(除了终结方法外其余的方法均为延迟方法)
  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用
  • 遍历:forEach方法
  • void forEach(Consumer<? super T> action)该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理,传递Lambda表达式,消费数据
  • java.util.function.Consumer<T>接口是一个消费型接口
    Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据
  • forEach方法作用:用来遍历流中的数据
    forEach是一个终结方法(返回值为void),遍历之后就不能继续调用Stream流中的其他方法
  • 示例Code
//使用Stream流中的方法forEach对Stream流中数据进行遍历
public class Demo04 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Baidu", "Alibaba", "Tencent");
        stream.forEach(name -> System.out.println(name));
    }
}
/*output:
Baidu
Alibaba
Tencent
*/
  • 错误的情况:流只能使用一次
  • Stream流属于管道流,只能被使用一次,第一个流调用方法完毕后,数据就会流转到下一个Stream流上,这时第一个Stream流已经使用完毕,就会关闭了,所以第一个Stream流就不再能调用方法了
  • 示例Code
public class Demo04 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Baidu", "Alibaba", "Tencent");
        stream.forEach(name -> System.out.println(name));
        //Stream流属于管道流,第一个流使用完后会关闭,若需要继续使用,
        //就将第一个流的方法返回值保存到另一个同一泛型的流中
        //而forEach返回值为void,所以是一个终结方法,这种情况下遍历之后就不能继续调用Stream流中的其他方法
        stream.forEach(name -> System.out.println(name));
    }
}
/*output:
Exception in thread "main" java.lang.IllegalStateException: 
stream has already been operated upon or closed
*/
  • 过滤:filter方法
  • Stream<T> filter(Predicate<? super T> predicate) filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤
    Predicate中的抽象方法:boolean test(T t)
  • 示例Code
public class Demo05 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Baidu", "Alibaba", "Tencent");
        //Stream流属于管道流,第一个流使用完后会关闭,若需要继续使用,就将第一个流的方法返回值保存到另一个同一泛型的流中
        Stream<String> stream1 = stream.filter(name -> name.startsWith("T"));
        stream1.forEach(name -> System.out.println(name));
    }
}
/*output:
Tencent
*/
  • 映射:map方法
  • <R> Stream<R> map(Function<? super T, ? extends R> mapper)该接口需要一个Function函数式接口参数,可以将当前流中的T类型的数据转换为另一种R类型的流
    Function中的抽象方法:
    R apply(T t);
  • 示例Code
//将数组中的字符串转换为数字,遍历打印
public class Demo06 {
    public static void main(String[] args) {
        //获取一个String类型的Stream流
        Stream<String> stream = Stream.of("34", "45", "34", "23");
        //使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数
        Stream<Integer> stream1 = stream.map((str) -> Integer.parseInt(str));
        //遍历新的Stream流
        stream1.forEach(num -> System.out.println(num));
    }
}
/*output:
34
45
34
23
*/
  • 示例Code2
    目标:将字符串类型Stream流转换为自定义类型Person的Stream流,并遍历打印
public class Person {
    private int age;
    private String name;

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

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

    public Person() {
    }

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

}
public class Demo11 {
    public static void main(String[] args) {
        Stream<String> stream1 = Stream.of("Tom", "Jeff", "Iris");
        Stream<Person> stream2 = stream1.map(str -> new Person(str));
        stream2.forEach(name -> System.out.println(name));
    }
}
/*output:
Person{age=0, name='Tom'}
Person{age=0, name='Jeff'}
Person{age=0, name='Iris'}
*/
  • 计算元素个数:count方法
  • long count()count方法是一个终结方法,返回值是一个long类型的整数,所以不能再调用Stream流中其他的方法了
  • 作用:用于统计Stream流中元素的个数
  • 示例Code
public class Demo07 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("a", "b", "c", "d");
        long num = stream.count();
        System.out.println(num);
    }
}
/*output:
4
*/
  • 截取流中前几个元素:limit
  • Stream<T> limit(long maxSize)
  • 参数是一个long类型的。如果集合当前的长度大于参数则进行截取,否则不进行操作
  • limit方法是一个延迟方法,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用Stream流中的其他方法
  • 作用:对流进行截取,只取前n个
  • 示例Code
public class Demo08 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(33);
        list.add(33);
        Stream<Integer> stream1 = list.stream();
        Stream<Integer> stream2 = stream1.limit(5L);
        stream2.forEach(num -> System.out.println(num));
    }
}
/*output:
1
1
2
2
33
*/
  • 跳过流中的前几个元素:skip
  • Stream<T> skip(long n)如果流的当前长度大于参数n,则跳过前n个。否则会得到一个长度为0的空流
  • 如果希望跳过前几个元素,可以使用skip方法获取一个截取后的新流
  • 示例Code
public class Demo09 {
    public static void main(String[] args) {
        Stream<Integer> stream1 = Stream.of(1, 2, 11, 22, 111, 222);
        Stream<Integer> stream2 = stream1.skip(3L);
        stream2.forEach(num -> System.out.println(num));
    }
}
/*output:
22
111
222
*/
  • 流的合成:concat
  • static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)这是一个静态方法,与java.long.String中的concat方法是不同的
  • 作用:如果有两个流,希望合并成一个流,那么可以使用Stream接口的静态方法concat
  • 示例Code
public class Demo10 {
    public static void main(String[] args) {
        Stream<String> stream1 = Stream.of("ice", "cream", "coca", "cola");
        List<String> list = new ArrayList<>();
        list.add("herb");
        list.add("meat");
        list.add("gold");
        list.add("axe");
        Stream<String> stream2 = list.stream();
        Stream<String> stream3 = Stream.concat(stream1, stream2);
        stream3.forEach(str -> System.out.println(str));
    }
}
/*output:
ice
cream
coca
cola
herb
meat
gold
axe
*/

四、方法引用

方法引用的概念

  • Lambda表达式的作用是明确某个接口的方法是做什么的(重写里面的方法),制定好要实现的方案(方法需要参数就传形参,处理这个形参)
  • 双冒号::引用运算符,它所在的表达式是方法引用。如果Lambda表达式中要表达的函数方案已经存在于某个方法实现中了,那么则可以通过双冒号来引用该方法作为Lambda的替代者
  • 举例:System.out对象中有一个重载的println(String)方法,这个方法恰好是Lambda表达式中需要让被重写的抽象方法也表现这种效果的方法,对于需要Lambda表达式使用的地方可以有两种写法:
  1. Lambda表达式的写法:s -> System.out.println(s)拿到参数后经过Lambda表达式,传递给System.out.println方法来处理
  2. 方法引用的写法:System.out::printlnSystem.out中的println取代Lambda表达式,这种方式复用了已有的方法,写法更加简洁,还可以省略形式参数

注意:方法引用格式,::之后的方法只写方法名,不写参数列表

  • 给需要的某种接口类型的参数传一个方法引用相当于是传了一个Lambda表达式同样相当于是传了一个匿名内部类对象

通过对象名引用成员方法

  • 通过对象名引用成员方法,使用前提是对象名是已经存在的,成员方法也是已经存在的,就可以使用对象名来引用成员方法
  • 示例Code
public class Demo12 {
    public void stringToLowercase(String str) {
        System.out.println(str.toLowerCase());
    }
}
@FunctionalInterface
public interface Demo13 {
    public void print(String str);
}
public class Demo14 {

    public static void printInfo(Demo13 demo13) {
        demo13.print("ICE CREAM");
    }

    public static void main(String[] args) {
        //Lambda表达式中,对象new Demo12()存在,成员方法stringToLowercase也存在,可以使用对象名引用成员方法优化Lambda
        printInfo(str -> new Demo12().stringToLowercase(str));

        printInfo(new Demo12() :: stringToLowercase);
    }
}
/*output:
ice cream
ice cream
*/

通过类名引用静态成员方法

  • 通过类名引用静态成员方法,类已经存在,静态成员方法也已经存在,就可以通过类名直接引用静态成员方法
  • 示例Code
@FunctionalInterface
public interface Demo01 {
    public int calc(int num);
}
public class Demo02 {
    public static void main(String[] args) {
        //Lambda表达式中,类Math存在,静态方法abs也存在,可以使用类名引用静态方法优化Lambda
        int num1 = absGenerator(-29, num -> Math.abs(num));
        int num2 = absGenerator(-67, Math::abs);

        System.out.println(num1);
        System.out.println(num2);
    }

    public static int absGenerator(int num, Demo01 demo01) {
        return demo01.calc(num);
    }
}
/*output:
29
67
*/

通过super引用父类成员方法

  • 如果存在继承关系,当Lambda中需要出现super调用时,可以使用方法引用进行替代
  • super是已经存在的,父类成员方法也是存在的,所以可以直接使用super引用父类的成员方法
  • 示例Code
@FunctionalInterface
public interface Greetable {
    void greet();
}
public class Parent {
    public void printInfo() {
        System.out.println("Hello, this is Parent");
    }
}
public class Child extends Parent{
    @Override
    public void printInfo() {
        System.out.println("Hello, this is Child");
    }

    public void method(Greetable greetable) {
        greetable.greet();
    }

    public void show() {
        method(() -> super.printInfo());
        method(super::printInfo);
    }

    public static void main(String[] args) {
        new Child().show();
    }
}
/*output:
Hello, this is Parent
Hello, this is Parent
*/

通过this引用本类的成员方法

  • this代表当前对象,如果需要引用的方法就是当前类中的成员方法,可以使用this::成员方法的格式来使用方法引用
  • 示例Code
@FunctionalInterface
public interface Greetable {
    void greet();
}
public class Demo01 {
    public void printInfo() {
        System.out.println("Hello, this is Demo01");
    }

    public void method(Greetable greetable) {
        greetable.greet();
    }

    public void useMethod() {
        //this是已经存在的,本类成员方法printInfo也是存在的,可以使用方法引用优化Lambda表达式
        method(() -> this.printInfo());
        method(this::printInfo);
    }

    public static void main(String[] args) {
        new Demo01().useMethod();
    }
}
/*output:
Hello, this is Demo01
Hello, this is Demo01
*/

类的构造方法引用

  • 由于构造器的名称与类名完全一样,所以构造方法引用使用类名称::new的格式表示
  • 示例Code
public class Person {
    private String name;

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

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
@FunctionalInterface
public interface NameBuilder {
    public Person generateName(String str);
}
public class Demo01 {

    public static void method(NameBuilder nb, String str) {
        String name = nb.generateName(str).getName();
        System.out.println(name);
    }

    public static void main(String[] args) {
        //构造方法Person(String name)已知,创建对象方式new已知,就可以使用Person引用new创建对象,
        //使用方法引用优化Lambda表达式
        method(str -> new Person(str), "Jeff");
        method(Person::new, "Lucy");
    }
}
/*output:
Jeff
Lucy
*/

数组的构造方法引用

  • 已知创建的数组元素类型,数组的长度也是已知的,就可以使用元素类型[]引用new根据参数的长度来创建数组,使用方法引用来优化Lambda表达式
  • 示例Code
@FunctionalInterface
public interface ArrayBuilder {
    public int[] buildArray(int length);
}
public class Demo01 {
    public static void main(String[] args) {
        //使用方法引用优化Lambda表达式:
        //已知创建的就是int[]数组,数组的长度也是已知的,就可以使用方法引用,int[]引用new,根据参数传递的长度来创建数组
        int[] arr1 = method(len -> new int[len], 15);
        System.out.println(arr1.length);
        int[] arr2 = method(int[]::new, 8);
        System.out.println(arr2.length);
    }

    public static int[] method(ArrayBuilder arrayBuilder, int length) {
        return arrayBuilder.buildArray(length);
    }
}
/*output:
15
8
*/

五、Junit

测试概述

测试分类:

  1. 黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值
  2. 白盒测试:需要写代码,关注程序具体的执行流程

Junit就是一种白盒测试

Junit使用步骤

  • 步骤:
  1. 定义一个测试类(测试用例)

建议格式:

测试类名:被测试的类名+Test

包名:xxx.xxx.xx.test

  1. 定义测试方法:可以独立运行

建议格式:

方法名:test+测试的方法名

返回值:void

参数列表:空参

  1. 给方法加注解@Test
  2. 导入Junit依赖环境
  • 判定结果:
    红色:失败
    绿色:成功
    一般不会在单元测试中输出结果(无法判断结果是否正确),会使用断言操作来处理结果:
    Assert.assertEquals(期望结果值, 运行的结果)
  • 示例Code
public class Demo01 {
    public int add(int num1, int num2) {
        return num1+num2;
    }

    public int sub(int num1, int num2) {
        return num1-num2;
    }
}
public class Demo01Test {
    @Test
    public void testAdd() {
        int result = new Demo01().add(10, 2);
        Assert.assertEquals(12, result);
    }

    @Test
    public void testSub() {
        int result = new Demo01().sub(10, 4);
        Assert.assertEquals(6, result);
    }
}

相关注解:@Before和@After

  • @Before:修饰的方法会在每个测试方法之前被自动执行
    用于资源的申请
  • @After:修饰的方法会在每个测试方法执行之后会被自动执行
    释放资源的方法
  • 示例Code
public class Demo01Test {

    @Before
    public void init() {
        System.out.println("init...");
    }

    @After
    public void close() {
        System.out.println("close...");
    }

    @Test
    public void testAdd() {
        int result = new Demo01().add(10, 2);
        Assert.assertEquals(12, result);
    }

    @Test
    public void testSub() {
        int result = new Demo01().sub(10, 4);
        Assert.assertEquals(6, result);
    }
}
/*output:
init...
close...
init...
close...
*/

六、反射

类的加载过程和类加载器

  • 类的加载时机(类初始化的时机)
  • 第一次创建类的对象时
  • 第一次创建子类的对象时
  • 第一次调用其静态资源(通过类名调用和main)
  • 第一次获得该类的class对象时(通过反射)
  • 类的加载过程
  1. 加载:load
    将字节码数据读入内存
  2. 连接:link
  1. 验证:校验合法性
  2. 准备:准备对应的内存,创建Class对象,为类变量赋默认值,为静态常量赋初始值
  3. 解析:把字节码中的符号引用替换为对应的直接地址引用
  1. 初始化:initialize(类初始化)
    执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化
  • 类加载器
  • 引导加载器(Bootstrap Classloader)

也叫根类加载器,它负责加载jre/rt.jar核心库(JDK给我们提供的类都是由此类加载器加载)

它本身不是Java代码实现的,也不是ClassLoader的子类,获取其类加载器对象一般返回null

  • 扩展类加载器(Extension Classloader)

使用第三方的jar包(框架、工具)

负责加载jre/lib/ext扩展库

它是ClassLoader的子类

  • 应用加载器(Application Classloader)

负责加载开发者自己创建的类(加载项目的classpath路径下的类)

它是ClassLoader的子类

ClassLoader classLoader1 = Person.class.getClassLoader();
ClassLoader classLoader2 = Student.class.getClassLoader();
ClassLoader classLoader3 = Date.class.getClassLoader();

System.out.println(classLoader1);//Application Classloader
System.out.println(classLoader2);//Application Classloader
System.out.println(classLoader3);//Bootstrap Classloader

/*output:
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
null
*/
  • 自定义类加载器

当程序需要加载“特定”目录下的类,可以自定义类加载器

反射概述

  • 框架:框架是半成品的软件,可以在框架的基础上进行软件开发,简化编码
  • 反射:将类的各个组成部分封装为其他对象,这就是反射机制
  • 反射的好处:
  1. 可以在程序运行的过程中,操作这些对象
  2. 可以解耦,提高程序的可扩展性

获取字节码Class对象的三种方式

  • Java代码在计算机中经历的三个阶段
  • 三种方式:
  1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
    使用场景:多用于配置文件,将类名定义在配置文件中。读取文件,加载类
  2. 类名.class:通过类名的属性class获取
    使用场景:多用于参数的传递
  3. 对象.getClass():getClass()方法在Object类中定义
    使用场景:多用于对象的获取字节码的方式
  • 总结:同一个字节码文件(*.class)在一次程序运行的过程中,只会被加载一次,不论通过哪一种方式获取的class对象都是同一个
  • 示例Code
package JavaAdvance.Day14JunitReflectionNotation;
public class Person {
        private int age;
    public Person() {
    }

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

    public int getAge() {
        return age;
    }

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

public class Demo02 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class cls1 = Class.forName("JavaAdvance.Day14JunitReflectionNotation.Person");
        Class cls2 = Person.class;
        Class cls3 = new Person().getClass();

        System.out.println(cls1);
        System.out.println(cls2);
        System.out.println(cls3);

        //同一个字节码文件(*.class)在一次程序运行的过程中,只会被加载一次,不论通过哪一种方式获取的class对象都是同一个
        System.out.println(cls1 == cls2);
        System.out.println(cls2 == cls3);
    }
}
/*output:
class JavaAdvance.Day14JunitReflectionNotation.Person
class JavaAdvance.Day14JunitReflectionNotation.Person
class JavaAdvance.Day14JunitReflectionNotation.Person
true
true
*/

Class对象功能(使用Class对象)

  • 获取功能概述:
  1. 获取成员变量们
  • Field[] getFields()获取所有public修饰的成员变量
  • Field getField(String name)获取指定名称,public修饰的成员变量
  • Field[] getDeclaredFields()获取所有的成员变量,不考虑修饰符
  • Field getDeclaredField(String name)
  1. 获取构造方法们
  • Constructor<?>[] getConstructors()
  • Constructor<T> getConstructor(class<?>... parameterTypes)
  • Constructor<?>[] getDeclardConstructors()
  • Constructor<T> getDeclaredConstructor(class<?>... parameterTypes)
  1. 获取成员方法们
  • Method[] getMethods()
  • Method getMethod(String name, class<?>... parameterTypes)
  • Method[] getDeclaredMethods()
  • Method getDeclaredMethod(String name, class<?>... parameterTypes)
  1. 获取类名
  • String getName()
  • Field:获取成员变量
  • 常用方法:
  • Field[] getFields()获取所有public修饰的成员变量
  • Field getField(String name)获取指定名称,public修饰的成员变量
  • Field[] getDeclaredFields()获取所有的成员变量,不考虑修饰符
  • Field getDeclaredField(String name)
  • 对Field对象的操作:
  1. 设置值
    void set(Object obj, Object value)将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
  2. 获取值
    get(Object obj)返回指定对象上此 Field 表示的字段的值。
  3. 忽略访问权限修饰符的安全检查(能够访问所有权限的对象)
  • 问题:使用get访问Field[] getDeclaredFields()Field getDeclaredField(String name)中的Field对象的非public变量会抛出IllegalAccessException异常
  • 解决方法:使用Field对象的setAccessible(true)方法,进行暴力反射
  • 示例Code
public class Student {
    private int age;
    public String name;
    private int ID;

    public Student() {
    }

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public int getID() {
        return ID;
    }

    public void setID(int ID) {
        this.ID = ID;
    }
}
public class Demo3 {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Student stu1 = new Student(15, "Jeff", 35);
        Class stuClass = stu1.getClass();
        Field[] arr = stuClass.getFields();
        System.out.println("length : " + arr.length);
        System.out.println("arr[0] : " + arr[0]);
        System.out.println("name = " + arr[0].get(stu1));
        System.out.println("=1=========================");

        arr[0].set(stu1, "Nick");
        System.out.println("length : " + arr.length);
        System.out.println("arr[0]: " + arr[0]);
        System.out.println("name = " + arr[0].get(stu1));
        System.out.println("=2=========================");

        Field field01 = stuClass.getField("name");
        System.out.println("name = " + field01.get(stu1));

        Field field02 = stuClass.getDeclaredField("age");
        field02.setAccessible(true);
        field02.set(stu1, 88);
        System.out.println("age = " + field02.get(stu1));

        Field field03 = stuClass.getDeclaredField("ID");
        field03.setAccessible(true);
        System.out.println("ID = " + field03.get(stu1));
        System.out.println("=3=========================");

        Field[] arr3 = stuClass.getDeclaredFields();
        arr3[0].setAccessible(true);
        System.out.println("length : " + arr3.length);
        System.out.println("arr[0] : " + arr3[0]);
        System.out.println("age = " + arr3[0].get(stu1));
    }
}
/*output:
length : 1
arr[0] : public java.lang.String JavaAdvance.Day14JunitReflectionNotation.Student.name
name = Jeff
=1=========================
length : 1
arr[0]: public java.lang.String JavaAdvance.Day14JunitReflectionNotation.Student.name
name = Nick
=2=========================
name = Nick
age = 88
ID = 35
=3=========================
length : 3
arr[0] : private int JavaAdvance.Day14JunitReflectionNotation.Student.age
age = 88
*/
  • Constructor:获取构造方法
  • 常用方法:
  • Constructor<?>[] getConstructors()获取public修饰的
  • Constructor<T> getConstructor(class<?>... parameterTypes)获取public修饰的
  • Constructor<?>[] getDeclardConstructors()
  • Constructor<T> getDeclaredConstructor(class<?>... parameterTypes)
  • 对Constructor对象的操作
  1. 创建对象:
  • 使用带参构造方法创建对象T newInstance(Object... initargs)
  • 使用空参构造方法创建对象,操作可以简化:可以直接使用Class对象的newInstance方法
  1. 忽略访问权限修饰符的安全检查(能够访问所有权限的对象)
    使用Constructor对象的setAccessible(true)方法,进行暴力反射
  • 示例Code
public class Student {
    private int age;
    public String name;
    private int ID;

    public Student() {
    }

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public int getID() {
        return ID;
    }

    public void setID(int ID) {
        this.ID = ID;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", ID=" + ID +
                '}';
    }
}
public class Demo04 {
    public static void main(String[] args) throws Exception {
        Student student0 = new Student();
        Class cls1 = student0.getClass();
        //获取对象cls1的含参构造放方法
        Constructor constructor1 = cls1.getConstructor(int.class, String.class, int.class);
        System.out.println(constructor1);
        //使用含参构造方法对象创建cls1的含参对象
        Object student1 = constructor1.newInstance(15, "Tim", 3355);
        System.out.println(student1);
        System.out.println("=============================");

        //获取对象cls1的空参构造方法
        Constructor constructor2 = cls1.getConstructor();
        System.out.println(constructor2);
        //使用空参构造方法对象创建cls1的空参对象
        Object student2 = constructor2.newInstance();
        System.out.println(student2);
        System.out.println("=============================");

        //创建cls1的空参对象
        Object student3 = cls1.newInstance();
        System.out.println(student3);
    }
}
/*output:
public JavaAdvance.Day14JunitReflectionNotation.Student(int,java.lang.String,int)
Student{age=15, name='Tim', ID=3355}
=============================
public JavaAdvance.Day14JunitReflectionNotation.Student()
Student{age=0, name='null', ID=0}
=============================
Student{age=0, name='null', ID=0}
*/
  • Method:获取成员方法
  • 常用方法:
  • Method[] getMethods()获取public修饰的
    打印出的一些方法中包含了Object类中的方法
  • Method getMethod(String name, class<?>... parameterTypes)获取public修饰的
    后面的变参是该方法的传入参数的Class对象,根据传参数的不同可调用不同的重载方法
  • Method[] getDeclaredMethods()
  • Method getDeclaredMethod(String name, class<?>... parameterTypes
  • 对Method对象的操作
  1. 调用指定的方法,第一个参数是要调用对象,后面的变参必须和获取Method对象方法中传入的参数Class对象类型对应
    Object invoke(Object obj, Object... args)
  2. 忽略访问权限修饰符的安全检查(能够访问所有权限的对象)
    使用Method对象的setAccessible(true)方法,进行暴力反射
  3. 获取方法的名称
    String getName()
  • 示例Code
public class Person {
    private String name;
    private int age;

    public Person() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

    public void printInfo(String info) {
        System.out.println("this.name = " + this.name + ", StringInfo = " + info);
    }

    public void printInfo(int info) {
        System.out.println("this.age = " + this.age + ", numInfo = " + info);
    }

    public void printInfo(String info1, int info2) {
        System.out.println("info1 = " + info1 + ", info2 = " + info2);
    }
}
public class Demo05 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        Person person1 = new Person("Jeff", 25);
        Class personClass = person.getClass();

        Method printInfoMethod1 = personClass.getMethod("printInfo", int.class);
        printInfoMethod1.invoke(person, 233);
        printInfoMethod1.invoke(person1, 255);

        Method printInfoMethod2 = personClass.getMethod("printInfo", String.class, int.class);
        printInfoMethod2.invoke(person, "person is running", 1);
        printInfoMethod2.invoke(person1, "person1 is running", 2);
    }
}
/*output:
this.age = 0, numInfo = 233
this.age = 25, numInfo = 255
info1 = person is running, info2 = 1
info1 = person1 is running, info2 = 2
*/

打印getMethods方法获取的Method数组中所有的方法(包含了类本身的成员方法+Object类中的一些方法)

public class Demo06 {
    public static void main(String[] args) {
        Person person = new Person();
        Class personClass = person.getClass();

        Method[] methodArr = personClass.getMethods();
        for(Method ele : methodArr) {
            System.out.println(ele);
        }
    }
}
/*output:
public java.lang.String JavaAdvance.Day14JunitReflectionNotation.Person.getName()
public java.lang.String JavaAdvance.Day14JunitReflectionNotation.Person.toString()
public void JavaAdvance.Day14JunitReflectionNotation.Person.setName(java.lang.String)
public void JavaAdvance.Day14JunitReflectionNotation.Person.printInfo(java.lang.String)
public void JavaAdvance.Day14JunitReflectionNotation.Person.printInfo(int)
public void JavaAdvance.Day14JunitReflectionNotation.Person.printInfo(java.lang.String,int)
public int JavaAdvance.Day14JunitReflectionNotation.Person.getAge()
public void JavaAdvance.Day14JunitReflectionNotation.Person.setAge(int)

//上面这8个是Person类本身的成员方法,下面的是Object类中的方法

public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
*/

打印getMethods方法获取的Method数组中所有的方法的名称(包含了类本身的成员方法+Object类中的一些方法)

public class Demo06 {
    public static void main(String[] args) {
        Person person = new Person();
        Class personClass = person.getClass();

        Method[] methodArr = personClass.getMethods();
        for(Method ele : methodArr) {
            System.out.println(ele.getName());
        }
    }
}
/*output:
getName
toString
setName
getAge
printInfo
printInfo
printInfo
setAge

//上面这8个是Person类本身的成员方法,下面的是Object类中的方法

wait
wait
wait
equals
hashCode
getClass
notify
notifyAll
*/
  • 从Class对象中获取类名
  • String getName()返回值是一个全类名
  • 示例Code
public class Demo06 {
    public static void main(String[] args) {
        Person person = new Person();
        Class personClass = person.getClass();

        String className = personClass.getName();
        System.out.println(className);
    }
}
/*output:
JavaAdvance.Day14JunitReflectionNotation.Person
*/

反射案例

  • 写一个框架,在不改变类的任何代码的前提下,可以用它创建任意类的对象,并且执行其中的方法
  • 实现工具:
  1. 配置文件
  2. 反射
  • 实现步骤:
  1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中
  2. 在程序中加载读取配置文件
  3. 使用反射技术来加载类文件进内存
  4. 创建对象
  5. 执行方法
  • 示例Code
//配置文件RT.properties中的信息
Class = JavaAdvance.Day14JunitReflectionNotation.ReflectionProject.Student
Method = printInfo
//测试用的类
public class Student {
    public void printInfo() {
        System.out.println("this is student");
    }
}
public class ReflectionTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {

        ClassLoader classLoader = ReflectionTest.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream("RT.properties");

        Properties properties = new Properties();
        properties.load(inputStream);
        String className = properties.getProperty("Class");
        String methodName = properties.getProperty("Method");

        Class obj = Class.forName(className);
        Method method = obj.getMethod(methodName);
        method.invoke(obj.newInstance());
    }
}
/*output:
this is student
*/

七、Enum类和使用enum定义的枚举类

枚举类概述

  • JDK1.5之后,可以定义有限数量的可穷举数据集,简而言之就是,当一个类有几个对象时,使用枚举
  • 必须在枚举类第一行声明枚举类对象
  • 枚举类对象的属性不应允许被改动,所以用public finall修饰
    枚举类使用public final修饰的对象的属性应该在构造器中为其赋值
    若枚举类显式的定义了带参数的构造器,则在列出的枚举值时也必须对应的传入参数
  • 枚举类和普通类的区别:
  • 使用enum定义的枚举类默认继承了java.lang.Enum类
  • 枚举类的构造器只能使用private访问控制符
  • 枚举类的所有实例必须在枚举类中显式列出
    列出的实例系统会自动添加public static final修饰

格式:使用,进行分隔两个实例,最后一个实例后使用;结尾

自定义枚举类(使用普通类来模拟枚举类)

  • 使用enum关键字定义的枚举类的特点:
  • 类的内部创建自身对象
  • 构造器用private修饰
  • 示例Code
//使用普通类来模拟枚举类
//特点:类的内部创建自身对象
public class Demo01 {
    public static final Demo01 DEMO_01 = new Demo01("Winter", "This is Winter");
    public static final Demo01 DEMO_02 = new Demo01("Summer", "This is Summer");
    public static final Demo01 DEMO_03 = new Demo01("Fall", "This is Fall");
    public static final Demo01 DEMO_04 = new Demo01("Spring", "This is Spring");

    private String season;
    private String description;

    Demo01(String season, String description) {
        this.season = season;
        this.description = description;
    }

    public String getSeason() {
        return season;
    }

    public void setSeason(String season) {
        this.season = season;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Demo01{" +
                "season='" + season + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

使用enum关键字定义的枚举类

  • 使用enum关键字定义的枚举类的特点:
  • 类的内部创建自身对象
  • 构造器用private修饰
  • 示例Code
//带参数形式的格式:
//对象名1(参数1,...参数n), 对象名2(参数1,...参数n), 对象名3(参数1,...参数n), ...对象名n(参数1,...参数n);
public enum Demo01enum {
    DEMO_01("Winter", "This is Winter"),
    DEMO_02("Summer", "This is Summer"),
    DEMO_03("Fall", "This is Fall"),
    DEMO_04("Spring", "This is Spring");

    private String season;
    private String description;

    private Demo01enum(String season, String description) {
        this.season = season;
        this.description = description;
    }

    public String getSeason() {
        return season;
    }

    public void setSeason(String season) {
        this.season = season;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Demo01enum{" +
                "season='" + season + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}
//打印带参数的枚举类对象,该枚举类重写toString方法,得到的是对象名和对象内容
//若没有在枚举类中重写toString方法,得到的是对象名(继承自Enum的toString方法,只打印对象名)
public class Demo02 {
  public static void main(String[] args) {

      ArrayList<Demo01enum> list = new ArrayList<>();
      list.add(Demo01enum.DEMO_01);
      list.add(Demo01enum.DEMO_02);
      list.add(Demo01enum.DEMO_03);
      list.add(Demo01enum.DEMO_04);

      for(Demo01enum ele : list) {
          System.out.println(ele);
      }
  }
}
/*output:
Demo01enum{season='Winter', description='This is Winter'}
Demo01enum{season='Summer', description='This is Summer'}
Demo01enum{season='Fall', description='This is Fall'}
Demo01enum{season='Spring', description='This is Spring'}
*/
//不带参数的形式
public enum Demo01enum02 {
    DEMO_01_ENUM_02_A,
    DEMO_01_ENUM_02_B,
    DEMO_01_ENUM_02_C,
    DEMO_01_ENUM_02_D;
}
//打印不带参数的枚举类对象,得到的是对象名,该枚举类继承了重写toString方法的Enum类
public class Demo02 {
    public static void main(String[] args) {

        ArrayList<Demo01enum02> list = new ArrayList<>();
        list.add(Demo01enum02.DEMO_01_ENUM_02_A);
        list.add(Demo01enum02.DEMO_01_ENUM_02_B);
        list.add(Demo01enum02.DEMO_01_ENUM_02_C);
        list.add(Demo01enum02.DEMO_01_ENUM_02_D);

        for(Demo01enum02 ele : list) {
            System.out.println(ele);
        }
    }
}
/*output:
DEMO_01_ENUM_02_A
DEMO_01_ENUM_02_B
DEMO_01_ENUM_02_C
DEMO_01_ENUM_02_D
*/

枚举类与switch(xxx)

  • JDK1.5中可以在switch表达式中使用枚举类对象作为表达式,case子句可以直接使用枚举值的名字,无需添加枚举类作为限定
  • 示例Code
public class Demo03 {
    public static void main(String[] args) {
        Demo01enum d1 = Demo01enum.DEMO_01;

        switch (d1) {
            case DEMO_01:
                System.out.println("Winter");
                break;
            case DEMO_02:
                System.out.println("Summer");
                break;
            case DEMO_03:
                System.out.println("Fall");
                break;
            case DEMO_04:
                System.out.println("Spring");
                break;
        }
    }
}
/*output:
Winter
*/

枚举类的方法

  • 枚举类常用的两个静态方法
  • 根据枚举类对象的名称,获取对应的枚举类对象
    valueOf(String name)
  • 获取当前枚举类中所有的枚举类对象,组成数组
    values()
  • 示例Code
public class Demo04 {
    public static void main(String[] args) {
        Demo01enum d1 = Demo01enum.valueOf("DEMO_01");
        System.out.println(d1 + "\n");

        Demo01enum[] arr = Demo01enum.values();
        for(Demo01enum ele:arr) {
            System.out.println(ele);
        }
    }
}
/*output:
DEMO_01

DEMO_01
DEMO_02
DEMO_03
DEMO_04
*/

枚举类实现接口

  • 枚举类实现的接口,在每个枚举类对象后可以重写接口中的方法
  • 示例Code
public interface MyInterface {
    public void printInfo();
}
public enum Demo01enum02 implements MyInterface{
    //使用匿名内部类的方式重写接口的方法,枚举类的对象使用不同的重写方法
    DEMO_01_ENUM_02_A{
        @Override
        public void printInfo() {
            System.out.println("Spring");
        }
    },
    DEMO_01_ENUM_02_B{
        @Override
        public void printInfo() {
            System.out.println("Summer");
        }
    },
    DEMO_01_ENUM_02_C{
        @Override
        public void printInfo() {
            System.out.println("Fall");
        }
    },
    DEMO_01_ENUM_02_D{
        @Override
        public void printInfo() {
            System.out.println("Winter");
        }
    };

	  //直接重写接口的方法,枚举类的对象使用同一个重写方法
//    @Override
//    public void printInfo() {
//        System.out.println("this is Demo01enum02");
//    }
}
public class Demo05 {
    public static void main(String[] args) {
        Demo01enum02[] arr = Demo01enum02.values();

        for(Demo01enum02 ele : arr) {
            ele.printInfo();
        }
    }
}
/*output:
Spring
Summer
Fall
Winter
*/

八、注解

注解概述

  • 注解和注释的区别
    注释:用文字描述,给程序员看的
    注解:说明程序,给计算机看的
  • 注解定义:注解(Annotation),也叫元数据。是JDK1.5之后的新特性,与类、接口、枚举是在同一个层次。可以声明在包、类、字段、方法、局部变量、方法参数的前面,用来对这些元素进行说明,注释
  • 注解格式:@注解名称
  • 注解不是程序的一部分,可以理解为注解就是一个标签
  • 作用分类:
  1. 编写文档:通过代码里标识的注解生成文档(生成doc文档)
  2. 代码分析:通过代码里标识的注解对代码进行分析(使用反射),解析注解
  3. 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查

JDK内置注解

  • JDK中预定义的一些注解:
  • @Override:用于注解方法,说明方法必须是重写方法
  • @Deprecated:用于注解属性、方法、类,说明已经过时
  • @SuppressWarnings:抑制编译器警告
    一般传递参数all:
    @SuppressWarnings("all")

自定义注解

  • 格式:
元注解1
元注解2
元注解3
...
public @interface 注解名称 {
    属性列表;
}
  • 本质:注解本质上就是一个接口,该接口默认继承Annotation接口
    public interface 注解名称 extends java.lang.annotation.Annotation{}
  • 属性:接口中可以定义抽象方法
    要求:
  • 属性的返回值类型有下列的取值:
  1. 基本数据类型
  2. String
  3. 枚举
  4. 注解
  5. 以上类型的数组
  • 定义了属性,在使用时需要给属性赋值
  1. 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值
  2. 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可
    例如:@SuppressWarnings("all"),这个注解中只有一个属性String[] value()
  3. 数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}省略
  • 元注解:用于描述自定义注解的注解
  • @Target:描述注解能够可用于修饰哪些程序元素
    参数:
  • ElementType.TYPE可用于修饰类
  • ElementType.FIELD可用于修饰成员变量
  • ElementType.METHO可用于修饰成员方法
  • ElementType.PARAMETER可用于修饰方法参数
  • ElementType.CONSTRUCTOR可用于修饰构造器
  • ElementType.LOCAL_VARIABLE可用于修饰局部变量
  • @Retention:描述注解被的声明周期
  • @Retention(RetentionPolicy.SOURCE):编译器直接丢弃这种策略的注释,不会记录在class文件中,作用类似于普通注释
  • @Retention(RetentionPolicy.CLASS):编译器将把注释记录在class文件中,当运行Java程序时,JVM不会保留注释,这是默认值
  • @Retention(RetentionPolicy.RUNTIME):编译器将把注释记录在class文件中,当运行Java程序时,JVM会保留注释,程序可以通过反射获取该注释
  • @Documented:随之生成说明文档,注解的生命周期必须是RUNTIME
  • @Inherited:被它修饰的Annotation将具有继承性
  • 示例Code
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface MyAnnotation {
    String name() default "Jeff";
    int[] arr();
}
@MyAnnotation(name = "Jack", arr = {1, 2, 3})
public class Demo01 {
    @MyAnnotation(arr = {1, 2, 3})
    public static void main(String[] args) {

    }
}

解析注解

  • 在程序中解析注解,获取注解中定义的属性值
  • 实现步骤:
  1. 获取注解定义的位置的对象(Class,Method,Field)
  2. 获取指定的注解
    getAnnotation(Class)
  3. 调用注解中的抽象方法获取配置的属性值
  • 示例Code
public class Person {
    public void printInfo() {
        System.out.println("this is person");
    }
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeMethod {
    String className();
    String methodName();
}
@InvokeMethod(className = "JavaAdvance.Day14JunitReflectionNotation.AnnotationParse.Person", methodName = "printInfo")
public class Demo01 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        //通过本类的Class对象获取注解,返回值的类型是Annotation或其子类,Class<? extends Annotation>
        //InvokeMethod自定义注解是Annotation的子类
        //Class cls = Demo01.class;
        //InvokeMethod annotation = (InvokeMethod) cls.getAnnotation(InvokeMethod.class);
        
        //这里Class加了泛型就相当于,cls获取注解方法返回值得到的是InvokeMethod的实现类的对象
        Class<Demo01> cls = Demo01.class;
        InvokeMethod annotation = cls.getAnnotation(InvokeMethod.class);
        /*getAnnotation()相当于创建了以下实现类的对象
        public class InvokeMethodImpl implements InvokeMethod{
            @Override
            String className() {
                return "JavaAdvance.Day14JunitReflectionNotation.AnnotationParse.Person";
            }
            
            @Override
            String methodName() {
                return "printInfo";
            }
        }
        */
        String className = annotation.className();
        String methodName = annotation.methodName();

        System.out.println(className);
        System.out.println(methodName);

        Class objClass = Class.forName(className);
        Method method = objClass.getMethod(methodName);
        method.invoke(objClass.newInstance());
    }
}
/*output:
JavaAdvance.Day14JunitReflectionNotation.AnnotationParse.Person
printInfo
this is person
*/

注解案例

  • 实现目的
    简单的测试框架:
    当主方法执行后,自动自行检测所有方法(加了Check注解的方法),判断方法是否有异常,记录到文件中
  • 实现步骤:
  1. 创建计算器测试类的对象
  2. 获取字节码文件对象
  3. 获取所有的方法
  4. 判断方法上是否有Check注解
  5. 有注解就执行
  6. 捕获异常
  • 示例Code
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {

}
public class Calculator {
    @Check
    public void Myadd() {
        System.out.println(5 + 0);;
    }

    @Check
    public void Mysub() {
        System.out.println(5 - 0);;
    }

    @Check
    public void MyMul() {
        String str = null;
        str.toLowerCase();
        System.out.println(5 * 0);;
    }

    @Check
    public void MyDiv() {
        System.out.println(5 / 0);;
    }
}
public class Demo01 {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, IOException {
        Calculator cal = new Calculator();
        Class cls = cal.getClass();
        Method[] methods = cls.getMethods();

        BufferedWriter bw = new BufferedWriter(new FileWriter("C:\\MyFilesFolder\\VM-share\\sharedir\\JavaProjects\\BHJavaDevelopment\\src\\JavaAdvance\\Day14JunitReflectionNotation\\AnnotationProject\\bug.txt"));
        int exceptionCount = 0;

        for(Method method : methods) {
            if(method.isAnnotationPresent(Check.class)) {

                try {
                    method.invoke(cal);
                } catch (Exception e) {
                    exceptionCount++;

                    bw.write(method.getName() + " method exception");
                    bw.newLine();
                    bw.write("name of exception: " + e.getCause().getClass().getSimpleName());
                    bw.newLine();
                    bw.write("reason of exception: " + e.getCause().getMessage());
                    bw.newLine();
                    bw.write("-------------------");
                    bw.newLine();
                }
            }
        }

        bw.write("number of total exception: " + exceptionCount);
        bw.flush();
        bw.close();
    }
}
/*output:
5
5
*/
//bug.txt文件信息

MyMul method exception
name of exception: NullPointerException
reason of exception: null
-------------------
MyDiv method exception
name of exception: ArithmeticException
reason of exception: / by zero
-------------------
number of total exception: 2