1 Socket编程
1.1 Socket编程概述
1.1.1 Socket简介
在网络编程中,Socket(套接字)是一种抽象概念,它用于在不同计算机之间进行通信。Socket可以看作是一种通信的端点,可以通过Socket与其他计算机上的程序进行数据传输。
1.1.2 Java中的套接字编程
在Java中,Socket是一个类,可以用于创建客户端和服务器端的网络连接,并进行数据传输。
在Java网络编程中,Socket类与IO类常常结合使用。通过Socket类建立网络连接后,可以使用它提供的输入输出流对象来进行数据的读取和写入。
可以通过桥和路的关系来理解Socket与IO的关系。假设我们要实现两岸的交通,首先需要搭建一座桥梁连通两岸,这是通过Socket完成的。有了桥之后,我们可以通过桥上的路实现双向的车辆流动,这是通过IO实现的。
1.1.3 Socket和TCP/UDP的关系
Socket是实现网络编程的工具,而TCP/UDP是网络传输协议。
Socket编程常用于实现基于TCP或UDP的网络通信。TCP和UDP是在传输层上的协议,而Socket是在应用层与传输层之间的一个接口。通过Socket,应用程序可以使用TCP或UDP协议与其他计算机进行通信。
在Java中,可以使用Socket类来创建TCP连接,也可以使用DatagramSocket类来创建UDP连接。通过Socket类和DatagramSocket类,Java程序员可以方便地实现TCP和UDP通信,并进行数据传输。
1.2 聊天室案例
1.2.1 聊天室案例概述
使用Socket网络编程逐步实现一个聊天室应用。此案例需要按照版本进行迭代,如图所示:
每个版本可以存放在一个单独的package中,开发下一个版本时,将上一个版本的代码复制到新的package中,再进行后续的开发。
1.2.2 Socket编程
在Java网络编程中,Socket 和 ServerSocket是最常见的2个类,均位于java.net包中:
- Socket:用于建立网络连接;在连接成功时,应用程序两端都会产生一个Socket实例
- ServerSocket:用于服务端
TCP套接字的通信模型如下图所示:
1.2.3 ServerSocket概述
运行在服务端的ServerSocket主要完成两个工作:
1、向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立连接。
2、监听端口,一旦一个客户端建立连接,会立即返回一个Socket。通过这个Socket就可以和该客户端交互了。
代码示意如下:
//创建ServerSocket并申请服务端口8088
ServerSocket server = new ServerSocket(8088);
/*方法会产生阻塞,直到某个Socket连接并返回请求连接的Socket*/
Socket socket = server.accept();
我们可以把ServerSocket想象成某客服的"总机":用户打电话到总机,总机分配一个电话使得服务端与你沟通;我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。交互模型如下图所示:
1.2.4 【案例】聊天室案例V01
本案例需要实现的需求:
- 分别开发客户端类和服务端类
- 实现客户端与服务端的连接
首先开发Client端功能:
1、定义Client类:构造器中创建Socket并定义连接
2、定义start方法:用于封装后续业务功能
3、定义main方法
代码示意如下:
import java.io.IOException;
import java.net.Socket;
/**
* 聊天室客户端
*/
public class Client {
// 客户端使用的Socket
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client(){
try {
System.out.println("正在连接服务端...");
/*
实例化Socket时要传入两个参数
参数1:服务端的地址信息
可以是IP地址,如果连接本机可以写"localhost"
参数2:服务端开启的服务端口
*/
socket = new Socket("localhost",8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
开发Server端功能:
1、定义Server类:构造器中创建ServerSocket
2、定义start方法:用accept方法,侦听连接
3、定义main方法
代码示意如下:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
// 服务端使用的SocketServer
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端连接...");
/*
ServerSocket提供了接受客户端连接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的连接,直到一个客户端连接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
1.2.5 获取网络输入流和网络输出流
使用 Socket 通讯时,可以通过 Socket 获取输入流与输出流,从而实现数据信息的交互。
- InputStream getInputStream():该方法用于返回此套接字的输入流
- OutputStream getOutputStream():该方法用于返回此套接字的输出流
代码示意如下:
public void testSocket()throws Exception {
Socket socket = new Socket("localhost", 8088);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
}
需要通过流操作来实现信息的发送和读取。操作过程如下图所示:
1.2.6 【案例】聊天室案例V02
本案例实现的需求:
- 客户端向服务器发送一条消息
- 服务端收到消息后输出到控制台
Client端实现信息的发送:start方法中添加代码,实现信息的发送。
代码示意如下:
import java.io.*;
import java.net.Socket;
/**
* 聊天室客户端
*/
public class Client {
// 客户端使用的Socket
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client() {
try {
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
/*
Socket提供了一个方法:
OutputStream getOutputStream()
该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
*/
//低级流,将字节通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//高级流,负责块写文本数据加速
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串,自动行刷新
PrintWriter pw = new PrintWriter(bw, true);
pw.println("你好服务端!");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
Server端实现信息的读取:start方法中添加代码,实现信息的读取及展示。
代码示意如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
// 服务端使用的SocketServer
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = br.readLine();
System.out.println("客户端说:"+message);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
1.2.7 close方法
当使用 Socket 进行通讯完毕后,需要调用 close方法关闭 Socket 以释放系统资源。关闭了套接字,也会同时关闭由此获取的输入流与输出流。完整流程如下所示:
1.2.8 【案例】聊天室案例V03
本案例需要实现客户端循环发送消息给服务端。
Client端代码示意如下:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
// 客户端使用的Socket
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client() {
try {
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
/*
Socket提供了一个方法:
OutputStream getOutputStream()
该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
*/
//低级流,将字节通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//高级流,负责块写文本数据加速
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串,自动行刷新
PrintWriter pw = new PrintWriter(bw, true);
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
/*
通讯完毕后调用socket的close方法。
该方法会给对方发送断开信号。
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
Server端代码示意如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
// 服务端使用的SocketServer
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while((message = br.readLine())!=null) {
// br.readLine()方法会处于阻塞状态,直到收到消息
// br.readLine()返回null,说明客户端发送了断开连接的信息
System.out.println("客户端说:" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
1.3 聊天室案例(多线程)
1.3.1 Server端多线程模型
若想使一个服务端可以支持多客户端连接,我们需要解决以下问题:
- 循环调用 accept 方法侦听客户端的连接
- 使用线程来处理单一客户端的数据交互
因为需要处理多客户端,所以服务端要周期性循环调用accept方法,但该方法会产生阻塞,所以与某个客户端的交互就需要使用线程来并发处理。
服务端的流程如下所示:
1.3.2 【案例】聊天室案例V04
本案例需要实现服务器端能够接受多个客户端的访问。
Server端代码如下所示:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
// 服务端使用的SocketServer
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程与该客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
Thread t = new Thread(clientHandler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 定义线程任务
* 目的是让一个线程完成与特定客户端的交互工作
*/
private class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try{
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
String name = Thread.currentThread().getName();
while ((message = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + message);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
Client端代码不需要改变,通过IDEA设置启动多个客户端程序,思路如下:
操作步骤如下:
1.3.3 获取本地地址和端口号
可以通过 Socket 获取本地的地址以及端口号:
- int getLocalPort():用于获取本地使用的端口号
- InetAddress getLocalAddress():用于获取套接字绑定的本地地址
使用 InetAddress 获取本地的地址:
- String getCanonicalHostName():获取此 IP 地址的完全限定域名
- String getHostAddress():返回 IP 地址字符串(以文本表现形式)
代码示意如下:
public void testSocket()throws Exception {
Socket socket = new Socket("localhost",8088);
InetAddress add = socket.getLocalAddress();
System.out.println(add.getCanonicalHostName());
System.out.println(add.getHostAddress());
System.out.println(socket.getLocalPort());
}
1.3.4 获取远端地址和端口号
通过 Socket 获取远端的地址以及端口号
- int getPort():用于获取远端使用的端口号
- InetAddress getInetAddress():该方法用于获取套接字绑定的远端地址
代码示意如下:
public void testSocket()throws Exception {
Socket socket = new Socket("localhost",8088);
InetAddress inetAdd = socket.getInetAddress();
System.out.println(inetAdd.getCanonicalHostName());
System.out.println(inetAdd.getHostAddress());
System.out.println(socket.getPort());
}
1.3.5 【案例】聊天室案例V05
本案例需求:服务端发送消息给客户端。
Server端代码如下所示:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
// 服务端使用的SocketServer
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程与该客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
Thread t = new Thread(clientHandler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 定义线程任务
* 目的是让一个线程完成与特定客户端的交互工作
*/
private class ClientHandler implements Runnable{
private Socket socket;
private String host; // 记录客户端的IP地址信息
public ClientHandler(Socket socket){
this.socket = socket;
// 通过socket获取远端计算机地址信息
this.host = socket.getInetAddress().getHostAddress();
}
public void run(){
try{
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
String message = null;
String name = Thread.currentThread().getName();
while ((message = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + message);
// 将消息回复给客户端
pw.println(this.host +"说:"+message);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
Client端代码如下所示:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
private Socket socket;
public Client() {
try {
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
// 读取服务器发来的消息并输出到控制台
line = br.readLine();
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
1.3.6 【案例】聊天室案例V06
本案例需要实现服务端向所有的客户端推送消息,如下所示:、
本案例仅需要更新服务端,代码示意如下:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 聊天室服务端
*/
public class Server {
private ServerSocket serverSocket;
// 存放所有客户端输出流,用于广播消息
private List<PrintWriter> allOut = new ArrayList();
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程与该客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
Thread t = new Thread(clientHandler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 定义线程任务
* 目的是让一个线程完成与特定客户端的交互工作
*/
private class ClientHandler implements Runnable{
private Socket socket;
private String host; // 记录客户端的IP地址信息
public ClientHandler(Socket socket){
this.socket = socket;
// 通过socket获取远端计算机地址信息
this.host = socket.getInetAddress().getHostAddress();
}
public void run(){
try{
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
// 将该输出流存入allOut中
allOut.add(pw);
String message = null;
String name = Thread.currentThread().getName();
while ((message = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + message);
// 将消息推送给所有客户端
for(PrintWriter o: allOut){
o.println(this.host +"说:"+message);
}
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
1.3.7 【案例】聊天室案例V07
在上一个版本中,客户端需要先发消息,才能接收到一条推送的消息,这与聊天室的需求不符。本案例继续改进:实现客户端收发消息的分离,不论是否发送消息,都可以接收到完整的推送消息。逻辑如下所示:
本案例仅需要更新客户端,代码示意如下:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
private Socket socket;
public Client() {
try {
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
//启动读取服务端发送过来消息的线程
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.setDaemon(true);
t.start();
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
/**
* 该线程负责接收服务端发送过来的消息
*/
private class ServerHandler implements Runnable{
public void run(){
//通过socket获取输入流读取服务端发送过来的消息
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String line;
//循环读取服务端发送过来的每一行字符串
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2 反射
2.1 反射概述
2.1.1 什么是反射
反射机制(Reflection)是程序在运行时能够获取自身的信息。在Java中,只要给定类的名字,就可以通过反射机制来获取类的所有属性和方法。
反射的作用:
- 可以动态查看一个类或对象的所有属性和方法,包括使用private修饰的属性和方法
- 可以动态加载类
- 可以动态的创建一个类的实例
- 可以动态调用一个对象的方法
2.1.2 反射的优缺点
反射的优点:
- 反射允许我们在程序运行期间获得类的信息并操作一个类中的方法,因此可以提高代码的灵活性和扩展性
- 反射是Java中很多高级特性的基础,比如后面会介绍的注解、动态代理等特性
- 在很多框架中,对反射技术的使用也非常多,比如大名鼎鼎的Spring框架、各类ORM框架、RPC框架等
反射的缺点:
- 反射的代码的可读性和可维护性都比较低
- 反射的代码执行的性能低
开发者应该在业务代码中尽量避免使用反射。但是,作为一个合格的Java开发者,需要具备读懂中间件和框架中反射代码的能力和使用反射解决特定问题的能力。
2.1.3 反射API
Java提供了反射相关的API,核心是java.lang.Class类,用于加载类和获取类的相关信息。
Java 反射 API 位于 java.lang.reflect 包中,主要包括:
- Constructor类:用来描述一个类的构造方法
- Field类:用来描述一个类的成员变量
- Method类:用来描述一个类的方法
- Modifier类:用来描述类内各元素的修饰符
- Array:用来对数组进行操作
2.2 Class类
2.2.1 Class类概述
java.lang.Class类是Java反射机制的基础。从面向对象编程的角度,Class类是对Java程序中的“类”进行的抽象,每个Class的对象代表一个被JVM加载到内存中的“类”。
在程序运行时,JVM首先检查要加载的类对应的Class对象是否已经创建。如果没有创建,JVM会根据类名查找.class文件,将其加载到内存中,并创建相应的Class对象。如下图所示:
需要注意,Class类的构造器被设计为私有的,也就是说开发者不能主动创建Class类的对象。Class类的对象仅能由JVM创建。
2.2.2 类加载器
类加载器(Class Loader)是JVM的一个子系统,负责将class文件加载到内存中,然后在堆中创建一个代表这个类的Class对象,作为方法区中类数据的访问入口。
2.2.3 获取Class对象
开发者可以通过4种方式获取一个Java类的Class对象:
- 调用对象的getClass()方法获取Class对象
- 根据类名.class获取Class对象
- 根据Class中的静态方法Class.forName()获取Class对象
- 通过类加载器ClassLoader加载类并获取Class对象
2.2.4 【案例】动态加载类示例
编写代码,实现动态加载类。代码示意如下:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
String str = "abc";
// 调用对象的getClass()方法获取Class对象
Class clazz1 = str.getClass();
System.out.println(clazz1.getName());
// 根据类名.class获取Class对象
Class clazz2 = String.class;
System.out.println(clazz2.getName());
// 根据Class中的静态方法Class.forName()获取Class对象
Class clazz3 = Class.forName("java.lang.String");
System.out.println(clazz3.getName());
// 通过类加载器ClassLoader加载类对象
ClassLoader classLoader = ReflectDemo1.class.getClassLoader();
Class clazz4 = classLoader.loadClass("java.util.ArrayList");
System.out.println(clazz4.getName());
}
}
2.2.5 动态创建对象
Class 提供了动态创建对象的方法:
Object newInstance()
newInstance方法将调用类信息中的无参数构造器创建对象。由于该方法在异常处理上存在缺陷,故在Java 9版本开始被标记为过期方法。
在Java 9及后续版本中,可以通过Constructor的API来动态创建对象。
clazz.getDeclaredConstructor().newInstance()
2.2.6 【案例】动态创建对象示例
编写代码,调用无参构造器动态创建实例。代码示意如下:
public class Student {
private String name;
private Integer age;
// 教材中省略无参构造器,带参构造器,get/set方法和toString方法
}
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
//1加载类对象
ClassLoader classLoader = ReflectDemo2.class.getClassLoader();
Class cls = classLoader.loadClass("jaf.day07.cases.reflect.Student");
//2通过类对象实例化
Object o1 = cls.newInstance();//调用无参构造器,已过期
System.out.println(o1);
Object o2 = cls.getDeclaredConstructor().newInstance();
System.out.println(o2);
// 3通过带参构造器实例化
Object o3 = cls.getDeclaredConstructor(String.class, Integer.class)
.newInstance("Tom", 18);
System.out.println(o3);
}
}
2.2.7 动态调用方法
Class 可以动态获取方法:
Method getMethod()
其中,返回的Method 代表方法信息,可以利用Method API获取方法对详细信息,如:方法名,返回值类型,参数类型列表等。
Method 还可以动态执行一个方法:
Object invoke(Object obj, Object… args)
- 参数1:obj 代表一个对象,该对象上一定包含当前方法,否则将出现调用异常;如果obj为null则抛出空指针异常
- 参数2:args 代表调用方法时候传递的实际参数,如果没有参数可以不用或者传递null,但是要注意参数的个数和类型必须和要调用的方法匹配,否则将出现参数错误异常
- 返回值:表示方法执行的结果,因为可能是任何类型,则其类型为Object,调用没有返回值的方法则返回值为null
当被调用方法执行出现异常时候抛出InvocationTargetException。
2.2.8 【案例】动态调用方法示例
编写代码,动态调用无参方法。代码示意如下:
import java.lang.reflect.Method;
import java.util.Scanner;
public class ReflectDemo3 {
public static void main(String[] args) throws Exception {
//实例化
Class cls = Student.class;
Object o = cls.getDeclaredConstructor().newInstance();
//调用方法
//1 通过类对象获取要调用的方法
Method method = cls.getMethod("toString");
//2 通过方法对象执行该方法
Object result = method.invoke(o);
System.out.println(result);
//3 调用带参方法
Method method1 = cls.getMethod("setName", String.class);
method1.invoke(o, "Jerry");
Method method2 = cls.getMethod("setAge", Integer.class);
method2.invoke(o, 18);
System.out.println(o);
}
}
2.3 注解
2.3.1 注解概述
Java注解(Annotation)是一种元数据(metadata)机制,可以将各种信息(例如类、方法、变量等)与程序元素相关联。Java注解可以用于许多目的,例如:
- 为代码提供元数据信息,例如文档、版本号等
- 为编译器提供指示,例如抑制警告、生成代码等
- 为运行时提供指示,例如启用事务、配置参数等
Java注解以@符号开头,后跟注解名称和注解元素。注解元素可以是单个值或一组值,类似于方法的参数。
Java提供了一些内置的注解,例如@Override、@Deprecated和@SupressWarnings等,也可以创建自定义的注解。
2.3.2 使用Java注解
使用Java注解的一般步骤如下:
1、定义注解
使用 @interface 关键字定义注解类型及其成员变量。
代码如下所示:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
@Target(ElementType.METHOD) 是Java注解中的一个元注解(meta-annotation),用于标记自定义注解的适用范围,指定了注解可以应用在哪些程序元素上。
- 本例中,@Target(ElementType.METHOD) 表示该注解只能应用于方法(Method)上。
- 换句话说,这个自定义注解只能标记在方法上,不能标记在其他程序元素(如类、字段、变量等)上。
@Retention(RetentionPolicy.RUNTIME) 是Java注解中的一个元注解,用于指定注解的生命周期。
- 本例中,@Retention(RetentionPolicy.RUNTIME) 表示该注解在运行时(Runtime)保留,可以通过反射机制获取注解的信息。
value() 是Java注解中的一个特殊方法,它是一种简化形式的注解成员变量,可以省略成员变量名,直接使用注解时赋值。
2、应用注解
在需要使用注解的地方,使用 @注解名 标记程序元素,并为注解成员变量赋值。代码如下所示:
public class MyClass {
@MyAnnotation("Hello, world!")
public void myMethod() {
// do something
}
}
在使用注解时,我们可以省略成员变量名,直接给 value 赋值,例如 @MyAnnotation("Hello, world!")。这等价于 @MyAnnotation(value = "Hello, world!")。
需要注意的是,如果注解中有多个成员变量,而我们只想设置其中的一个成员变量,那么在赋值时必须使用成员变量名,而不能省略成员变量名。
3、处理注解
使用反射机制获取程序元素上的注解,并根据注解的值进行处理。代码如下所示:
import java.lang.reflect.Method;
public class AnnotationDemo {
public static void main(String[] args) throws Exception {
//获取方法信息
Method method = MyClass.class.getMethod("myMethod");
//获取方法上的注解信息
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
//获取注解的值
String value = annotation.value();
System.out.println(value);
//检查方法上是否标注了@MyAnnotation注解
boolean hasAnnotation = method.isAnnotationPresent(MyAnnotation.class);
System.out.println(hasAnnotation);
}
}
method.getAnnotation(MyAnnotation.class) 是Java反射机制中的一个方法,用于获取指定方法(Method)上的指定注解(Annotation)。
上面的例子中,我们首先定义了一个自定义注解 MyAnnotation,它有一个成员变量 value。然后我们在 myMethod 方法上应用了 MyAnnotation 注解,并设置了 value 的值为 "Hello, world!"。
在main方法中,我们使用反射机制获取myMethod方法,并使用 method.getAnnotation(MyAnnotation.class) 方法获取该方法上的 MyAnnotation 注解。如果获取到了注解,我们就可以通过注解对象调用 value() 方法获取注解的值,即输出 "Hello, world!"。