UDP:首先应用层逻辑将传输的数据封装成一个数据报包,将数据报包交给传输层的DatagramSocket对象,然后DatagramSocket对象将数据报包传输给对端传输层的DatagramSocket对象,然后对端的DatagramSocket对象将数据报包交给应用层逻辑,应用层经过解析得到数据。
TCP:客户端和服务器端建立好连接,通过输入流和输出流传输数据。
注意事项:
1. ServerSocket 仅仅只处理客户端的连接请求,并且其accept方法在接收到连接请求之后,会为本次连接创建新的Socket对象
2. 是服务器端的Socket和客户端的Socket对象,之间建立连接并传输数据
我们不去关心,不同协议实现上的不同,只从使用基于TCP协议实现的Socket的角度来说,基于TCP的socket使用起来,似乎要简单许多!
为什么这么说呢?因为基于TCP的Socket的数据传输是基于流来实现的。
这样一来,目的主机就可以被看做一个普通的文件!!
客户端:
1.建立客户端的Socket对象,并明确要连接的服务器。
2.如果对象建立成功,就表明已经建立了连接.就可以在该通道通过IO进行数据的读取和写入
根据需要从socket对象中获取输入,或输出流
3.向流中读取或写入数据
4.释放资源
1.服务器给客户端反馈
Client:
package com.cskaoyan.tcp.basic;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*
Socket: 此类实现客户端套接字(也可以就叫“套接字”)。
构造方法:
该Socket对象本身,绑定的本机ip + 一个创建该对象时随机分配的端口号
Socket(String host, int port)
创建一个流套接字,并将其连接到 指定主机上 的 指定端口号。
host和port,指的要连接的对端的ip地址和端口号
host - 主机名或ip地址字符串,或者为 null,表示回送地址(127.0.0.1)。
port - 端口号。
如果在没有服务器端的时候,直接运行客户端:
java.net.ConnectException: Connection refused: connect
原因是还没有和服务器端建立连接
*/
public class Client {
public static void main(String[] args) throws IOException {
//1.建立客户端的Socket对象,并明确要连接的服务器。
Socket socket = new Socket("192.168.0.100", 11111);
//如果要发送数据,那么就从Socket对象中获取,输出流
OutputStream outputStream = socket.getOutputStream();
//通过输出流,向对端发送数据
String s = "hello, tcp";
// 利用输出流的write方法传输数据
outputStream.write(s.getBytes());
//关闭资源, Socket中的流,不用专门关闭,socket会负责关闭它所持有的流
socket.close();
}
}
服务器端:
1.创建ServerSocket对象,在指定端口,监听客户端连接请求
2.收到客户端连接请求后,建立Socket连接
3.如果连接建立成功,就表明已经建立了数据传输的通道.就可以在该通道通过IO进行数据的读取和写入
从socket中根据需要获取输入,或输出流
4.根据需要向流中写入数据或从流中读数据
5.释放资源
Server:
package com.cskaoyan.tcp.basic;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*
ServerSocket(int port) // 本机ip + port
创建绑定到特定端口的服务器套接字。
public Socket accept()
侦听并接受到此套接字的连接(请求)。此方法在连接传入之前一直阻塞。
注意: BindException: Address already in use
同一个端口号,只能属于一个进程
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.创建ServerSocket对象,在指定端口,监听客户端连接请求
ServerSocket serverSocket = new ServerSocket(11111);
//2.收到客户端连接请求后,建立Socket连接
Socket socket = serverSocket.accept();
//3. 在建立好的连接中,通过在服务器端的Socket对象中,获取流,来进行数据传输
InputStream inputStream = socket.getInputStream();
//4. 在输入流中读取客户端发送的数据
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
//解析接收到的数据
String s = new String(buffer, 0, len);
System.out.println(s);
//关闭资源, Socket中的流,不用专门关闭,socket会负责关闭它所持有的流
socket.close();
serverSocket.close();
}
}
注意:两次启动Server端也会报错,BindException: Address already in use。原因同UDP编程中的Receiver,因为serverSocket.accept()方法是一个阻塞方法,阻塞方法意味着进程还没有结束,第二次运行Server端就会新建一个进程,而同一台主机上的一个端口号只能绑定一个进程。
············································································································································································································
2.客户端键盘录入,服务器输出文本文件
package com.cskaoyan.tcp.exercise03;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*
* 客户端键盘录入,服务器输出文本文件
*/
public class Client {
public static void main(String[] args) throws IOException {
//1.建立客户端的Socket对象,并明确要连接的服务器。
Socket socket = new Socket("192.168.1.2", 10086);
//如果要发送数据,那么就从Socket对象中获取,输出流
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
//通过输出流,向对端发送数据
String s = "hello, tcp";
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while ((s = br.readLine()) != null) {
// 利用输出流的write方法传输数据
bw.write(s);
bw.newLine();
bw.flush();
}
//关闭标准输入流
br.close();
//关闭资源, Socket中的流,不用专门关闭,socket会负责关闭它所持有的流
socket.close();
}
}
Servet:
package com.cskaoyan.tcp.exercise03;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.创建ServerSocket对象,在指定端口,监听客户端连接请求
ServerSocket serverSocket = new ServerSocket(10086);
//2.收到客户端连接请求后,建立Socket连接
Socket socket = serverSocket.accept();
byte[] buffer = new byte[1024];
InputStream in = socket.getInputStream();
FileOutputStream fos = new FileOutputStream("record.txt");
//准备文件输出流,将客户端发送的内容输出到文件中去
int len;
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
//关闭资源, Socket中的流,不用专门关闭,socket会负责关闭它所持有的流
fos.close();
socket.close();
serverSocket.close();
}
}
3.客户端文本文件,服务器输出到控制台
Client:
package com.cskaoyan.tcp.exercise04;
import java.io.*;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 15:57
*
* 3.客户端文本文件,服务器输出到控制台
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("192.168.1.2",11111);
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
//读取文件内容
BufferedReader br = new BufferedReader(new FileReader("F:\\test\\test01.txt"));
//按行将文本文件数据输出到服务器端
String s;
while((s=br.readLine())!=null){
bw.write(s);
bw.newLine();
}
bw.close();
br.close();
socket.close();
}
}
Server:
package com.cskaoyan.tcp.exercise04;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 15:57
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.
ServerSocket serverSocket = new ServerSocket(11111);
//2.
Socket socket = serverSocket.accept();
//3.
InputStream is = socket.getInputStream();
//4.
byte[] bytes = new byte[1024];
int len;
while((len=is.read(bytes))!=-1){
System.out.println(new String(bytes,0,len));
}
//5.
//is.close(); is流是从socket.getInputStream中读取的,
// 不需自己要关闭,将socket关闭即可
socket.close();
serverSocket.close();
}
}
4.客户端文本文件,服务器输出文本文件(文件上传功能)
Client:
package com.cskaoyan.tcp.exercise05;
import java.io.*;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 15:57
*
* 3.客户端文本文件,服务器输出到控制台
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("192.168.1.2",11111);
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
//读取文件内容
BufferedReader br = new BufferedReader(new FileReader("F:\\test\\test01.txt"));
//按行将文本文件数据输出到服务器端
String s;
while((s=br.readLine())!=null){
bw.write(s);
bw.newLine();
}
bw.flush();
br.close();
//当文件上传完之后
socket.shutdownOutput();
//接收服务器端的反馈信息
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
// Socket中的流的read方法都是阻塞方法
int len = in.read(buf);
System.out.println("接收到反馈:" + new String(buf, 0, len));
socket.close();
}
}
Server:
package com.cskaoyan.tcp.exercise05;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 15:57
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.
ServerSocket serverSocket = new ServerSocket(11111);
//2.
//accept方法是一个阻塞方法
Socket socket = serverSocket.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream("F:\\test\\test13.txt");
byte[] bytes = new byte[1024];
int len;
//Socket对象中的read是一个阻塞方法
while((len=is.read(bytes))!=-1){
fos.write(bytes,0,len);
}
//给客户端发送反馈信息
OutputStream os = socket.getOutputStream();
os.write("文件上传完毕!".getBytes());
//5.
//is.close(); is流是从socket.getInputStream中读取的,
// 不需自己要关闭,将socket关闭即可
fos.close();
socket.close();
serverSocket.close();
}
}
文件传输完毕后,我们让服务器端给客户端发送反馈信息:文件传输完毕!,但真正执行时,Client的控制台却没有反馈信息,并且Client和Server都处于阻塞状态。why?
分析一下代码:
首先启动Server端,serverSocket.accept();是一个阻塞方法,会让Server端处于阻塞状态。
然后启动Client端,会和Server端建立连接请求,创建一个文件输出流和文件输入流,将txt文件内容传输到服务端。
然后读取反馈信息,由于Socket中的流的read方法都是阻塞方法,所以当执行到int len = in.read(buf);时,Client端阻塞在这里,就等着Server端给他发送反馈数据。
再看Server端,会将读取到的内容写入test13.txt文件中。
但是,Socket对象中的read是一个阻塞方法
while((len=is.read(bytes))!=-1){
fos.write(bytes,0,len);
}
也就是说这个循环永远不会结束,所以当传输完毕时,Server的代码就会永远阻塞在len=is.read(bytes))!=-1这里。
也就是说,Server在等待Client发送从test01.txt文件中读取的文件内容。而Client已经读取完test01.txt中的所有内容并发送给了Server。目前,Client在等待Server发送反馈信息。
所以,现在出现了相互等待的情况。我们看到了Client和Server的程序都没有结束,但文件内容已经上传完毕。
调动shutdown后,单方面的关闭Client端的输出流,代表不会向Server端传输数据了,此时Server端自然就不会卡在read处。
- public void shutdownOutput()
禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
当Client关闭输出流后,会发送TCP 的正常连接终止序列,Server端一看,Client端要终止传输了,也就不再等待。
··············································································································································································································
之前讲的都是一个客户端对应一个服务端,但其实一个服务器端可以对应多个客户端。
改进一:
Client端代码不变,Server端代码如下:
package com.cskaoyan.tcp.exercise06;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 15:57
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.
boolean flag = true;
ServerSocket serverSocket = new ServerSocket(11111);
while(flag){
//2.
//accept方法是一个阻塞方法
Socket socket = serverSocket.accept();
uploadFile(socket);
sendResponse(socket);
//5.
socket.close();
}
serverSocket.close();
}
private static void sendResponse(Socket socket) throws IOException {
//给客户端发送反馈信息
OutputStream os = socket.getOutputStream();
os.write("文件上传完毕!".getBytes());
os.close();
}
private static void uploadFile(Socket socket) throws IOException {
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream("F:\\test\\test13.txt");
byte[] bytes = new byte[1024];
int len;
while((len=is.read(bytes))!=-1){
fos.write(bytes,0,len);
}
fos.close();
}
}
这样会有一个问题:
假如Client端网速不同,上传文件大小不同,显然客户端3虽然传输时间很少,但它却要等待很长时间。
··············································································································································································································
如何改善呢?
同步建立连接。异步传输数据
会发现整个代码其实分为两步,连接请求和数据传输。
我们应当把数据传输放在子线程中执行。而Server端一直建立连接建立连接。Server端在三个线程中分别接受client端传输的数据。
public static void main(String[] args) throws IOException {
//1.
boolean flag = true;
ServerSocket serverSocket = new ServerSocket(11111);
while(flag){
//2.
//accept方法是一个阻塞方法
//连接请求
Socket socket = serverSocket.accept();
//-----------------------------------------------------------------------
//数据传输
uploadFile(socket);
sendResponse(socket);
//5.
socket.close();
}
serverSocket.close();
}
接下来,上最终版代码:
UploadServerThread:
package com.cskaoyan.tcp.exercise07;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*/
public class UploadServerThread extends Thread {
private Socket socket;
private String fileName;
public UploadServerThread(Socket socket, String fileName) {
this.socket = socket;
this.fileName = fileName;
}
@Override
public void run() {
//开始读取数据
InputStream inputStream = null;
FileOutputStream fos = null;
try {
inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
fos = new FileOutputStream(fileName);
// Socket对象中的read方法都是阻塞方法
String s;
while ((s = br.readLine()) != null) {
fos.write(s.getBytes());
fos.write('\n');
}
//发送响应
OutputStream out = socket.getOutputStream();
out.write("文件上传完毕".getBytes());
out.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//自己创建的流,自己关闭
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
UploadClientThread:
package com.cskaoyan.tcp.exercise07;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*/
public class UploadClientThread extends Thread{
private File uploadFile;
public UploadClientThread (File uploadFile) {
this.uploadFile = uploadFile;
}
@Override
public void run() {
Socket socket = null;
try {
socket = new Socket("localhost", 9801);
BufferedWriter bw = new BufferedWriter
(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader br = new BufferedReader(new FileReader(uploadFile));
//按行,将文本文件数据,输出到服务器端
String s;
while ((s = br.readLine()) != null) {
bw.write(s);
bw.newLine();
}
bw.flush();
br.close();
//当文件上传完之后
socket.shutdownOutput();
//接收服务器端的反馈信息
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
// Socket中的流的read方法都是阻塞方法
int len = in.read(buf);
System.out.println("接收到反馈:" + new String(buf, 0, len));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Server:
package com.cskaoyan.tcp.exercise07;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*
* 多个客户端传输文件到一个服务器
*/
public class Server {
//给每一个上传文件一个唯一编号
static long fileId = 0;
public static void main(String[] args) throws IOException {
boolean flag = true;
ServerSocket serverSocket = new ServerSocket(9801);
while (flag) {
// accept是一个阻塞方法
Socket socket = serverSocket.accept();
//对于每一个上传文件,都要存储到一个服务器端的文件中,所以也就说对每一个上传的文件
//起一个不同的文件名
String filename = "F:\\test\\" + System.currentTimeMillis() + "-" + fileId++ + ".txt";
new UploadServerThread(socket , filename).start();
}
// 7 * 24小时工作 服务器,服务器是不会关的,不能让服务器宕机
serverSocket.close();
}
}
Client:
package com.cskaoyan.tcp.exercise07;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* @author zhangshuai@Cskaoyan.onaliyun.com on 2020/5/5.
* @version 1.0
*
* public void shutdownOutput()
禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
*/
public class Client {
public static void main(String[] args) throws IOException {
File[] uploadFiles = {new File("F:\\test\\test01.txt"),
new File("F:\\test\\test01.txt"),
new File("F:\\test\\test01.txt")};
for (int i = 0; i < uploadFiles.length; i++) {
new UploadClientThread(uploadFiles[i]).start();
}
}
}
··············································································································································································································
作业:
- 参考com.cskaoyan.tcp包下的exercise07中的代码,实现多线程(每个线程代表一个客户端)上传,多个jpg图片文件的代码,但是要注意,exercise04中实现的是文本文件的上传,但是我们只能用字节流处理图片文件
TCPSender:
package com.cskaoyan.tcp.hoework2;
import java.io.IOException;
/**
* @author shihao
* @create 2020-05-06 21:25
*/
public class TCPSender {
public static void main(String[] args) throws IOException, InterruptedException {
String[] files = {"upload.jpg", "upload1.jpg"};
for (int i = 0; i < files.length; i++) {
new UploadSenderThread(files[i]).start();
}
}
}
UploadSenderThread:
package com.cskaoyan.tcp.hoework2;
import java.io.*;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 21:26
*/
public class UploadSenderThread extends Thread {
public String fileName;
public UploadSenderThread (String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
//1. 创建tcp的发送端(客户端)Socket对象,并且和指定的 接收端ip和端口的 socket建立连接
Socket socket = null;
byte[] buffer = new byte[1024];
BufferedInputStream bis = null;
try {
socket = new Socket("192.168.0.100", 10086);
System.out.println(socket.getLocalAddress() + ":" + socket.getLocalPort());
//建立好连接之后要发送数据
// 从已经建立好连接的Socket对象中,获取传输数据的输出流
OutputStream out = socket.getOutputStream();
BufferedOutputStream bw = new BufferedOutputStream(out);
//创建文件输入流,读取文件内容
FileInputStream fis = new FileInputStream(fileName);
bis = new BufferedInputStream(fis);
int len;
while ((len = bis.read(buffer)) != -1) {
bw.write(buffer, 0, len);
}
//刷新缓冲输出流缓冲区
bw.flush();
//禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
socket.shutdownOutput();
接收一下服务器端的反馈
InputStream inputStream = socket.getInputStream();
//如果服务器端没有发送反馈,发送端,会卡在这里
buffer = new byte[1024];
len = inputStream.read(buffer);
System.out.println(new String(buffer, 0, len));
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭socket
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
//关闭读取文件的缓冲流
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
TCPReceiver:
package com.cskaoyan.tcp.hoework2;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 21:27
*/
public class TCPReceiver {
//与uploadFileName配合解决文件重名问题
// 给上传的文件编个号相当于(或者只是用该文件编号来解决上传文件的命名冲突问题也可以)
static long fileId = 0L;
public static void main(String[] args) throws IOException {
boolean flag = true;
ServerSocket serverSocket = new ServerSocket(10086);
//ServerSocket 只是接受,连接请求,并建立连接
// accept方法是一个阻塞方法
Socket socket = null;
while (flag) {
socket = serverSocket.accept();
//解决客户端文件重名问题
String uploadFileName = System.currentTimeMillis() + "-" + fileId++;
new UploadReceiverThread(socket, uploadFileName).start();
}
serverSocket.close();
}
}
UploadReceiverThread:
package com.cskaoyan.tcp.hoework2;
import java.io.*;
import java.net.Socket;
/**
* @author shihao
* @create 2020-05-06 21:27
*/
public class UploadReceiverThread extends Thread {
private Socket socket;
private String fileName;
public UploadReceiverThread(Socket socket, String fileName) {
this.socket = socket;
this.fileName = fileName;
}
@Override
public void run() {
//从建立好连接的服务器端(接收端)的Socket对象中拿得到,输入流,准备通过输入流接收数据
InputStream inputStream = null;
byte[] buffer = new byte[1024];
BufferedOutputStream bos = null;
try {
inputStream = socket.getInputStream();
System.out.println(socket.getLocalPort());
BufferedInputStream bis = new BufferedInputStream(inputStream);
//创建文件字节输出流
FileOutputStream fos = new FileOutputStream(fileName + ".jpg");
bos = new BufferedOutputStream(fos);
int len;
// Socket 中的InputStream的read也是一个阻塞方法
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
//刷新缓冲字节流缓冲区
bos.flush();
//发送反馈信息
OutputStream outputStream = socket.getOutputStream();
outputStream.write("文件上传完毕".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭写文件内容的缓冲输出流
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
//关闭socket
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
············································································································································································································
反射
-
比如说现在我们持有了一个对象
A a = new A();
对于该对象,在不看A类代码前提下,我们根本不知道如何访问使用a对象
原因在于,我们不知道a对象中,有哪些成员(类中定义的) -
我们自己定义的类,jvm天生是不认识的,但是jvm有办法认识这些类对应的数据类型
-> 通过加载并解析类对应的字节码文件 -
在jvm类加载的过程中,会对每一个类生成一个Class对象,一个Class对象中就包含了
一个类定义的完整信息,所以我们可以在程序运行的过程中,通过访问某个类对应的Class对象
通过访问Class对象,获取到相应的类型信息说白了,反射,就是用来获取运行时类型信息(Class对象):
- 构造方法 -> 创建对象
- 成员变量 -> 在该类型任意对象上访问该成员变量(即使是private成员变量)
- 成员方法 -> 在该类型的任意对象,调用该成员方法(即使是private成员方法)
所以,这阶段我们的学习步骤是:
- 简单了解类加载过程(经过类加载才有Class对象)
- 如何获取Class对象?(反射技术的起点)(三种方式)
- 如何获取Class对象中,所包含的类定义相关信息(成员方法、成员变量)
············································································································································································································
1. 简单了解类加载过程
类的加载
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。
加载
通过一个类的全限定名来获取定义此类的二进制字节流
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
连接
验证:确保被加载类的正确性(如果一个文件的前四个字是 ca fe ba be,jvm才认为它是字节码文件,才会加载)
准备:负责为类的静态成员分配内存并设置默认初始化值
解析:将类中的符号引用替换为直接引用(简单理解:将引用变量的值替换为内存地址)
初始化
给静态成员变量赋初值,执行静态代码块内容(所以说静态代码块随着类加载而执行)
类加载时机
创建类的实例(首次创建该类对象)
访问类的静态变量
调用类的静态方法
使用反射方式来强制创建某个类或接口对应的java.lang.Class对象,如后面讲的:static Class forName(String className)
初始化某个类的子类,会先触发父类的加载
直接使用java.exe命令来运行某个主类
类加载器
完成通过一个类的全限定名来获取描述此类的二进制字节流的功能
类加载器的分类(jdk8)
Bootstrap ClassLoader 根类加载器
负责Java核心类的加载,JDK中JRE的lib目录下rt.jar
Extension ClassLoader 扩展类加载器
负责JRE的扩展目录中jar包的加载,在JDK中JRE的lib目录下ext目录
Sysetm ClassLoader 系统类加载器
负责加载自己定义的Java类
那.jar文件是啥呢?jar包中方的都是一些写好的类对应的字节码文件,相当于我们把字节码文件打包了。
如何查看其中的字节码文件呢?可以将jar包当做普通的压缩文件解压,然后查看。
所以为什么很多类我们可以直接拿来用呢,因为这些类已经在程序运行之前,利用根类加载器,把java程序运行时可能用到的核心类的字节码文件已经加载到内存中来了。
············································································································································································································
回忆一下,在之前我们如何认识类型? source code
也就是说,之前我们认识类型都是在程序运行之前
也就是说,如果之前我们没有看过某对象所属类的代码,那么在程序运行期间,对于该对象我们将“一无所知”
综上所述,我们就有一个问题了,我们能否在运行时发现和使用类型信息呢?
当然可以,今天所学习的Java反射技术,就可以帮助我们在运行时发现和使用运行时的类型信息。
其实仔细想想,所谓的反射技术,他又能如何实现运行时发现和使用类型信息的呢?仔细想想Java程序的运行过程。
问题的关键在于,Java中类型(即类)信息在运行时如何表示?
不难想到Java中的类型信息,都是由Class对象来表示的。
因此,反射技术也没有什么特别,主要就是研究Class及与它对象及其使用。
学习反射技术,你就像站在了上帝视角!没有你不能知道和使用的东西。
············································································································································································································
2. 如何获取Class对象?
通过对象
通过类的字面值常量
通过静态方法
public class Demo1 {
public static void main(String[] args) throws ClassNotFoundException {
//1.通过对象 Object类中的方法 getClass()
Demo1 demo = new Demo1();
System.out.println(demo.getClass());
//2.通过类的字面值常量
Class class1 = Demo1.class;
System.out.println(class1);
//3.Class.forName(String className)方法
Class class2 = Class.forName("com.cskaoyan.mytest.Demo1");
System.out.println(class2);
}
}
Class类中定义的静态方法
static Class forName(String className)
className - 所需类的完全限定名(包名+类名)。
// 在某个类没被加载的时候,可以利用该方法,强制加载目标类(通过类名指定),返回和该类对应的Class对象
// 如果这个类已经被加载了,就会直接返回这个类对应的Class对象。
注意事项:
- Class.forName方法,参数必须是一个类全类名
- 类名.class触发的类加载过程,不完整的类加载过程(静态代码块中的代码不会执行)
而Class.forName触发的是完整的类加载过程 - 开发中,多用第三种方式,来获取某个类的Class对象, 因为这种方式很灵活,该方法可以根据不同全类名,加载全类名
第一二种如果以后类名更改的话需要改代码,而第三种只需要改参数
三者比较:第一种必须得创建对象,第二种是不完整的类加载过程,第三种不用创建对象且是完整的类加载
············································································································································································································
第三种可以通过配置文件的方式来进行修改:
选中Network&&Reflection这个Moudle,右击-----new------Resource Bundle,可以新建一个.properties文件。
配置文件中的内容读取到内存中只会被当做字符串,也就是说key和value的类型一定是String类型。
package com.cskaoyan.mytest;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
/**
* @author shihao
* @create 2020-05-07 13:11
*/
public class Demo02 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.读取配置文件中的信息
Properties properties = new Properties();
//2.创建指向目标文件的文件输入流
FileInputStream fis = new FileInputStream("config.properties");
//3.通过load方法,将配置文件中的每一条配置信息内容,读取并保存到properties的一个存储单元中
//load方法会利用已经创建好的FileInputStream读取配置文件内容,并保存到Properties中
properties.load(fis);
//4.获取类名
String className = properties.getProperty("className");
System.out.println(className);
//5.利用Class.forName方法加载目标类
Class aclass = Class.forName(className);
System.out.println(aclass);
}
}
class LoadClass {
static {
System.out.println("hello class");
}
}
这段代码运行出来后说找不到文件config.properties,后来发现是工作目录的问题,如何解决呢?
首先你要知道你的路径----点击Run----找到EditConfigurations…找到Application----在Working directory:-----------设置项目路径(一般设置成当前工作目录的路径即可,或设置成 $
M
O
D
U
L
E
D
I
R
MODULE_DIR
MODULEDIR $)----点击Apply—点击OK就可以运行了,出现问题一般是这个没设置好。