前言
(一)字符集的概念
在计算机中,任意数据都以二进制的形式来存储,事实上,字节是计算机的最小存储单元。字符与字节是不同的概念,任何一个文字或符号都是一个字符,但所占的字节并不一定,为使得字符可在计算机中存储而制定了不同的编码方式(因为计算机只能识别0与1),这就是字符集。
事实上,字符集就是一套编码规范的子概念 ,为了显示字符,国际组织就制定了编码规范,希望使用不同的二进制数来表示代表不同的字符,这样电脑就可以根据二进制数来显示其对应的字符。我们通常就称呼其为XX编码,XX字符集。编码规范的三大概念:
-
字库表: 一套编码规范不一定包含世界上所有的字符,每套编码规范都有自己的使用场景。而字库表就存储了编码规范中能显示的所有字符,计算机就是根据二进制数从字库表中找到字符然后显示给用户,相当于一个存储字符的数据库。
-
编码字符集(字符集): 在一个字库表中,每一个字符都有一个对应的二进制地址,而编码字符集就是这些地址的集合,如,在ASCII码的编码字符集中,"A"的编码就是65,即,01000001.
-
字符编码(编码方式): 程序员制定了一套算法来节省空间,而每种不同的算法都被称作一种编码方式(下文中为了便于理解都将使用编码方式来称呼字符编码)。一套编码规范可以有多种不同的编码方式,不同的编码方式有不同的适应场景。 如:UTF-8就是一种编码方式,Unicode是一种编码规范。此外,Unicode还有UTF-16,UTF-32这两种编码方式。不同的编码方式节约的空间不同。
(二)常见的编码规范
- ASCII码: ASCII码,是最早产生的编码规范,一共包含00000000~01111111共128个字符,可以表示阿拉伯数字和大小写英文字母,以及一些简单的符号。可以看出ASCII码只需要1个字节的存储空间,最高位为0。后被称为(American Standard Code for Information Interchange,美国信息交换标准代码)。它没有特定的编码方式,直接使用地址对应的二进制数来表示,非要说那就叫他ASCII 编码方式。
- GBK: GBK全称《汉字内码扩展规范》,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字。GBK字符集中所有字符占2个字节,不论中文英文都是2个字节。 没有特殊的编码方式,习惯称呼GBK 编码。一般在国内,汉字较多时使用
- Unicode: 从以上几种编码规范可以看出,各种编码规范互不兼容,且只能表示自己需要的字符,于是,国际标准化组织(ISO)决定制定一套全世界通用的编码规范,这就是Unicode。Unicode包含了全世界所有的字符。Unicode最多可以保存4个字节容量的字符。也就是说,要区分每个字符,每个字符的地址需要4个字节。这是十分浪费存储空间的,于是,程序员就设计了几种字符编码方式,比如:UTF-8( 英文在UTF-8字符编码后只占1个字节,中文占了3个字节),UTF-16,UTF-32。最为广泛的就是UTF-8,UTF-8是一种变长字符编码,注意:UTF-8不是编码规范,而是编码方式。
注意,1个字符不一定只对应1个字节,需要在具体的编码方式中查看。
(三)编码与解码
- 解码:一串二进制数,使用一种编码方式,转换成字符,这个过程我们称之为解码。 程序员可以选用任意的编码方式进行解码,但往往只有一种编码方式可以解开密码显示出正确的文字,而使用错误的编码方式,产生其他不合理的字符,这就是我们通常说的————乱码。
- 编码:一串已经解码后的字符,我们也可以选用任意类型的编码方式重新转换成一串二进制数,这个过程就是编码,我们也可以称之为加密过程,无论使用哪一种编码方式进行编码,最终都是产生计算机可识别的二进制数,但如果编码规范的字库表不包含目标字符,则无法在字符集中找到对应的二进制数。这将导致不可逆的乱码!例如:像ISO-8859-1的字库表中不包含中文,因此哪怕将中文字符使用ISO-8859-1进行编码,再使用ISO-8859-1进行解码,也无法显示出正确的中文字符。
编码与解码的代码演示
//Java中编码的方式
public byte[] getBytes() //使用默认方式进行编码(UTF-8)
public byte[] getBytes(String charsetName)//使用指定方式进行编码
//Java中解码的方式
String(byte[] bytes) //使用默认方式编码(UTF-8)
String(byte[] bytes,String charsetName)//使用指定方式解码
将一串中文字符串使用UTF-8编码方式进行编码变成字节数组:
package org.example.SimpleCode;
import java.io.UnsupportedEncodingException;
public class Demo {
public static void main(String[] args) throws UnsupportedEncodingException {
String chinese="你好 世界!";
byte[] bytes=chinese.getBytes("UTF-8");
System.out.println(chinese+"转为UTF-8编码后为:");
for(byte b:bytes){
System.out.printf(b+" ");
}
System.out.println();
String utf8=new String(bytes,"UTF-8");
System.out.println(bytes+"转为汉字后为:"+utf8);
}
}
但若使用GBK进行解码,则会出现乱码:
package org.example.SimpleCode;
import java.io.UnsupportedEncodingException;
public class Demo {
public static void main(String[] args) throws UnsupportedEncodingException {
String chinese="你好 世界!";
byte[] bytes=chinese.getBytes("UTF-8");
System.out.println(chinese+"转为UTF-8编码后为:");
for(byte b:bytes){
System.out.printf(b+" ");
}
System.out.println();
String utf8=new String(bytes,"GBK");
System.out.println(bytes+"转为汉字后为:"+utf8);
}
}
注:简体中文Windows中默认使用的是GBK字符集,而项目中一般使用UTF-8字符集。
一、什么是IO流
IO,即,in和out,输入与输出。指应用程序和外部设备之间的数据传输,常见的外部设备包括文件、管道、网络连接。Java通过流来处理IO,而流(Stream),是一个抽象的概念,指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。且,与File类不同,前者只能对文件本身进行操作,而不能读写文件中存储的数据,而IO流可读写文件中或网络中的数据(将程序中的数据写入文件,即为写入;将文件中的数据加载到程序中,即为读入)。
当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
二、IO流的分类
(一)按数据流方向分类
IO流按照数据流的方向分类可分为输入流、输出流。
(二)按数据处理单位分类
IO流按照数据流的处理单位可分为字节流、字符流。事实上,二者的用法几乎完全一致,区别在于字节流和字符流所操作的数据单元不同,字节流的操作单元是8位的字节,而字符流操作的是数据单元为16位的字符。
- 字节流:可操作所有类型的文件。
- 字符流:只能操作纯文本文件(txt、md、xml、lrc等)。
本文只介绍常见的基本流、缓冲流,且,InputStream、OutputStream、Reader、Writer都是抽象类,只能使用它们的实现类来进行操作。
三、字节流
(一)字节输出流
OutputStream 是字节输出流所有类的超类。
①FileOutputStream
FileOutputStream,文件字节输出流,是OutputStream的一个典型实现类。是操作本地文件的字节输出流,可将程序中的数据写到本地文件中。
构造方法:
构造函数 | 参数说明 |
---|---|
FileOutputStream(String) | 输入表示路径的字符串 |
FileOutputStream(String,boolean) | 输入表示路径的字符串,是否追加到文件末尾,若为false,则清空文件后再写入 |
FileOutputStream(File) | 传入File对象来构建输出流 |
FileOutputStream(File,boolean) | 传入File对象来构建输出流,是否追加到文件末尾,若为false,则清空文件后再写入 |
FileOutputStream(FileDescriptor) | 用文件描述符对象来创建输出流 |
write方法
write方法用于向文件中写入数据,是否覆写由构造方法决定。
方法声明 | 参数说明 |
---|---|
write(int b) | 写入整型b所对应的ASCII字符到输出文件 |
write(byte[] arr) | 将一个字节数组所对应的ASCII字符写入输出文件 |
write(byte[] arr,int index,int len) | 写入字节数组的一部分 |
close方法
close方法用于释放流所占用的资源。
案例
package org.example;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileOutputStream fileOutputStream=null;
try {
//1.创建本地文件对象
fileOutputStream=new FileOutputStream(PathName);
/*
* 注意:
* a.参数是String或File对象皆可(查看源码可知,拿到字符串后会拿字符串new一个File对象).
* b.若文件不存在则会创建一个新的文件,但是要保证父级路径是存在的.
* c.若文件已存在,则会清空文件,而若不想要情况,则可使用:
* FileOutputStream(String name,boolean append)来进行构造,构造方法中第二个参数传递true时,表示在文件末尾继续写数据.
* */
//2.写出数据
fileOutputStream.write("Hello World".getBytes());
} catch (IOException ioException){
System.out.println("IO异常");
}finally {
if(fileOutputStream!=null)
try{
//3.释放资源
fileOutputStream.close();
/*
* 注意:
* 若不释放资源,则文件会因为处于正在被使用的状态被无法被使用.
* */
}catch (IOException e){
e.printStackTrace();
}
}
}
}
注,换行符可使用:
fileOutputStream.write("\r\n".getBytes());
Windows中是"\r\n",Linux中是"\n",Mac中是"\r"。
源码分析
②BufferOutputStream
BufferOutputStream,字节缓冲输出流,与FileOutputStream不同,FileOutputStream一次只传输一个字节的数据,在传输大量数据时需要使用多次系统调用,使得效率低下,而BufferedInputStream不同,其本质是通过一个内部缓冲区数组实现。
原理图:
在创建缓冲流对象时,会创建一个内置的、有默认大小的缓冲区数组,通过缓冲区读写来减少系统IO调用的次数,以此提高读写的效率。
源码中缓冲区的默认大小:
private static int DEFAULT_BUFFER_SIZE = 8192;
构造方法:
方法声明 | 参数说明 |
---|---|
BufferedOutputStream(OutputStream out) | 使用OutputStream对象创建一个缓冲流对象,而OutputStream是一个抽象类,故使用其实现类FileOutputStream |
BufferedOutputStream(OutputStream out,int size) | 创建一个缓冲流对象,并指定缓冲区大小 |
其余方法与普通的字节输出流是一致的,但需要先创建一个FileOutputStream对象 |
package org.example;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
BufferedOutputStream bufferedOutputStream=null;
try {
bufferedOutputStream=new BufferedOutputStream(new FileOutputStream(PathName));
bufferedOutputStream.write("Hello World!".getBytes());
} catch (IOException ioException){
System.out.println("IO异常");
}finally {
if(bufferedOutputStream!=null)
try{
bufferedOutputStream.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
bufferedOutputStream.close();
}
}
}
}
(二)字节输入流
①FileInputStream
FileInputStream,文件字节输入流,是InputStream的一个典型实现类,用于从本地文件当中获取数据,读取到项目当中。
构造方法:
构造函数 | 参数说明 |
---|---|
FileInputStream(String) | 输入文件地址字符串 |
FileInputStream(File) | 输入File对象构造流对象 |
FileInputStream(FileDescriptor) | 输入文件描述符对象构造流对象 |
read方法:
read方法用于从文件中读取数据。
方法声明 | 参数说明 |
---|---|
read() | 从输入流读取一个字节,返回对应的ASCII吗,若到达文件末尾,则返回-1 |
read(byte[] b) | 从输入流中读取b.length个字节到字节数组中,返回读入缓冲区的总字节数,若到达文件末尾,则返回-1 |
read(byte b[], int off, int len) | 从输入流中读取最多len个字节到数组中,并从数组off位置开始存储字节。 |
read()实例
package org.example;
import java.io.FileInputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileInputStream fileInputStream=null;
try {
//1.建立连接
fileInputStream=new FileInputStream(PathName);
int t=fileInputStream.read();
while(t!=-1){
char c=(char)t;
System.out.print(c);
t=fileInputStream.read();
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(fileInputStream!=null)
try{
fileInputStream.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
fileInputStream.close();
}
}
}
}
read(byte[] b)实例
package org.example;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileInputStream fileInputStream=null;
try {
fileInputStream=new FileInputStream(PathName);
byte[] bytes=new byte[2];
int i=1;
int t=fileInputStream.read(bytes);
while(t!=-1){
System.out.println("第"+i+"次读取的数据为:"+ Arrays.toString(bytes));
i++;
t=fileInputStream.read(bytes);
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(fileInputStream!=null)
try{
fileInputStream.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
fileInputStream.close();
}
}
}
}
事实上,此时的txt中内容为"12345",一共5个字节,而字节数组大小为2,最后一次读取时只能读取到一个字节,故选择读取最后两个字节。
而read(byte b[], int off, int len)的处理和他是一样的,为了避免这种情况,将字节数组的长度设置为文件的字节大小,使得可刚好填充满。
byte[] b=new byte[(int) file.length()];
释放资源
public void close() throws IOException
②BufferedInputStream
BufferedInputStream,字节缓冲输入流, 当创建BufferedInputStream时,将创建一个内部缓冲区数组。相对于 FileInputStream,使用BufferedInputStream读资源比FileInputStream读取资源的效率高(BufferedInputStream的read方法会读取尽可能多的字节,执行read时先从缓冲区读取,当缓冲区数据读完时再把缓冲区填满),因此,当每次读取的数据量很小时,FileInputStream每次都是从硬盘读入,而BufferedInputStream大部分是从缓冲区读入。读取内存速度比读取硬盘速度快得多,因此BufferedInputStream效率高,且FileInputStream对象的read方法会出现阻塞。
BufferedInputStream的默认缓冲区大小是8192字节。当每次读取数据量接近或远超这个值时,两者效率就没有明显差别了。BufferedOutputStream和FileOutputStream同理,差异更明显一些。
构造方法
构造方法 | 参数说明 |
---|---|
BufferedInputStream(InputStream) | 创建一个BufferedInputStream对象,并传入InputStream对象in(由于InputStream是抽象类,故而使用其实现类FileInputStream) |
BufferedInputStream(InputStream,int) | 创建BufferedInputStream并具有指定缓冲区大小. |
read方法
使用方法和FileInputStream是一样的,对于字节处理会产生同样的问题。
package org.example;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
BufferedInputStream bufferedInputStream=null;
try {
//1.建立连接
bufferedInputStream=new BufferedInputStream(new FileInputStream(PathName));
byte[] bytes=new byte[2];
int i=1;
int t=bufferedInputStream.read(bytes);
while(t!=-1){
System.out.println("第"+i+"次读取的数据为:"+ Arrays.toString(bytes));
i++;
t=bufferedInputStream.read(bytes);
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(bufferedInputStream!=null)
try{
bufferedInputStream.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
bufferedInputStream.close();
}
}
}
}
四、字符流
字节流与字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是1个字节,而字符流操作的是数据单元为1个字符(而我们常说的1个字符对应1个字节是因为采用的是ASCII编码,事实上,不同编码方式中1个字符不一定对应1个字节)。这是因为, Java中字符是采用Unicode标准,Unicode 编码中,一个英文字母或一个中文汉字为两个字节。 而在UTF-8编码中,一个中文字符是3个字节。例如下面图中,“云深不知处”5个中文对应的是15个字节:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124
如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。为了更方便地处理中文这些字符,Java就推出了字符流。
字节流和字符流的其他区别:
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,但处理纯文本文件时可能因为编码方式的问题而出现错误,字符流只能处理纯文本文件,且不会出错。
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
可见,字符流自带缓冲区。
事实上,字符流的底层起始就是字节流,即,字符流=字节流+字符集,种类: - 字符输入流:一次读一个字节,遇到中文等其他语言时,一次读多个字节(GBK读取两个,UTF-8读取三个)。
- 字符输出流:底层会将数据按照指定的编码方式进行编码,变成字节后再写到文件中。
字符流体系结构
(一)字符输入流
①FileReader
FileReader,文件字符输入流。
底层:
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
构造方法
常用方法
注:虽是按照字节读取,但遇到中文时会一次读多个数据(GBK读取两个,UTF-8读取三个),事实上,在读取之后方法的底层会进行解码,将其转换为十进制,故而需要进行类型转换,转换成相应的中文字符。
read()实例
package org.example;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileReader fileReader=null;
try {
fileReader=new FileReader(PathName);
int t;
while((t=fileReader.read())!=-1){
System.out.print((char)t);
//需要进行类型转换
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(fileReader!=null)
try{
fileReader.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
fileReader.close();
}
}
}
}
read(char[] buffer)实例
package org.example;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileReader fileReader=null;
try {
fileReader=new FileReader(PathName);
char[] chars=new char[2];
int i=1;
int t=fileReader.read(chars);
while(t!=-1){
System.out.println("第"+i+"次读取的数据为:"+ Arrays.toString(chars));
i++;
t=fileReader.read(chars);
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(fileReader!=null)
try{
fileReader.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
fileReader.close();
}
}
}
}
字符输入流底层原理
②BufferedReader
BufferedReader,缓冲字符输入流
底层源码:
构造一:
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
构造二:
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
默认大小:
private static int defaultCharBufferSize = 8192;
可见,其并非通过创建FileInputStream来创建的,而是在内存中自定义一个8kb的char[]数组来充当缓存区(注意区分Cache和Buffer),事实上,其效率和FileReader差不多(快一点点),但是,其提供了readline方法,可依次读取一行数据(根据换行符进行判断)。
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
BufferedReader bufferedReader=null;
try {
bufferedReader=new BufferedReader(new FileReader(PathName));
String t=bufferedReader.readLine();
int i=1;
while(t!=null){
System.out.println("第"+i+"行内容为:"+t);
t=bufferedReader.readLine();
i++;
}
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(bufferedReader!=null)
try{
bufferedReader.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
bufferedReader.close();
}
}
}
}
(二)字符输出流
①FileWriter
FileWriter,文件字符输出流。
底层源码:
public FileWriter(String fileName) throws IOException {
super(new FileOutputStream(fileName));
}
构造方法:
若文件不存在则会创建一个新文件,但要保证父级目录存在。
成员方法:
package org.example;
import java.io.FileWriter;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
FileWriter fileWriter=null;
try {
fileWriter=new FileWriter(PathName);
char[] chars="Hello World!".toCharArray();
fileWriter.write(chars);
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(fileWriter!=null)
try{
fileWriter.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
fileWriter.close();
}
}
}
}
字符输出流底层原理
事实上,字符输入流底层是将数据写入缓冲区当中,此时有三种情况:
- 缓冲区装满:此时会自动将缓冲区中的数据写入到文件当中,并刷新缓冲区。
- 手动刷新缓冲区:调用flush函数,会将缓冲区内的内容写入文件当中。
- 释放资源:调用close函数,会将会将缓冲区内的内容写入文件当中,并关闭流。
②BufferedWriter
BufferedWriter,缓冲字符输出流。
构造方法:
方法声明 | 参数声明 |
---|---|
BufferedWriter(Writer) | 使用Writer对象创建流,由于Writer是抽象类,故一般使用FileWriter对象 |
BufferedWriter(Writer,int) | 创建流,并指定缓冲区大小 |
其余方法与FileWriter基本一致,但提供了newLine方法,便于换行写数据。 |
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
String PathName="Demo.txt";
BufferedWriter bufferedWriter=null;
try {
bufferedWriter=new BufferedWriter(new FileWriter(PathName));
bufferedWriter.write("Hello World");
bufferedWriter.newLine();
bufferedWriter.write("你好,世界!");
} catch (IOException ioException){
System.out.println("IO异常");
} finally {
if(bufferedWriter!=null)
try{
bufferedWriter.close();
}catch (IOException e){
System.out.println("IO异常");
}finally {
bufferedWriter.close();
}
}
}
}
五、转换流
转换流是一种高级流,其提供了在字节流和字符流之间的转换,字节流中的数据都是字符时,转换成字符操作会更高效,且此时可根据字符集一次读写多个字节,读写数据也不会乱码。
①InputStreamReader
将InputStream转换为Reader(将一个字节的输入流转换为字符的输入流)。
①InputStreamReader
将InputStream转换为Reader(将一个字节的输入流转换为字符的输入流)。
实例:使用两个流实现文本文件的转码。
@Test
public void test2() throws Exception {
//1.造文件、造流
File file1 = new File("dbcp.txt");
File file2 = new File("dbcp_gbk.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
//2.读写过程
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
osw.write(cbuf,0,len);
}
//3.关闭资源
isr.close();
osw.close();
}
六、序列化流与反序列化流
(一)序列化与序列化ID
Java提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含对象的数据、对象的类型、对象中存储的属性等信息。通俗易懂的说,就是把我们创建的对象进行保存到文件中和从文件中读取出来的操作过程就叫序列化流。
序列化:将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
用途:
- 把对象的字节序列永久保存到硬盘上,通常存放在一个文件中(序列化对象)。
- 在网络上传送对象的字节序列(网络传输对象)。
对于实体类,需要实现 java.io.Serializable接口以启用其序列化功能。未实现此接口的类将无法使用其任何状态序列化和反序列化如果序列化的时候没有实现 Serializable接口,会抛出NotSerializableException没有序列化异常。Serializable接口也叫标记型接口(其类中是空的),要进行序列化和反序列化的类必须实现Serializable接口,就会给类添加一个标记,当我们进行序列化和反序列化的时候,就会检查类上是否有这个标记。
序列化ID: 序列化ID起着关键的作用,它决定这是否能够成功反序列化,简单的来说就是,java的序列化机制是通过运行时判断类的serialVersionUID来验证版本是否一致,在进行反序列化的时候,JVM会传来字节流中的serialVersionUID与本地实体类中的serialVersionUID进行对比(要先确保已创建相应的本地实体类),如果相同则认为一致可以进行反序列化,否则就会报异常 。
如果实体里面没有显示的定义一个serialVersionUID ,Java序列化机制会根据编译的class自动生成一个serialVersionUID,比如,当我们编写一个实体类的时候,后来发现业务需要添加新的字段,这个时候如果再次反序列化的时候就会出现sersionVersionUID不一致,导致反序列化失败,那么如何解决这个问题?
便是在本地类的实体中显示定义一个serialVersionUID变量 这个时候每次编译的时候值都保持不变
需要先进行设置:
此时在类名上使用Alt+Enter:
生成实体类:
package org.example.polo;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
public class Student implements Serializable {
@Serial
private static final long serialVersionUID = 6384314432141925333L;
private String Name;
private int age;
public Student() {
}
public Student(String name, int age) {
Name = name;
this.age = age;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Name.equals(student.Name);
}
@Override
public int hashCode() {
return Objects.hash(Name, age);
}
@Override
public String toString() {
return "Student{" +
"Name='" + Name + '\'' +
", age=" + age +
'}';
}
}
序列化
把对象以流的方式,写入到文件中保存,叫写对象,也叫对象的序列化。对象就是Object,转换成字节,写入字节流就是OutputStream,那我们就使用ObiectOutputStream类把对象序列化。
抽象类ObjectOutput.java
public interface ObjectOutput extends DataOutput, AutoCloseable {...}
接口ObjectStreamConstants.java
public interface ObjectStreamConstants {}
实现类ObjectOutputStream.java
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants{...}
构造方法
关键成员方法writeObject
public final void writeObject(Object obj) throws IOException {...}
用于将指定对象写入ObjectOutputStream
使用步骤:
- 创建ObjectOutputStream对象,构造方法中传递字节输出流。
- 使用ObjectOutputStream对象中的writeObject()方法,将对象写入文件。
- 释放资源
package org.example;
import org.example.polo.Student;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException /*此处需要抛出 IOException异常*/{
//1.创建ObjectOutputStream对象,构造方法中传递字节输出流
ObjectOutputStream obj1=new ObjectOutputStream(new FileOutputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
//2.使用ObjectOutputStream对象中的方法writeObjectW方法,把对象写到文件中
obj1.writeObject(new Student("张三",25));
//3.释放资源
obj1.close();
}
}
得到序列化文件:
其是用二进制进行存储的,而又因txt默认打开后会进行解码,故而出现了乱码。
(二)反序列化
把文件中保存的对象,以流的方式方式读取出来,叫读对象,也叫对象的反序列化。同样,对象Object。保存的对象是字节,我们读取对象,读出来也是字节,就用读字节流InputStream,所以我们用ObjectInputStream类来把对象反序列化。
ObjectOutputStream.java
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants{...}
作用:将文件中保存的对象以流的形式读取出来使用。
构造方法:
使用步骤:
- 创建ObjectInputStream对象,构造方法中传递字节输出流。
- 使用ObjectInputStream对象中的readObject方法把对象从文件中读取出来。
- 释放资源。
- 使用读取出来的对象(打印)。
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.创建ObjectInputStream对象,构造方法中传递字节输出流
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
//2.使用ObjectInputStream对象中的readObject方法把对象从文件中读取出来
Object o=ois.readObject();
//3.释放资源
ois.close();
//4.使用读取出来的对象(打印)
System.out.println(o);
}
}
transient关键字
属于瞬态关键字,作用是被其修饰的成员变量是不能被序列化的,常用于保护私密信息。
使用方式:
private transient int age;
先序列化:
package org.example;
import org.example.polo.Student;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException /*此处需要抛出 IOException异常*/{
//1.创建ObjectOutputStream对象,构造方法中传递字节输出流
ObjectOutputStream obj1=new ObjectOutputStream(new FileOutputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
//2.使用ObjectOutputStream对象中的方法writeObjectW方法,把对象写到文件中
obj1.writeObject(new Student("张三",25));
//3.释放资源
obj1.close();
}
}
反序列化:
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.创建ObjectInputStream对象,构造方法中传递字节输出流
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
//2.使用ObjectInputStream对象中的readObject方法把对象从文件中读取出来
Object o=ois.readObject();
//3.释放资源
ois.close();
//4.使用读取出来的对象(打印)
System.out.println(o);
}
}
可见,此时age被初始化为默认值0。
注意:
- 序列化流写到文件中的数据不能修改,一旦修改就写不回来了。
- 序列化对象后,若修改了Javabean类,再次反序列化会抛出InvalidClassException异常,需要给Javabean加上对于的serialVersionUID,此时无论对象是否对的上,都能成功序列化从而得到对象。
对于多个对象,可通过集合来同时实现序列化
package org.example;
import org.example.polo.Student;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student student1=new Student("张三",18);
Student student2=new Student("李四",19);
Student student3=new Student("王五",20);
ArrayList<Student>list=new ArrayList<>();
Collections.addAll(list,student1,student2,student3);
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
objectOutputStream.writeObject(list);
}
}
反序列化:
package org.example;
import org.example.polo.Student;
import java.io.*;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("C:\\Users\\lenovo\\Desktop\\a.txt"));
ArrayList<Student>list=(ArrayList<Student>)objectInputStream.readObject();
System.out.println(list);
}
}
七、打印流
打印流只有输出流,一般指PrintStream和PrintWriter两个类,其提供了非常方便的打印功能,可打印任何的数据类型,例如小数、整数、字符串、布尔类型等。
(一)PrintStream
PrintStream,字节打印流,之前在打印信息的时候需要使用OutputStream,但是这样一来,所有的数据输出的时候会非常的麻烦,String——>byte[],但是字节打印流中可以方便的进行输出。 之前在打印信息的时候需要使用OutputStream,但是这样一来,所有的数据输出的时候会非常的麻烦,String——>byte[],但是字节打印流中可以方便的进行输出。
构造方法:
常用方法:
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
PrintStream printStream=new PrintStream("C:\\Users\\lenovo\\Desktop\\a.txt");
//格式化输出
printStream.printf("姓名:%s 年龄:%d 成绩:%f 性别:%c","小明", 8, 97.5, '男');
printStream.close();
}
}
(二)PrintWriter
PrintWriter,字符打印输出流,PrintStream与PrintWriter的区别在于,PrintStream作为处理流使用时,PrintStream只能封装OutputStream类型的字节流,而PrintWriter既可以封装OutputStream类型的字节流,还能够封装Writer类型的字符输出流并增强其功能。
构造函数
常用函数
package org.example;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileWriter fileWriter=new FileWriter("C:\\Users\\lenovo\\Desktop\\a.txt");
PrintWriter printWriter=new PrintWriter(fileWriter);
printWriter.print("Hello World");
}
}