Java 基础(尾篇)

Java 基础(尾篇)


内容概要:

网络编程入门

软件结构

C/S 结构

C/S 结构 全称为 Client/Server 结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。结构如下图:

B/S 结构

B/S 结构 全称为 Browser/Server 结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。结构如下图:

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。从而有了网络编程这个概念。网络编程:就是在一定的协议下,实现两台计算机的通信的程序。

网络通信协议

网络通信协议:
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
TCP/IP 协议:
传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol)是 Internet 中最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都需要它的下一层所提供的协议来完成自己的需求。

上图中,TCP/IP 协议中的四层分别是 应用层、传输层、网络层和链路层 ,每层分别负责不同的通信功能。

链路层:
链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
网络层:
网络层是整个 TCP/IP 协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
传输层:
主要使网络程序进行通信,在进行网络通信时,可以采用 TCP 协议,也可以采用 UDP 协议。
应用层:
主要负责应用程序的协议,例如 HTTP 协议、FTP 协议等。

协议分类

通信的协议是比较复杂的,java.net 包中的类和接口,它们提供低层次的通信细节。所以我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。java.net 包中提供了两种常见的网络协议的支持:

UDP 协议:
UDP 协议即用户数据报协议(User Datagram Protocol),是面向无连接的通信协议。即在数据传输时,数据的发送端和接收端都不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议。

UDP 的交换过程如下图所示:

UDP 协议特点: 数据被限制在 64kb 以内,超出这个范围就不能发送了。

数据报(Datagram): 网络传输的基本单位。

TCP 协议:
TCP 协议即传输控制协议 (Transmission Control Protocol),是面向连接的通信协议。即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过 "三次握手" :即 TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

三次握手:
第一次握手:客户端向服务器端发出连接请求,等待服务器确认。 第二次握手:服务器端向客户端回送一个响应,通知客户端收到了连接请求。 第三次握手:客户端再次向服务器端发送确认信息,确认连接。

三次握手 的整个交互过程如下图所示:

完成三次握手并连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP 协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

网络编程三要素

协议

协议:
协议:计算机网络通信必须遵守的规则称之为协议。

IP地址

IP地址: 指互联网协议地址(Internet Protocol Address),俗称 IP 。IP 地址用来给一个网络中的计算机设备做唯一的编号。假如我们把 个人电脑 比作 一台手机 的话,那么 IP 地址 就相当于 电话号码 。IP 地址分类有如下两种:

IPv4:
IPv4 是一个 32 位的二进制数,通常被分为 4 个字节,表示成 `a.b.c.d` 的形式,例如 `192.168.65.100` 。其中 a、b、c、d 都是 0~255 之间的十进制整数,那么最多可以表示 42 亿个。
IPv6:
由于互联网的蓬勃发展,IP 地址的需求量愈来愈大,但是网络地址资源有限,使得 IP 的分配越发紧张。为了扩大地址空间,拟通过 IPv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分成 8 组十六进制数,IPV6 具体的表示方法如下所示: `ABCD:EF01:2345:6789:ABCD:EF01:2345:6789` ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

一些相关的常用命令:

1
2
3
4
5
6
# 查看本机IP地址,在控制台输入
ipconfig

# 检查网络是否连通,在控制台输入
ping 空格 IP地址
ping 220.181.57.216

特殊的IP地址: 本机IP地址:127.0.0.1localhost

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?如果说 IP 地址 可以唯一标识网络中的设备,那么 端口号 就可以唯一标识设备中的进程(应用程序)了。

端口号:
用两个字节表示的整数,它的取值范围是 0~65535 。其中,0~1023 之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

利用 协议 + IP地址 + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

TCP 通信程序

TCP 通信概述

TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

两端通信时步骤:
1、服务端程序,需要事先启动,等待客户端的连接。 2、客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在 Java 中,提供了两个类用于实现TCP通信程序:
1、客户端:java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 2、服务端:java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

Socket 类

Socket 类: 该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。

构造方法:
1、方法:public Socket(String host, int port) 2、作用:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的 host 是 null ,则相当于指定地址为回送地址。

小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

成员方法:
1、public InputStream getInputStream() : 返回此套接字的输入流。     如果此 Scoket 具有相关联的通道,则生成的 InputStream 的所有操作也关联该通道。     关闭生成的 InputStream 也将关闭相关的 Socket。
2、public OutputStream getOutputStream() : 返回此套接字的输出流。     如果此 Scoket 具有相关联的通道,则生成的 OutputStream 的所有操作也关联该通道。     关闭生成的 OutputStream 也将关闭相关的 Socket。
3、public void close() :关闭此套接字。     一旦一个 socket 被关闭,它不可再使用。关闭此socket也将关闭相关的 InputStream 和 OutputStream 。
4、public void shutdownOutput() : 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。

ServerSocket 类

ServerSocket 类: 这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法:
1、方法:public ServerSocket(int port) 2、作用:使用该构造方法在创建 ServerSocket 对象时,就可以将其绑定到一个指定的端口号上,参数 port 就是端口号。
成员方法:
1、方法:public Socket accept() 2、作用:侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

简单的TCP网络程序

TCP 通信步骤分析:
1、【服务端】启动,创建 ServerSocket 对象,等待连接。 2、【客户端】启动,创建 Socket 对象,请求连接。 3、【服务端】接收连接,调用 accept 方法,并返回一个 Socket 对象。 4、【客户端】Socket 对象,获取 OutputStream,向服务端写出数据。 5、【服务端】Scoket 对象,获取 InputStream,读取客户端发送的数据。

到此为止,客户端向服务端发送数据成功。接下来,服务端将向客户端回写数据。

TCP 服务器端回写数据:
6、【服务端】Socket 对象,获取 OutputStream,向客户端回写数据。 7、【客户端】Scoket 对象,获取 InputStream,解析回写数据。 8、【客户端】释放资源,断开连接。

客户端向服务器发送数据

服务端实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 .... ");
// 1、创建 ServerSocket 对象,绑定端口,开始等待连接
ServerSocket server = new ServerSocket(6666);

// 2、接收连接 accept 方法, 返回 socket 对象.
Socket socket = server.accept();

// 3、通过 socket 获取输入流
InputStream is = socket.getInputStream();

// 4、一次性读取数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
String msg = new String(bytes, 0, len);
System.out.println(msg);

// 5、关闭资源.
is.close();
server.close();
}

客户端实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
// 1、创建 Socket ( ip , port ) , 确定连接到哪里
Socket client = new Socket("localhost", 6666);

// 2、获取输出流对象
OutputStream os = client.getOutputStream();

// 3、写出数据.
os.write("你好么? tcp ,我来了".getBytes());

// 4、关闭资源
os.close();
client.close();
}

服务器向客户端回写数据

服务端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 .... ");
// 1、创建 ServerSocket对象,绑定端口,开始等待连接
ServerSocket server = new ServerSocket(6666);

// 2、接收连接 accept 方法, 返回 socket 对象.
Socket socket = server.accept();

// 3、通过 socket 获取输入流
InputStream is = socket.getInputStream();

// 4、一次性读取数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
String msg = new String(bytes, 0, len);
System.out.println(msg);

// =================回写数据=======================
// 5、通过 socket 获取输出流
OutputStream out = socket.getOutputStream();

// 6、回写数据
out.write("我很好,谢谢你".getBytes());

// 7、关闭资源
out.close();
is.close();
server.close();
}

客户端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
// 1、创建 Socket ( ip , port ) , 确定连接到哪里
Socket client = new Socket("localhost", 6666);

// 2、通过 Scoket , 获取输出流对象
OutputStream os = client.getOutputStream();

// 3、写出数据
os.write("你好么? tcp ,我来了".getBytes());

// ==============解析回写=========================
// 4、通过 Scoket,获取输入流对象
InputStream in = client.getInputStream();

// 5、读取数据数据
byte[] b = new byte[100];
int len = in.read(b);
System.out.println(new String(b, 0, len));

// 6、关闭资源
in.close();
os.close();
client.close();
}

文件上传案例

文件上传分析图解

  1. 【客户端】输入流,从硬盘读取文件数据到程序中。
  2. 【客户端】输出流,写出文件数据到服务端。
  3. 【服务端】输入流,读取文件数据到服务端程序。
  4. 【服务端】输出流,写出文件数据到服务器硬盘中。

服务端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws IOException {
System.out.println("服务器启动.....");
// 1、创建服务端 ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);

// 2、建立连接
Socket accept = serverSocket.accept();

// 3、创建输入流对象,读取文件数据 和 创建输出流,保存到本地
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));

// 4、读写数据
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}

//5、关闭资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
}

客户端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws IOException {
// 1、创建输入流对象,读取文件数据 和 创建输出流,保存到本地
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

// 2、写出数据
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
bos.flush();
}
System.out.println("文件发送完毕");

// 3、释放资源
bos.close();
socket.close();
bis.close();
System.out.println("文件上传完毕 ");
}

文件上传优化分析

1、文件名称写死的问题

服务端代码中,保存文件的名称如果写死,那么最终只会保留一个文件。通过使用系统时间优化,保证文件名称唯一,代码如下:

1
2
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg")    // 文件名称
BufferedOutputStream bos = new BufferedOutputStream(fis);

2、循环接收的问题

服务端代码中,保存一个文件就关闭了,之后的用户无法再上传。通过使用循环改进,可以不断的接收不同用户的文件,代码如下:

1
2
3
4
5
// 每次接收新的连接,创建一个 Socket
whiletrue){
Socket accept = serverSocket.accept();
......
}

3、效率问题

服务端代码中,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传。所以,使用多线程技术优化,代码如下:

1
2
3
4
5
6
7
8
9
whiletrue){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(String[] args) throws IOException {
System.out.println("服务器启动.....");
// 1、创建服务端 ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);

// 2、循环接收,建立连接
while (true) {
Socket accept = serverSocket.accept();

// 3、socket 对象交给子线程处理,进行读写操作。Runnable接口中,只有一个run方法,使用lambda表达式简化格式
new Thread(() -> {
try (
// 3.1 获取输入流对象
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());

// 3.2 创建输出流对象, 保存到本地
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {

// 3.3 读写数据
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}

// 4、关闭资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}

信息回写分析图解

前四步与基本文件上传一致,后两步如下:

1、【服务端】获取输出流,回写数据。
2、【客户端】获取输入流,解析回写数据。

服务器端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void main(String[] args) throws IOException {
System.out.println("服务器 启动..... ");
// 1、创建服务端 ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);

// 2、循环接收,建立连接
while (true) {
Socket accept = serverSocket.accept();

// 3、socket 对象交给子线程处理,进行读写操作。Runnable接口中,只有一个run方法,使用lambda表达式简化格式
new Thread(() -> {
try (
// 3.1 获取输入流对象
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());

// 3.2 创建输出流对象, 保存到本地
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {

// 3.3 读写数据
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}

// 4、===============信息回写===================
System.out.println("back ........");
OutputStream out = accept.getOutputStream();
out.write("上传成功".getBytes());
out.close();

// 5、关闭资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}

客户端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) throws IOException {
// 1.创建输入流对象,读取本地文件。 创建输出流,写到服务端
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

// 2、写出数据
byte[] bytes = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}

// 3、关闭输出流,通知服务端,写出数据完毕
socket.shutdownOutput();
System.out.println("文件发送完毕");

// 4、=======解析回写=========
InputStream in = socket.getInputStream();
byte[] back = new byte[20];
in.read(back);
System.out.println(new String(back));
in.close();

// 5、释放资源
socket.close();
bis.close();
}

模拟 B/S 服务器

模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。分析如下图:

案例分析

1、准备页面数据,web 文件夹,复制到 Module 中,如下图所示:

2、我们模拟服务器端,ServerSocket 类监听端口,使用浏览器访问,如下图:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8000);
Socket socket = server.accept();
InputStream in = socket.getInputStream();

byte[] bytes = new byte[1024];
int len = in.read(bytes);
System.out.println(new String(bytes, 0, len));

socket.close();
server.close();
}

  1. 服务器程序中字节输入流可以读取到浏览器发来的请求信息,如下图:

GET/web/index.html HTTP/1.1 是浏览器的请求消息。/web/index.html 为浏览器想要请求的服务器端的资源,使用字符串切割方式获取到请求的资源。

1
2
3
4
5
6
7
8
9
10
// 转换流,读取浏览器请求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();

// 取出请求资源的路径
String[] strArr = requst.split(" ");

// 去掉 web 前面的 /
String path = strArr[1].substring(1);
System.out.println(path);

案例实现

服务端代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 ....");
// 创建 ServerSocket 对象
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();

// 转换流读取浏览器的请求消息
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();

// 取出请求资源的路径
String[] strArr = requst.split(" ");

// 去掉web前面的 /
String path = strArr[1].substring(1);

// 读取客户端请求的资源文件
FileInputStream fis = new FileInputStream(path);
byte[] bytes= new byte[1024];
int len = 0 ;

// 字节输出流,将文件写会客户端
OutputStream out = socket.getOutputStream();

// 写入 HTTP 协议响应头,固定写法
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());

// 必须要写入空行,否则浏览器不解析
out.write("\r\n".getBytes());
while((len = fis.read(bytes)) != -1){
out.write(bytes, 0, len);
}

fis.close();
out.close();
readWb.close();
socket.close();
server.close();
}

访问效果

火狐浏览器中访问,如下图:

小贴士:不同的浏览器,内核不一样,解析效果有可能不一样。

发现浏览器中出现了的问题,浏览器没有读取到图片信息。这是为什么呢?原因:浏览器工作原理是遇到图片会开启一个线程进行单独的访问,因此需要在服务器端加入线程技术。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while(true){
Socket socket = server.accept();
new Thread(new Web(socket)).start();
}
}
static class Web implements Runnable{
private Socket socket;

public Web(Socket socket){
this.socket=socket;
}

public void run() {
try{
//转换流,读取浏览器请求第一行
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出请求资源的路径
String[] strArr = requst.split(" ");
System.out.println(Arrays.toString(strArr));
String path = strArr[1].substring(1);
System.out.println(path);

FileInputStream fis = new FileInputStream(path);
System.out.println(fis);
byte[] bytes= new byte[1024];
int len = 0 ;
//向浏览器 回写数据
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
}catch(Exception ex){

}
}
}

再次浏览器进行访问,如下图:


函数式编程

暂时预留一个位置……

JUnit 单元测试

什么是 JUnit

在平时的开发当中,一个项目往往包含了大量的方法,可能有成千上万个。如何去保证这些方法产生的结果是我们想要的呢? 当然了,最容易想到的一个方式,就是我们通过 System.out 来输出我们的结果,看看是不是满足我们的需求,但是项目中这些成千上万个方法,我们总不能在每一个方法中都去输出一遍嘛。这也太枯燥了。这时候用我们的单元测试框架 JUnit 就可以很好地解决这个问题。


JUnit 如何解决这个问题的呢?答案在于内部提供了一个 断言机制 他能够将我们预期的结果和实际的结果进行比对,判断出是否满足我们的期望。相信到这,你已经迫不及待的想认识一下 JUnit ,下面我们就对 JUnit 进行简单的介绍以及使用。


JUnit 简介: JUnit 是一个 Java 编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。JUnit 促进了“先测试后编码”的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。 这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。单元测试是一个对单一实体(类或方法)的测试。单元测试是每个软件公司提高产品质量、满足客户需求的重要环节。

单元测试分类

单元测试分类主要有两种: 黑盒测试白盒测试黑盒测试 不用写代码,只需要输入相关的值,看程序是否能够输出期望的值即可。而 白盒测试 是需要写代码的,且关注程序具体的执行流程。其他介绍如下图:

JUnit 注解

以下是 JUnit 测试中常用的注解。 相关介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
#################################################################################################################

# JUnit 注解 注解作用

@Test # 标识一条测试用例。可加参数: (A) (expected=XXEception.class) (B) (timeout=xxx)
@Ignore # 有时候我们想暂时不运行某些测试方法\测试类,可以在方法前加上这个注解。
@Before # 在每一个测试方法执行之前运行。
@After # 每一个测试方法执行完成之后运行。
@BefreClass # 所有测试开始之前运行。
@AfterClass # 所有测试结果之后运行。

#################################################################################################################

JUnit 使用

了解完 JUnit ,我们就来使用一下。第一步: 查看 JUnit 的大致使用步骤。 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# JUnit 属于白盒测试,需要编写一点代码。其使用步骤如下:
1、定义一个测试类(测试用例) 测试类名:被测试的类名Test 例如:CalculatorTest 包名:xxx.xx.test 例如:com.demo
2、定义测试方法:可以独立运行
方法名:test测试的方法名 例如:testAdd()
返回值:void
修饰符: JUnit4 需要加 public ,JUnit5 就可省略
参数列表:空参
3、给方法加上 @Test 注解
4、导入 JUnit 依赖环境

# 判定结果:红色 --> 失败, 绿色 --> 成功
一般我们会使用断言操作来处理结果:Assert.assertEquals(期望的结果,运算的结果)

# JUnit 的使用步骤到此结束!!!

第二步: 新建一个需要被测试的类。 代码如下:

Calculator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package JunitTest;

/**
* @Author: guoshizhan
* @Create: 2020/5/2 21:12
* @Description: Calculator 类
*/
public class Calculator {

// add 方法实现两个数相加操作
public static int add(int a, int b){
return a + b;
}

// sub 方法实现两个数相减操作
public static int sub(int a, int b){
return a - b;
}

}

第三步: 编写测试类。注意:@Test 注解 需要导包,需要把 Junit 包添加到类路径下【IDEA 会给出提示】。 代码如下:

CalculatorTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package JunitTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;


/**
* @Author: guoshizhan
* @Create: 2020/5/2 21:37
* @Description: CalculatorTest 类
*/
public class CalculatorTest {

/**
* 这个是初始化方法,在测试方法之前执行。方法体内大部分为公共部分,例如对象的创建等等。
*/
@Before
public void init() {
Calculator calculator = new Calculator();
System.out.println("初始化方法执行了……");
}

/**
* 测试 add 方法
*/
@Test
public void addTest() {
Calculator calculator = new Calculator();
int result = calculator.add(1, 2);

// 进行断言操作,如果期待结果和实际结果一样,那么测试通过
Assert.assertEquals(3, result);
}

/**
* 测试 sub 方法
*/
@Test
public void subTest() {
Calculator calculator = new Calculator();
int result = calculator.sub(1, 2);
System.out.println("测试 sub 方法执行了……");

// 进行断言操作,如果期待结果和实际结果一样,那么测试通过
Assert.assertEquals(3, result);
}

/**
* 这是一个结束方法,在测试方法之后执行。方法体内一般涉及一些需要最后执行的代码。例如:流的关闭等等
*/
@After
public void destory() {
System.out.println("销毁方法执行了……");
}

}

第四步: 执行我们需要进行测试的方法【我执行的是 subTest 方法】。 结果如下图:

TIPS:

到此为止,JUnit 就说完了。这里的所说的 JUnit 是 JUnit4 ,如果以后有时间,JUnit5 也可安排一下。

反射相关知识

反射: 是将类的各个组成部分封装为其他对象,这就是反射机制。 但使用反射的 前提条件 是: 必须先得到字节码的 Class ,Class 类用于表示 .class 文件【即我们常说的字节码文件】。 下图是我们 Java 代码的三个阶段:

反射的概述

JAVA 反射机制 是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是 Class类 中的方法,所以先要获取到每一个字节码文件对应的 Class 类型的对象。

那么我们使用反射有什么好处呢? 其好处如下:

1
2
3
4
5
6
7
# 反射的好处

1、可以在程序运行过程中,操作这些对象。
2、可以解耦,提高程序的可扩展性。
3、反射是框架设计的灵魂。

# 反射的好处

获取对象字节码

我们可以通过反射技术来 获取到对象的字节码文件。 那就开始操作吧。 第一步: 新建 Person 类。 代码如下:

Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package domain;

/**
* @Author: guoshizhan
* @Create: 2020/5/3 22:41
* @Description: Person 实体类
*/
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;
}
}

第二步: 新建 PersonReflectTest 类,然后编写代码来获取 Person 类的字节码文件。 代码如下:

PersonReflectTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package domain;

/**
* @Author: guoshizhan
* @Create: 2020/5/3 22:41
* @Description: PersonReflectTest 类
*/
public class PersonReflectTest {
public static void main(String[] args) throws Exception {

/**
* 获取 Class 对象的三种方式:
* 1、Class.forName("全类名"):将字节码文件加载进内存,返回 Class 对象。使用场合:多用于配置文件,将类名定义在配置文件中。读取文件,加载类
* 2、类名.class:通过类名的属性 class 获取。使用场合:多用于参数的传递
* 3、对象.getClass():getClass() 方法在 Object 类中定义着。使用场合:多用于对象的获取字节码的方式
* 4、得出的结论:同一个字节码文件 (*.class) 在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的 Class 对象都是同一个。
*/

// 1、Class.forName("全类名")
Class clazz01 = Class.forName("domain.Person");
System.out.println(clazz01);

// 2、类名.class
Class clazz02 = Person.class;
System.out.println(clazz02);

// 3、对象.getClass()
Person p = new Person();
Class clazz03 = p.getClass();
System.out.println(clazz03);

// == 比较三个对象
System.out.println(clazz01 == clazz02); // true
System.out.println(clazz01 == clazz03); // true

}
}

第三步: 执行上述代码,查看输出结果。 结果如下:

1
2
3
4
5
6
7
8
9
10
################################################################################################

# 输出结果:
class domain.Person
class domain.Person
class domain.Person
true
true

################################################################################################

Class 对象功能

获取成员变量

Class 对象具有 获取成员变量 的功能。具体操作: 新建一个反射测试类,用于作为反射的字节码对象。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package domain;

/**
* @Author: guoshizhan
* @Create: 2020/5/3 23:16
* @Description: 反射测试类
*/
public class ReflectTest {

public String a;
protected String b;
String c;
private String d;

@Override
public String toString() {
return "ReflectTest{" +
"a='" + a + '\'' +
", b='" + b + '\'' +
", c='" + c + '\'' +
", d='" + d + '\'' +
'}';
}

public String getA() {
return a;
}

public void setA(String a) {
this.a = a;
}

public ReflectTest() {
}

public ReflectTest(String a, String b, String c, String d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}

public String getB() {
return b;
}

public void setB(String b) {
this.b = b;
}

public String getC() {
return c;
}

public void setC(String c) {
this.c = c;
}

public String getD() {
return d;
}

public void setD(String d) {
this.d = d;
}

public void eat(){
System.out.println("吃饭……");
}

public void eat(String food){
System.out.println("吃饭……" + food);
}
}

获取成员变量功能的方法和解释都在代码的注释里面了,这里就不过多介绍。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 获取成员变量们
* Field[] getFields() 获取所有 public 修饰的成员变量
* Field getField(String name) 获取指定名称的 public 修饰的成员变量
* Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
* Field getDeclaredField(String name) 获取单个成员变量,不考虑修饰符,如果被 private 修饰,需要进行暴力反射
*/
public static void main(String[] args) throws Exception {

// 0、获取 ReflectTest 的 Class 对象
Class reflectTestClass = ReflectTest.class;

// 1、Field[] getFields() :获取所有 public 修饰的成员变量
Field[] fields = reflectTestClass.getFields();
for (Field field : fields) {
System.out.println(field);
}

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

// 2、Field getField(String name)
Field a = reflectTestClass.getField("a");
// 获取成员变量 a 的值
ReflectTest reflectTest = new ReflectTest();
Object value = a.get(reflectTest);
System.out.println(value);
a.set(reflectTest, "张三"); // 设置 a 的值
System.out.println(reflectTest);

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

// 3、Field[] getDeclaredFields() :获取所有的成员变量,不考虑修饰符
Field[] declaredFields = reflectTestClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}

// 4、Field getDeclaredField(String name)
Field d = reflectTestClass.getDeclaredField("d");
// 忽略访问权限修饰符的安全检查
d.setAccessible(true); // 暴力反射
Object value2 = d.get(reflectTest);
System.out.println(value2);

}

最后,我们查看一下上述代码的执行结果。 结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
################################################################################################

# 输出结果:
public java.lang.String domain.ReflectTest.a
-------------------------------------------------------
null
ReflectTest{a='张三', b='null', c='null', d='null'}
-------------------------------------------------------
public java.lang.String domain.ReflectTest.a
protected java.lang.String domain.ReflectTest.b
java.lang.String domain.ReflectTest.c
private java.lang.String domain.ReflectTest.d
null

################################################################################################

获取构造方法

Class 对象也具有 获取构造方法 的功能。此功能的方法和解释都在代码的注释里面了,这里就不过多介绍。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 获取构造方法
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(类<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
* Constructor<?>[] getDeclaredConstructors()
*/
public static void main(String[] args) throws Exception {
// 获取 ReflectTest 的 Class 对象
Class reflectTestClass = ReflectTest.class;

// Constructor<T> getConstructor(类<?>... parameterTypes)
Constructor constructor = reflectTestClass.getConstructor(String.class,String.class,String.class,String.class);
System.out.println(constructor);

// 创建对象
Object test = constructor.newInstance("第一个", "第二个", "第三个", "第四个");
System.out.println(test);
System.out.println("-------------------------------------------------------");

Constructor constructor2 = reflectTestClass.getConstructor();
System.out.println(constructor2);

// 如果使用空参数构造方法创建对象,操作可以简化:使用 newInstance 方法
Object cons = constructor2.newInstance();
System.out.println(cons);
Object o = reflectTestClass.newInstance();
System.out.println(o);

//constructor1.setAccessible(true);
}

我们运行上述代码,查看一下执行的结果。 结果如下:

1
2
3
4
5
6
7
8
9
10
11
################################################################################################

# 输出结果:
public domain.ReflectTest(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
ReflectTest{a='第一个', b='第二个', c='第三个', d='第四个'}
-------------------------------------------------------
public domain.ReflectTest()
ReflectTest{a='null', b='null', c='null', d='null'}
ReflectTest{a='null', b='null', c='null', d='null'}

################################################################################################

获取成员方法和类名

Class 对象也具有 获取成员方法和类名 的功能。此功能的方法和解释都在代码的注释里面了,这里就不过多介绍。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 获取成员方法
* Method[] getMethods()
* Method getMethod(String name, 类<?>... parameterTypes)
* Method[] getDeclaredMethods()
* Method getDeclaredMethod(String name, 类<?>... parameterTypes)
*
* 获取类名
* String getName()
*/
public static void main(String[] args) throws Exception {
// 获取 ReflectTest 的 Class 对象
Class reflectTestClass = ReflectTest.class;

// 获取指定名称的方法
Method eat_method = reflectTestClass.getMethod("eat");
ReflectTest reflectTest = new ReflectTest();
eat_method.invoke(reflectTest); // 执行方法


Method eat_method2 = reflectTestClass.getMethod("eat", String.class);
eat_method2.invoke(reflectTest,"水果"); // 执行方法

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

// 获取所有 public 修饰的方法
Method[] methods = reflectTestClass.getMethods();
for (Method method : methods) {
System.out.println(method);
String name = method.getName();
System.out.println(name);
//method.setAccessible(true);
}

// 获取类名
String className = reflectTestClass.getName();
System.out.println(className); // domain.ReflectTest
}

我们运行上述代码,查看一下执行的结果。 结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
################################################################################################

# 输出结果:
吃饭……
吃饭……水果
-----------------
public java.lang.String domain.ReflectTest.toString()
toString
public java.lang.String domain.ReflectTest.getA()
getA
public void domain.ReflectTest.setA(java.lang.String)
setA
public void domain.ReflectTest.setB(java.lang.String)
setB
public java.lang.String domain.ReflectTest.getC()
getC
public void domain.ReflectTest.setC(java.lang.String)
setC
public java.lang.String domain.ReflectTest.getD()
getD
public void domain.ReflectTest.eat()
eat
public void domain.ReflectTest.eat(java.lang.String)
eat
public void domain.ReflectTest.setD(java.lang.String)
setD
public java.lang.String domain.ReflectTest.getB()
getB
public final void java.lang.Object.wait() throws java.lang.InterruptedException
wait
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
wait
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
wait
public boolean java.lang.Object.equals(java.lang.Object)
equals
public native int java.lang.Object.hashCode()
hashCode
public final native java.lang.Class java.lang.Object.getClass()
getClass
public final native void java.lang.Object.notify()
notify
public final native void java.lang.Object.notifyAll()
notifyAll
domain.ReflectTest

################################################################################################

TIPS: 到此为止,反射基本就介绍完了。接下来做一个反射小案例。

反射小案例

案例需求: 编写一个 “框架” ,可以创建任意类的对象,可以执行任意方法。 前提: 不能改变该类的任何代码。


第一步: 在 domain 包下新建 Student 类。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package domain;

/**
* @Author: guoshizhan
* @Create: 2020/5/3 23:16
* @Description: Student 类
*/
public class Student {

public void eat(){
System.out.println("吃饭……");
System.out.println("睡觉……");
System.out.println("打豆豆……");
}

}

第二步: src 目录 【即类路径】下新建 pro.properties 配置文件。 代码如下:

pro.properties
1
2
3
4
5
# 配置需要被反射的类的全类名
className=domain.Student

# 配置需要被反射的方法
methodName=eat

第三步: 在 domain 包下编写 StudentReflectTest 类进行测试。 代码如下:

StudentReflectTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package domain;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;

/**
* @Author: guoshizhan
* @Create: 2020/8/7 10:33
* @Description: StudentReflectTest 类
*/
public class StudentReflectTest {
public static void main(String[] args) throws Exception {

// 1、创建 Properties 对象
Properties pro = new Properties();

// 2、加载配置文件,转换为一个集合
ClassLoader classLoader = StudentReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
pro.load(is);

// 3、获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");

Class cls = Class.forName(className); // 加载该类进内存
Object obj = cls.newInstance(); // 创建该类对象
Method method = cls.getMethod(methodName); // 获取方法对象
method.invoke(obj); // 执行方法

}

}

第四步: 我们运行上述代码,查看一下执行的结果。 结果如下:

1
2
3
4
5
6
7
8
################################################################################################

# 输出结果:
吃饭……
睡觉……
打豆豆……

################################################################################################

TIPS: 到此为止,反射知识就结束了!!!

注解知识

注解简介

定义: 注解(Annotation),也叫元数据。 一种代码级别的说明。它是 JDK1.5 及以后版本引入的一个特性,与 类、接口、枚举 是在同一个层次。它可以声明在 包、类、字段、方法、局部变量、方法参数等 的前面,用来对这些元素进行说明,注释。

1
2
3
4
5
6
7
8
9
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++ 概念描述:

+ 1、JDK1.5 之后的新特性
+ 2、说明程序的,给计算机看的
+ 3、使用注解:@注解名称

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
作用分类:
①编写文档:通过代码里标识的注解生成文档【生成文档 doc 文档】 ②代码分析:通过代码里标识的注解对代码进行分析【使用反射】 ③编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】

使用注解生成文档

我们经常使用的 api 文档就是使用 注解 生成的, 如下图:

那么我们该如何把我们自己写的类也生成这样的一个文档呢? 接下来我们就来操作一下。


创建一个文件夹

随便在桌面上建立一个文件夹【我的叫 doc】, 里面放一个 HelloWorld.java 文件。如下图:

编辑代码

编写这个 Java 文件。 代码内容如下:

HelloWorld.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 注解 Javadoc 演示
* @author guoshizhan
* @version 1.0
* @since 1.5
*/

public class HelloWorld {

/**
* 打印一条语句
* @param str String
* @return 无返回值
*/
public void print(String str){
System.out.println("Hello,World" + str);
}
}

这里要注意一下: 如果是 Windows 系统,此 java 文件编码必须设置为 GBK 编码,否则会导致乱码问题。

命令行执行

使用 javadoc 命令生成文档,如下图:

生成之后,doc 文件夹生成了好多文件,如下图:

查看效果

我们打开 index.html 即可查看效果 【和 api 文档一样哦】。如下:

预定义注解和元注解

下面是 预定义注解和元注解 的相关介绍,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 预定义的一些注解
@Override # 检测被该注解标注的方法是否是继承自父类(接口)的
@Deprecated # 该注解标注的内容,表示已过时
@SuppressWarnings # 压制警告,一般传递参数 all @SuppressWarnings("all") 参数:all deprecation unchecked


# 元注解
@Documented # 作用是能够将注解中的元素包含到 Javadoc 中去
@Repeatable # JavaSE 8 加入的特性,表明被注解的注解可以多次应用于相同的声明。
@Inherited # 表明注解类型可以从超类继承。当超类使用了 @Inherited 注解后,如果它的子类没有添加任何注解,那么子类会继承超类的注解。

@Retention # 指定注解的存储方式:
# RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
# RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
# RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。

@Target # 限制可以应用注解的 Java 元素类型:
# ElementType.ANNOTATION_TYPE 可以应用于注解类型。
# ElementType.CONSTRUCTOR 可以应用于构造函数。
# ElementType.FIELD 可以应用于字段或属性。
# ElementType.LOCAL_VARIABLE 可以应用于局部变量。
# ElementType.METHOD 可以应用于方法级注释。
# ElementType.PACKAGE 可以应用于包声明。
# ElementType.PARAMETER 可以应用于方法的参数。
# ElementType.TYPE 可以应用于类的任何元素。

# It's over!!!

预定义注解 的代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @Author: guoshizhan
* @Create: 2020/5/5 19:25
* @Description: 预定义注解的使用
*/
@SuppressWarnings("all")
public class AnnotationTest {

@Override
public String toString() {
return super.toString();
}

@Deprecated
public void print() {
System.out.println("我是过时的方法……");
}

public void printNew(){
System.out.println("我是新的 print 方法……");
}

public void test() {
print();
}

}

自定义注解

自定义注解 的相关介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
#######################################################################################

# 1、格式:注解的定义格式
1、元注解
2、public @interface 注解名称 {
属性列表;
}

# 2、本质:注解本质上就是一个接口,该接口默认继承 Annotation 接口
# 3、属性:接口中的抽象方法

#######################################################################################

自定义注解的格式 举例如下:

1
2
3
4
5
6
7
# 自定义注解格式举例:
/**
* @Author: guoshizhan
* @Create: 2020/5/5 19:47
* @Description: 自定义 MyAnnotation 注解
*/
public @interface MyAnnotation {}

自定义注解的本质

那么这个注解到底是个什么东西呢? 我们对上述定义的 MyAnnotation 注解 进行反编译一下,如下图:

通过反编译的结果 public interface MyAnno extends java.lang.annotation.Annotation {} 可知,注解本质上就是一个接口,该接口默认继承 Annotation 接口 既然是我们熟悉的接口,那就更好理解一点了。


自定义注解的属性

自定义注解的属性其实就是接口中的抽象方法, 只不过我们使用注解时这些方法 需要被赋值,否则会报错。 以下是 注解属性的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++ 属性要求:属性的返回值类型有下列取值
+ 基本数据类型
+ String
+ 枚举
+ 注解
+ 以上类型的数组

+++ 定义了属性,在使用时需要给属性赋值
+ 1、如果定义属性时,使用 default 关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
+ 2、如果只有一个属性需要赋值,并且属性的名称是 value,则 value 可以省略,直接定义值即可。
+ 3、数组赋值时,值使用 {} 包裹。如果数组中只有一个值,则 {} 可以省略

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

自定义注解的代码模板 演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Author: guoshizhan
* @Create: 2020/5/5 19:47
* @Description: 自定义 MyAnnotation 注解
*/
public @interface MyAnnotation {
// 基本数据类型
int age();

// String 类型
String name() default "guoshizhan"; // 使用 default 关键字给属性默认初始化值,使用注解时,可以不进行赋值。

// 数组类型
String[] strs();

// 注解、枚举等其他类型就不举例了
}

注解格式之元注解

元注解: 用于描述注解的注解。 接下来就简单介绍以下四个元注解:元注解相关参考

1
2
3
4
5
6
7
8
9
10
11
12
#######################################################################################

@Target # 描述注解能够作用的位置。ElementType 取值有三: TYPE,METHOD,FIELD ,作用如下:
# TYPE 只能作用于类上;METHOD 只能作用于方法上;FIELD 只能作用于成员变量上

@Retention # 描述注解被保留的阶段,
# 举例:@Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到 class 字节码文件中,并被 JVM 读取到

@Documented # 描述注解是否被抽取到 api 文档中
@Inherited # 描述注解是否被子类继承

#######################################################################################

解析注解

解析注解: 获取注解中定义的属性值。 具体步骤如下:

  • 1、获取注解定义的位置的对象 (Class,Method,Field)
  • 2、获取指定的注解 getAnnotation(Class)
  • 3、调用注解中的抽象方法获取配置的属性值

创建注解

既然要解析注解,那么就要先创建一个注解。 以下是我创建的 Pro 注解 ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @Author: guoshizhan
* @Create: 2020/5/5 22:58
* @Description: 定义一个 Pro 注解
*/

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
String className();
String methodName();
}

创建一个类,这个类会被 Pro 注解 使用。代码如下:

1
2
3
4
5
6
7
public class ReflectTest {

public void eat(){
System.out.println("吃饭……");
}

}

创建测试类

这是 测试 Pro 注解 的测试类,代码及运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 我们自己定义的 Pro 注解
@Pro(className = "domain.ReflectTest",methodName = "eat")
public class AnnotationCustom {
public static void main(String[] args) throws Exception {
// 1、解析注解。获取该类的字节码文件对象
Class<AnnotationCustom> testClass = AnnotationCustom.class;

// 2、获取注解对象,其实就是在内存中生成了一个该注解接口的子类实现对象
Pro an = testClass.getAnnotation(Pro.class);

// 3、调用注解对象中定义的抽象方法,获取返回值
String className = an.className();
String methodName = an.methodName();
System.out.println(className);
System.out.println(methodName);

Class cls = Class.forName(className); // 4、加载该类进内存
Object obj = cls.newInstance(); // 5、创建对象
Method method = cls.getMethod(methodName); // 6、获取方法对象
method.invoke(obj); // 7、执行方法
}
}

// 输出结果:
// domain.ReflectTest
// eat
// 吃饭……

注解小案例

第一步

新建一个 demo 包 ,在包下建立 @Check 注解 ,代码如下:

demo/Check.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
/**
* 定义 @Check 注解
*/
public @interface Check {
}

第二步

在 demo 包下建立 Calculator 类 ,并使用上面的 @Check 注解 ,代码如下:

demo/Calculator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package demo;

/**
* 定义计算器类
*/
public class Calculator {

// 加法
@Check
public void add(){
String str = null;
str.toString();
System.out.println("1 + 0 =" + (1 + 0));
}

// 减法
@Check
public void sub(){
System.out.println("1 - 0 =" + (1 - 0));
}

// 乘法
@Check
public void mul(){
System.out.println("1 * 0 =" + (1 * 0));
}

// 除法
@Check
public void div(){
System.out.println("1 / 0 =" + (1 / 0));
}

public void show(){
System.out.println("永无bug...");
}

}

第三步

在 demo 包下建立 TestCheck 类 ,用于测试注解是否起作用。代码如下:

demo/TestCheck.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package demo;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* 当主方法执行后,会自动自行被检测的所有方法(加了 @Check 注解的方法),判断方法是否有异常,记录到文件中
*/
public class TestCheck {
public static void main(String[] args) throws IOException {

Calculator c = new Calculator(); // 创建计算器对象
Class cls = c.getClass(); // 获取字节码文件对象
Method[] methods = cls.getMethods(); // 获取所有方法

int number = 0; // 出现异常的次数
BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));

for (Method method : methods) {
// 判断方法上是否有 @Check 注解
if (method.isAnnotationPresent(Check.class)) {
// 有 @Check 注解的话,执行如下代码
try {
method.invoke(c);
} catch (Exception e) {
// 捕获异常,然后记录到 bug.txt 文件中
number++;

bw.write(method.getName() + " 方法出异常了");
bw.newLine();
bw.write("异常的名称:" + e.getCause().getClass().getSimpleName());
bw.newLine();
bw.write("异常的原因:" + e.getCause().getMessage());
bw.newLine();
bw.write("--------------------------");
bw.newLine();

}
}
}

bw.write("本次测试一共出现 " + number + " 次异常");

bw.flush();
bw.close();

}
}

第四步

运行 TestCheck 类 ,查看生成的结果文件 bug.txt ,结果如下:

bug.txt
1
2
3
4
5
6
7
8
9
add 方法出异常了
异常的名称:NullPointerException
异常的原因:null
--------------------------
div 方法出异常了
异常的名称:ArithmeticException
异常的原因:/ by zero
--------------------------
本次测试一共出现 2 次异常

案例小结

案例小结:
1、以后大多数时候,我们会使用注解,而不是自定义注解 2、注解给谁用?给编译器解析程序用 3、注解不是程序的一部分,可以理解为注解就是一个标签

JavaSE 最后总结: 到此为止,JavaSE 就完结了,以后会慢慢完善 JavaSE 的这四篇文章。这些文章都是我学习视频课程的文字版记录,为的是自己以后能够方便快速的复习。 以后我还会进行各种修改,希望能够让初学者从这四篇博文就学完 JavaSE,这是我的终极目标,也是写博客的初衷。最后,感谢大家的阅读!!!


📚 本站推荐文章
  👉 从 0 开始搭建 Hexo 博客
  👉 计算机网络入门教程
  👉 数据结构入门
  👉 算法入门
  👉 IDEA 入门教程

可在评论区留言哦

一言句子获取中...

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×