一、Java IO流的实现机制
流的作用和本质:数据传输
缓冲流的作用:
- 由原来的一个一个字节读取,变成放到缓冲流(可配置字节数)进行批量操作,这样和底层的硬盘操作频率变少,效率提高。”从应用的角度,任何减少磁盘活动的策略都有帮助,例如使用带缓存的输入、输出流以减少读、写操作次数用以减少磁盘交互。
用缓冲区的速度是不用缓冲区的几十倍
使用方法
字节流
File file1 = new File(path1);
File file2 = new File(path2);
//输入输出文件流
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
//输入输出缓冲流
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
字符流
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
在Java中,输入和输出都被称为抽象的流,流可以看做是一组有序的字节集合,就是数据在两个设备之间传输。
流的本质本来就是数据传输,根据类型的不同可分为两大类。
字节流和字符流。
字节流是以字节(8bit)为单位包含两个抽象类:InputStream(输入流)和OutputStream(输出流)。
字符流是以字符(16bit)为单位,根据码表映射字符,一次可以读多个字节,包含了两个抽象类:Reader(输入流)和Writer(输出流)。
IO 流的构成如图所示,IO类在设计时采用了Decorator(装饰者)设计模式
两者的主要区别字节流在处理的时候不会用到缓存,而字符需要用到缓存。
字节流继承了InputStream(输入流)和OutputStream(输出流),字符流继承了Reader(输入流)和Writer(输出流),流的作用主要是为了改善程序的性能并且使用方便。
假如要设计一个输入流的类,该类的作用是把大写的的字母转换成小写的字母,把小写字母转换成大写的字母,我通过继承装饰类(FilterInputStream)来实现一个装饰类。
public class MyInputStream extends FilterInputStream{
public MyInputStream(InputStream in) {
super(in);
}
public int read() throws IOException{
int b = 0;
if((b = super.read()) != -1) {
//小写变大写
if(Character.isLowerCase((char)b)) {
return Character.toUpperCase((char)b);
//大写变小写
}else if(Character.isUnicodeIdentifierPart((char)b)){
return Character.toLowerCase((char)b);
//不是字母不变
}else {
return b;
}
}else {
return -1;
}
}
}
public class Test {
public static void main(String[] args) {
int b;
try {
InputStream is = new MyInputStream( new BufferedInputStream( new FileInputStream("D:"+File.separator+"日常存档"+File.separator+"Test11.txt")));
while((b = is.read())>=0) {
System.out.print((char)b);
}
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println(e.getMessage());
}
}
}
在test文件中放入asdfASDF123
测试结果:
ASDFasdf123
管理文件和目录的类是什么
目录是管理文件的特殊机制,同类文件保存在同一个目录下不仅可以简化文件管理,而且还可以提高工作效率。Java 语言在 java.io 包中定义了一个 File 类专门用来管理磁盘文件和目录。
每个 File 类对象表示一个磁盘文件或目录,其对象属性中包含了文件或目录的相关信息。通过调用 File 类提供的各种方法,能够创建、删除、重名名文件、判断文件的读写权限以及是否存在,设置和查询文件的最近修改时间等。不同操作系统具有不同的文件系统组织方式,通过使用 File 类对象,Java 程序可以用与平台无关的、统一的方式来处理文件和目录。
构造方法 | 功能描述 |
---|---|
public File(String path) | 指定与 File 对象关联的文件或目录名,path 可以包含路径及文件和目录名 |
public File(String path, String name) | 以 path 为路径,以 name 为文件或目录名创建 File 对象 |
public File(File dir, String name) | 用现有的 File 对象 dir 作为目录,以 name 作为文件或目录名创建 File 对象 |
public File(URL ui) | 使用给定的统一资源定位符来定位文件 |
方法 | 作用 |
---|---|
File(String pathname) | 根据指定的路径创建一个File对象 |
boolean createNewFile() | 若目录或者文件不存在,就返回false,否则创建文件或者文件夹 |
boolean delete() | 删除文件或者文件夹 |
boolean isFile() | 判断这个对象是否是文件 |
boolean isDirectory() | 判断这个对象是否是文件夹 |
File[] listFiles() | 如果对象是目录,就返回目录中所有的文件File对象 |
boolean mkdir() | 根据当前对象指定的路径创建目录 |
boolean exists() | 判断对象对应的文件是否存在 |
boolean renameTo(File newFile) | 将文件重命名为指定的文件名 |
string getName() | 返回文件名或目录名的字符串 |
string getPath() | 返回文件或目录路径的字符串 |
boolean equals(File f) | 比较两个文件或目录是否相同 |
如何列出某个目录下的所有目录和文件?
假设目录 D:\Html5/H5页面设计
public class TestFile {
public static void main(String[] args) {
// TODO Auto-generated method stub
File file = new File("D:\\Html5/H5页面设计");
//判断目录是否存在
if(! file.exists()) {
System.out.println("dirctory is empty");
return;
}
File[] fileList = file.listFiles();
for (int i = 0; i < fileList.length; i++) {
//判断是否为目录
if(fileList[i].isDirectory()) {
System.out.println("dirctory is:"+ fileList[i].getName());
}else {
System.out.println("file is :"+ fileList[i].getName());
}
}
}
}
运行结果:
dirctory is:ajax
file is :h5.css
file is :开端.html
dirctory is:拖动垃圾桶
dirctory is:时钟
dirctory is:注册页面设计
file is :表格制作.html
dirctory is:问卷调查
Java Socket是什么
可以用来实现实时通讯功能和上传下载文件进度条功能等
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个socket。socket也就是套接字,可以用来实现不同虚拟机或者不同计算机之间的通信。在Java语言中,socket可以分为两种类型:面向连接的socket通信协议(TCP)和面向无连接的socket通信协议(UDP)。任何一个socket都是用IP地址与端口号唯一确定的。
其中UDP是一种面向无连接的传输层协议。UDP不关心对端(客户端/服务端)是否真正收到了传送过去的数据。
如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。
UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。
在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。
基于TCP的通信过程如下:
首先,服务器端Listen(监听)指定的某个端口是否有连接请求;
其次,客户端向服务器端发出Connect(连接)请求;
最后,服务器端向客户端返回Accept(接受)消息。
一个连接就建立起来了,回话随即产生。服务器端与客户端都可以通过Send,Write等方法与对方通信。
首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,
此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。
Socket的生命周期有三部分:打开Socket,使用Socket收发的数据与关闭Socket。
在Java 语言中,可以使用ServerSocket作为服务器端,Socket作为客户端来实现网络通信。
socket通信基本示例:
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
//初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取一行数据
String str = bufferedReader.readLine();
//输出打印
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package socket.socket1.socket;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str = "你好,这是我的第一个socket";
bufferedWriter.write(str);
//刷新输入流
bufferedWriter.flush();
//关闭socket的输出流
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行步骤:
先执行服务端
服务端执行到Socket socket = serverSocket.accept();会等待目标客户端连接
然后执行客户端 记住客户端一定要关闭socket的输出流socket.shutdownOutput(); 不然服务端不知道客户端什么时候发完信息,还觉得客户端没发完会一直等待其实客户端执行完了结果报错
然后输出:
Connected to the target VM, address: '127.0.0.1:5564', transport: 'socket'
你好,这是我的第一个socket
Disconnected from the target VM, address: '127.0.0.1:5564', transport: 'socket'
结束socket输出流两种方法和区别:
- socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。
- socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。
while循环连续接受客户端信息
上面的示例中scoket客户端和服务端固然可以通信,但是客户端每次发送信息后socket就需要关闭,下次如果需要发送信息,需要socket从新启动,这显然是无法适应生产环境的需要。
比如在我们是实际应用中QQ,如果每次发送一条信息,就需要重新登陆QQ,我估计这程序不是给人设计的,那么如何让服务可以连续给服务端发送消息?
下面我们通过while循环进行简单展示:
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
//初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine()) != null) {
//输出打印
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket = new Socket("127.0.0.1", 9999);
//通过socket获取字符流
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
//在控制台输入自己想发送的话
String str = bufferedReader.readLine();
bufferedWriter.write(str);
//标识进行判断,如果接受到这个标识,表明数据已经传入完\n
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过一个while 循环,就可以实现客户端不间断的通过标准输入流读取来的消息,发送给服务端。
在这里有个细节,大家看到没有,我客户端没有写socket.close() 或者调用socket.shutdownOutput();
服务端是如何知道客户端已经输入完成了?服务端接受数据的时候是如何判断客户端已经输入完成呢?
这就是一个核心点,双方约定一个标识,当客户端发送一个标识给服务端时,表明客户端端已经完成一个数据的载入。
而服务端在结束数据的时候,也通过这个标识进行判断,如果接受到这个标识,表明数据已经传入完成,那么服务端就可以将数据度入后显示出来。
在上面的示例中,客户端端在循环发送数据时候,每发送一行,添加一个换行标识“\n”标识,在告诉服务端我数据已经发送完成了。
而服务端在读取客户数据时,通过while ((str = bufferedReader.readLine())!=null) 去判断是否读到了流的结尾,负责服务端将会一直阻塞在哪里,等待客户端的输入。
通过while方式,我们可以实现多个客户端和服务端进行聊天。但是,下面敲黑板,划重点。
由于socket通信是阻塞式的,假设我现在有A和B俩个客户端同时连接到服务端的上,
当客户端A发送信息给服务端后,那么服务端将一直阻塞在A的客户端上,不同的通过while循环从A客户端读取信息,
此时如果B给服务端发送信息时,将进入阻塞队列,直到A客户端发送完毕,并且退出后,B才可以和服务端进行通信。
简单地说,我们现在实现的功能,虽然可以让客户端不间断的和服务端进行通信,与其说是一对一的功能,
因为只有当客户端A关闭后,客户端B才可以真正和服务端进行通信,这显然不是我们想要的。
下面我们通过多线程的方式给大家实现正常人类的思维。
多线程下socket编程
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) throws IOException {
//初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
//等待客户端的连接
Socket socket = serverSocket.accept();
//每当有一个客户端连接进来后,就启动一个单独的线程进行处理
new Thread(new Runnable() {
@Override
public void run() {
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine()) != null) {
//输出打印
System.out.println("客户端说:" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
客户端
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket = new Socket("127.0.0.1", 9999);
//通过socket获取字符流
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
通过这里我们可以发现,客户端A和客户端B同时连接到服务端后,都可以和服务端进行通信,也不会出现前面讲到使用while(true)时候客户端A连接时客户端B不能与服务端进行交互的情况。
在这里我们看到,主要是通过服务端的 new Thread(new Runnable() {} 实现的,每一个客户端连接进来后,服务端都会单独起个一线程,与客户端进行数据交互,
这样就保证了每个客户端处理的数据是单独的,不会出现相互阻塞的情况,这样就基本是实现了QQ程序的基本聊天原理。
但是实际生产环境中,这种写法对于客户端连接少的的情况下是没有问题,但是如果有大批量的客户端连接进行,那我们服务端估计就要歇菜了。
假如有上万个socket连接进来,服务端就是新建这么多进程,反正楼主是不敢想,而且socket 的回收机制又不是很及时,这么多线程被new 出来,就发送一句话,然后就没有然后了,
导致服务端被大量的无用线程暂用,对性能是非常大的消耗,
在实际生产过程中,我们可以通过线程池技术,保证线程的复用,下面请看改良后的服务端程序。
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerSocketTest {
public static void main(String[] args) throws IOException {
//初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
while (true) {
//等待客户端的连接
Socket socket = serverSocket.accept();
Runnable runnable = () -> {
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine()) != null) {
//输出打印
System.out.println("客户端说:" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
};
executorService.submit(runnable);
}
}
}
通过线程池技术,我们可以实现线程的复用。其实在这里executorService.submit在并发时,如果要求当前执行完毕的线程有返回结果时,
在实际应用中,socket发送的数据并不是按照一行一行发送的,比如我们常见的报文,那么我们就不能要求每发送一次数据,都在增加一个“\n”标识,这是及其不专业的,
在实际应用中,通过是采用数据长度+类型+数据的方式,在我们常接触的热Redis就是采用这种方式
原socket链接
Java NIO(非阻塞IO Nonblocking IO)是什么?
在非阻塞IO出现之前,Java是通过传统的Socket来实现基本的网络通讯功能的。
Socket是同步阻塞IO(BIO),如果客户端没有对服务器发起连接请求,那么accept就会阻塞(阻塞指的是暂停一个线程的执行以等待某个条件发生,例如某资源就绪)。如果连接成功,当数据没准备好时,对read的调用同样会阻塞,当要处理多个连接时就要用到多线程,但是每个线程都有自己的栈空间,由于阻塞会导致线程进行上下文切换,效率低下。
NIO和和BIO的区别
其本质就是阻塞与非阻塞的区别。
Java IO 的各种流都是阻塞的,这意味着,**当一个线程进行流处理(如read()和write())时,无论是否有数据,该线程会一直被阻塞,直到流通信结束。**在此期间线程不能干其他的事情,就算当前没有数据,线程依然保持等待状态。这样无疑会浪费大量的资源。而在NIO的非阻塞模式下,线程发送数据与接收数据都是通过“通道”进行的,线程只需要去询问通道是否有数据需要处理,有则处理,无则立即返回不会进行等待。线程通常将非阻塞IO的空闲时间用于处理其他通道上的IO事件,使用一个单独的线程就可以管理多个输入和输出通道。
那么NIO是怎么实现非阻塞的呢?其实原理很简单,NIO是面向块的,先把数据搬运过来,存放到一个缓冲区中,线程过一段时间来缓冲区看看,有没有数据,这个样线程就不需要始终关注IO了。
(BIO为同步阻塞模型,NIO为同步非阻塞模型。NIO没有实现异步,在JDK1.7后,升级了NIO库包,支持异步非阻塞通信模型,即AIO)
Java NIO的几个相关概念
学习NIO,必须了解几个概念:
(1) . Buffer
Buffer是一个对象,它用来存放即将发送的数据和即将到来的数据。Buffer是NIO核心思想,它与普通流IO的区别是,普通流IO直接把数据写入或读取到Stream(流)对象中,而NIO是先把读写数据交给Buffer(缓冲),后在用流处理的。Buffer实际上就是一个数组,通常是字节数组,这个数组提供了访问数据的读写等操作属性,如位置,容量,上限等概念。
Buffer类型:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这写Buffer覆盖了你能通过IO发送的基本数据类型:byte , short , int , long , double , char 。
(2) .Channel
Channel(通道),与Stream(流)的不同之处在于通道是双向的,流只能在一个方向上操作(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以和多路复用器结合起来,提供状态位,多路复用器可识别Channel所处的状态。
通道分两大类:用于网络读写的SelectableChannel,和用于文件操作的FileChannel。
(3) . Selector(挑选者/选择器)实现IO多路复用
NIO的编程基础,Selector提供选择已经就绪的任务的能力。简单说,就是Selector会不断询问注册在Selector上的通道(Channel),如果这个通道发生了读写操作,这个通道就会处于就绪状态,会被Selector察觉到,然后通过SelectionKey可以取出就绪的Channel集合,从而进行IO操作。
一个Selector可以负责成千上万的通道,没有上限。获得连接句柄没有限制。意味着我们只需要一个线程负责Selector的轮询,就可以接入成百上千的客户端,这是JDK NIO库的巨大进步。
科普一下实现IO多路复用的方式还有:select、poll、epoll
(4) . 原理图
示例:
服务端:
public class Server implements Runnable
{
private Selector selector;
private ByteBuffer buffer = ByteBuffer.allocate(1024); //?
public Server(int port){
try {
selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置服务器为非阻塞方式
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(port));
//把服务器通道注册到多路复用选择器上,并监听阻塞状态
ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Server start whit port : "+port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void run(){
while(true){
try {
//会这这里处理事件,也是阻塞的,事件包括客户端连接,客户端发送数据到来,以及客户端断开连接等等
//若没有事件发生,也会阻塞
selector.select();
//System.out.println("阻塞在这");
//返回所有已经注册到多路复用选择器的通道的SelectionKey
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
//遍历keys
while(keys.hasNext()){
SelectionKey key = keys.next();
//下一个key,就像数组访问的i++
keys.remove();
if(key.isValid()){ //判断key是否有效
if(key.isAcceptable()){ //请求连接事件
accept(key); //处理新客户的连接
}
if(key.isReadable()){ //有数据到来
read(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理客户端连接
* 服务器为每个客户端生成一个Channel
* Channel与客户端对接
* Channel绑定到Selector上
* **/
private void accept(SelectionKey key){
try {
//获取之前注册的SocketChannel通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//执行阻塞方法,Channel和客户端对接
SocketChannel sc = ssc.accept();
//设置模式为非阻塞
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}catch(Exception e){
e.printStackTrace();
}
}
private void read(SelectionKey key){
try {
//清空缓冲区的旧数据
buffer.clear();
SocketChannel sc = (SocketChannel) key.channel();
int count = sc.read(buffer);
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//读取到了数据,将buffer的position复位到0
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes).trim();
System.out.println("Server:"+body);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main( String[] args )
{
new Thread(new Server(8379)).start();
}
}
客户端:
public class Client {
public static void main(String[] args){
InetSocketAddress address = new InetSocketAddress("127.0.0.1",8379);
SocketChannel sc =null;
ByteBuffer buffer = ByteBuffer.allocate(1024);
try{
sc = SocketChannel.open();
sc.connect(address);
while(true){
byte[] bytes = new byte[1024];
System.in.read(bytes);
buffer.put(bytes);
buffer.flip();
sc.write(buffer);
buffer.clear();
}
}catch(IOException e){
e.printStackTrace();
}finally {
if(sc!=null){
try {
sc.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
}
输出:
基于Java NIO的框架
推荐大家使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。
什么是Java序列化
序列化:将 Java 对象转换成字节流的过程。
反序列化:将字节流转换成 Java 对象的过程。
作用:当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。
什么是持久化?
简单来说:对于概念上来说,就是数据保存到硬盘系统重启可恢复,对于开发人员来说,就是对象保存到数据库。
持久化是将程序数据在持久状态和瞬时状态间转换的机制。
持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。
持久化的主要应用是将内存中的对象存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。
JDBC就是一种持久化机制。文件IO也是一种持久化机制。
序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
注意事项:
- 某个类可以被序列化,则其子类也可以被序列化
- 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
- 反序列化读取序列化对象的顺序要保持一致
示例:
package constxiong.interview;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* 测试序列化,反序列化
* @author ConstXiong
* @date 2019-06-17 09:31:22
*/
public class TestSerializable implements Serializable {
private static final long serialVersionUID = 5887391604554532906L;
private int id;
private String name;
public TestSerializable(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "TestSerializable [id=" + id + ", name=" + name + "]";
}
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\日常存档\TestSerializable.txt"));
oos.writeObject("测试序列化");
oos.writeObject(618);
TestSerializable test = new TestSerializable(1, "ConstXiong");
oos.writeObject(test);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\日常存档\TestSerializable.txt"));
System.out.println((String)ois.readObject());
System.out.println((Integer)ois.readObject());
System.out.println((TestSerializable)ois.readObject());
}
}
输出:
测试序列化
618
TestSerializable [id=1, name=ConstXiong]
由于序列化的使用会影响系统的性能,所以我们只在这几种情况下序列化
1)需要通过网络来发送对象,或对象的状态需要被持久化到数据库或文件中。
2)序列化能实现深复制,即可以复制引用的对象
反序列化
需要声明serialVersionUID
private static final long serialVersionUID = 5887391604554532906L;
serialVersionUID主要有如下3个优点:
- 提高程序的运行效率。如果在类中未显示声明serialVersionUID,那么在序列化时会通过计算得到一个serialVersionUID值,通过显示声明省去了计算的过程,因此提高了程序运行的效率。
- 提高程序不同平台上的兼容性。由于各个平台的编译器计算serialVersionUID时有可能采用不同算法,会导致一个平台上序列化对象在另外一个平台上将无法实现序列化。
- 增强程序各个版本的可兼容性。在默认情况下,每个类都有唯一的serialVersionUID,因此,当后期对类进行修改时(例如加入新的属性),类的serialVersionUID值将会发生变化,这将会导致类在修改前对象序列化文件在修改后将无法进行反序列化操作。
System.out.println需要注意的问题:
system.out.println(new People());
system.out.println(1 + 2 + "");
system.out.println("" + 1 + 2);
输出:
name:谢震宇 age:1111111
3
12
解析:第一个是实体类在构造方法的时候赋值了,然后重写了toString方法,所以可以直接输出。参数是从左到头第三个是先转成了字符串“1”+2,所以等于“12”