一、IO流概述
日常中,数据被保存在硬盘、U盘等设备上,IO技术的作用主要就是解决设备和设备之间的数据传输问题,比如:硬盘 --->内存,内存数据------>硬盘中,把键盘的数据写入到内存等。
我们进行的数据传输,可以看作一种数据的流动,通过“流”进行数据传输。
- 流的概念:流是对数据传输的总称或抽象,它的特性是进行数据传输。可以理解为通道,数据通过这个通道进行传输。
- 流的分类:
- 按数据流动方向划分,以内存为基准,把数据从其他设备上读取到内存中的为输入流,从内存中写出到其他设备的为输出流。对于输入流而言,流指定的地址是源,程序从指向源的输入流中读取源中的数据;对于输出流而言,指定的地址是数据要传送去的目的地,程序通过输出流将数据传送到目的地。
- 按照处理数据类型的单位不同,可以分为字节流和字符流。
二、文件字节输入流、输出流
java.io包提供了大量的流类,Java把InputStream抽象类的子类创建的流对象称作字节输入流、OutputStream抽象类的子类创建的流对象称作字节输出流。
Java把Reader抽象类的子类创建的流对象称作字符输入流、Writer抽象类的子类创建的流对象称作字符输出流。
以上四个类都有若干子类,下面以文件相关的输入/输出流为主进行了解。因为数据保存时为了永久化,一般是以文件的形式保存在磁盘中的,程序运行时再从磁盘中读取数据到内存。数据保存在文件中,而Java中我们是通过File文件类来操作文件和文件夹的。因此很有必要了解文件输入流和文件输出流。
对于文件输入流,只要不关闭,每次调用read方法就能顺序地读取其余内容,直到读到文件末尾或输入流被关闭。
对于文件输出流,顺序地写文件,只要不关闭流,每次调用write方法就顺序地向目的地写入内容,直到流被关闭。
文件输入流和文件输出流并不是Java直接操作磁盘数据,而是Java——JVM——OS(操作系统)——文件。
1.文件字节输入流、输出流
java.io.FileInputStream 是 java.io.InputStream
java.io.FileOutputStream 是 java.io.OutputStream 的子类,是文件字节输出流,可通过该类实现对文件的写入操作。
我们知道,所有数据,无论是文本、图片还是视频,底层都是二进制数据,数据传输也是传输的二进制数据,只是在打开文件的时候会获取文件编码对这些二进制数据进行转换成我们认识的字符。英文、数字和英文标点都是占一个字节,而汉字和中文符号则根据编码方式的不同导致占用字节数不同,比如UTF-8,一个汉字三个字节,GBK,一个汉字两个字节。对于字节流而言,传输数据以字节为单位,这就导致了中文乱码的可能,因此读取有中文字符的文件更建议用字符流。
- FileInputStream
功能类型 | 常用方法 | 描述 |
|
| 参数类型为抽象路径File |
| 参数类型为字符串路径名String | |
| ||
|
| 从该输入流读取一个字节的数据,将读取到结果作为返回值返回。 读取到的结果:还未读取完,返回一个字节的数据(0~255);已到达文件的末尾,返回-1。 特点:每次只读取一个字节;读取到的数据作为返回值返回。 |
| 从该输入流中读取b.length的数据,将结果存储在b数组里。 读取到的结果:如果数据不够指定长度,以空字符补充。 返回的结果:真正读取到的数据长度,或读取已到达文件的末尾返回-1。 特点:可指定每次读取数据的数量;数据存储在数组里;数据不足会空字符补充。 | |
| 从该输入流读取 读取到的结果:如果数据不够指定长度,以空字符补充。 返回的结果:真正读取到的数据长度,或已到达文件的末尾返回-1。 特点:可指定每次读取数据的数量;数据存储在数组的指定位置;数据不足会空字符补充。 | |
read:每次读取一个数据得到的是int类型的数据,得到真正内容需要强转成char;每次读取多个数据得到的是byte类型的数据,得到真正内容需要强转成String,并且不是全数组转, 而是只转真正获取到的数据长度,这样可以去除那些补充的空字符。 | ||
关闭 |
| 关闭此文件输入流,并释放与流相关联的任何系统资源。 |
- FileOutPutStream:
功能类型 | 常用方法 | 描述 |
创建输出流 | FileOutputStream(File file) | |
FileOutputStream(File file, boolean append) | 可设置是否追加 | |
FileOutputStream(String name) | | |
FileOutputStream(String name, boolean append) | 可设置是否追加 | |
构造函数:创建一个输出流,指向目的地,写入指定文件。如果目的地文件不存在,则创建;如果存在,则内容覆盖/追加。可设置是否追加,默认覆盖不追加。 创建输出流不保证调用构造函数就能成功,对于某些允许一次只能打开一个文件来写入一个FileOutputStream (或其他文件写入对象)的平台,如果所涉及的 文件已经打开,则此类中的构造函数将失败。 | ||
写入文件 |
| 将指定的一个字节写入此文件输出流。 |
| 将 | |
| 从b的起,写入 | |
write:(1)字节流写数据不需要刷新,因为没有用到缓冲区,是文件直接操作,执行完write就立刻写入, 字符流需要刷新,因为数据读取后放在了缓冲区,刷新时才真正写入数据,手动flush可以实现数据从缓冲区刷新到磁盘上的文件中,close也会自动进行一次flush。 (2)默认直接追加,而不是换行追加,如果要换行,需要手动添加换行符:Windows:\r\n,Linux:\n,Mac:\r。 (3)如果是使用write(byte[] b)写入,当要写入的数据少,缓冲数组设置的大时,第一次读就无法充满数组,会造成多写入空字符的问题,建议用
| ||
关闭输出流 |
| 关闭流,释放系统资源。 |
刷新缓冲区 |
| 刷新输出流,把数据马上写到输出流中。 |
以下是用字节流实现文件复制的示例代码:
package com.ex.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
//字节流实现文件复制:因字节流特性,存在部分乱码问题
public class OutPutStreamTest {
public static void main(String[] args) {
//创建流,设定源和目的地
FileInputStream inputStream = null;
FileOutputStream outputStream=null;
try {
inputStream = new FileInputStream("D:\\a.png");
outputStream = new FileOutputStream("a.png");
//定义变量:bytes:临时存放数据的数据,len:读取数据的长度
byte[] bytes = new byte[1024];
int len=-1;
//写出数据
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭流
try {
if (outputStream!=null){
outputStream.close();
}
if (inputStream!=null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:流关闭前最好进行判空一下,为了避免报空指针异常。
2.文件字符输入流、输出流
java.io.FileReader 是 java.io.Reader的子类,是文件字符输入流。
java.io.FileWriter 是 java.io.Writer的子类,是文件字符输出流。
字符流传输数据以字符为单位,字节流需要选择编码方式,但是字符流不需要,是已经指定好的,因此无论是中文、英文还是标点,都可以正确解读,不用担心中文乱码问题。
常用方法和文件字节输入流、输出流差不多。不再多写。
以下是用字符流实现文件复制的示例代码:
package com.ex.io;
import java.io.*;
//用字符流实现文件复制
public class ReaderTest {
public static void main(String[] args) {
Reader reader=null;
Writer writer=null;
try {
//创建输入、输出流
reader = new FileReader("D:\\doc_words.txt");
writer = new FileWriter("b.txt");
//定义变量:chars:临时存放数据的数据,len:读取数据的长度
char[] chars=new char[1024];
int len=-1;
while ((len = reader.read(chars)) != -1) {
writer.write(chars,0,len);
}
//刷新输出流,向目的地写出数据
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭流
try {
writer.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:先关输出流,再关输入流,因为当写操作完成时读操作肯定已经完成了,但读操作完成不意味着写操作完成。
三、关于缓冲区刷新问题
(1)缓冲区原理:缓冲区其实就是封装了一个默认大小的数组进行数据的临时存储,缓冲区的数据写到硬盘上有两种可能,一是当缓冲区写满了就会自动刷到硬盘上,或者调用flush方法提前写出。
(2)close与flush:flush是强制刷新缓冲区,无论缓冲区数据有没有满都将进行数据写出,此后流没有关闭可以继续使用;close是关闭流,但会在关闭前进行一次数据刷新。
基于以上两点,其实在一般情况下无需手动flush,因为无论是close前flush,还是直接close,实际会进行的操作是一样的,都是刷新数据后关闭流。
但是当数据需要实时更新时,例如聊天记录,是需要提前刷新的,也就是手动调用flush方法。
尽管close前会flush,但手动flush是个好习惯:不确定这个流的close前是否会flush而手动flush;为了防止一些意外情况的发生导致数据丢失。
四、字节流和字符流的选择
数据底层类型:任何数据,文本、视频、图片、音频等信息底层都是以字节存储。字符只是存在于内存当中的。
缓冲区:字符流在操作时会用到缓冲区(内存),通过缓冲区再操作文件。而字节流不用,是直接对文件本身进行操作的。
可处理:字节流可以处理任何类型的对象,但字节流不能直接处理Unicode字符。
基于以上三点,得出结论:
- 字符流以字符为单位,适合处理字符串、文本,尤其是涉及到中文的数据。
- 字节流适合处理非文本类数据,字节流可以处理数据的范围更广。
- 字符流适合处理需要频繁进行IO操作的数据,因为频繁IO意味着效率低,将数据暂时存储在字符流的缓冲区(内存),以后直接从内存中读取数据就可以避免多次IO操作,提高效率。
五、Properties集合
java.util.Properties类表示了一个持久的属性集,可以进行IO操作,Properties
可保存在流中或从流中加载。
Properties类继承了Hashtable类,Hashtable类实现了Map接口,也就是说Properties
是用键值对结构对存储数据,并且键和值都是字符串。
功能类型 | 方法 | 描述 |
|
| 创建 一个空的Properties集合 |
|
| 根据key值获取对应value值,如果没有该key,则返回null |
| 同上,如果没有该key,则返回默认值
| |
| 获取集合中所有key的集合,相当于Map的entrySet() | |
|
| 保存一个键值对。如果没有该key,就添加键值对,如果有该key,就覆盖该key的原value |
存储到流中 (IO) |
| 将集合中的临时数据永久化存储到磁盘中。 out:字节输出流,不适合中文,会乱码;comments:注释。 |
| 同上。 writer:字符输出流,适合处理中文;comments:注释。 | |
从流中加载 (IO) |
| 把磁盘中保存的文件通过输入流读取到集合中,文件中用#注释的部分不读取。 注意字符输入流可以读有中文字符的数据,字节流不能,会乱码,除非是用字节输出流 写入的数据。
键和值可以用等号"="、冒号":"、双等号":="等符号进行分隔。 键和值前后的空白符将被忽略,不作为键值字符串的一部分。 |
| 同上。 | |
注意:以上有输入输出流作为参数的方法,如果不是创建的匿名对象,记得关闭流。 |
package com.ex.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
//Properties集合IO相关方法练习
public class PropritesTest {
public static void main(String[] args) throws IOException {
store();
// load();
}
//从磁盘文件加载数据到Properties集合中
public static void load() throws IOException {
Properties prop = new Properties();
prop.load(new FileInputStream("a.txt"));
System.out.println(prop);
}
//将Properties集合中的临时数据存储到磁盘文件中
public static void store() throws IOException {
//创建Properties集合
Properties prop = new Properties();
prop.setProperty("1班","55");
prop.setProperty("2班","65");
prop.setProperty("3班","75");
prop.setProperty("4班","85");
prop.setProperty("a","15");
prop.setProperty("a",",");//将覆盖掉a=15,变为a=,
prop.setProperty("b",",");
//创建输出流:如果创建的不是匿名对象要记得关闭流
//调用方法保存到磁盘
prop.store(new FileOutputStream("prop2.txt"),"data");
// prop.store(new FileWriter("prop.txt"),"data");
}
}
六、缓冲流
缓冲流是在基本的流对象基础上创建而来,是对基本流对象的增强。
缓冲流的特点是:每次创建缓冲流对象时会创建一个内置的默认大小(缓冲区大小可指通过重载的构造函数指定)的缓冲区,通过缓冲区读写,减少IO操作次数,从而提高读写效率。
缓冲流的使用:和基本流差不多,只是在基本流外面又包装了一层缓冲流,然后将读写操作移交给缓冲流来。
缓冲流的读写速度:比基本流高不少。
缓冲流是对基本流的增强,因此相应的,缓冲流也有四种:
- 字节缓冲流:BufferedInputStream 和 BufferedOutputStream。没什么新增方法,只增加了可指定缓冲区大小的重载构造函数。
- 字符缓冲流:BufferedReader 和 BufferedWriter。有好几个新增的方法。见下。
| 方法 | 描述 |
|
| 创建使用指定大小的输入缓冲区的缓冲字符输入流。 |
| 创建使用指定大小的输入缓冲区的缓冲字符输出流。 | |
|
| 读取一行文字,一行被视为以系统行终止符终止。 返回结果:返回不包含换行符的一行内容;如果流已经到达末尾,则返回null。 |
BufferedWriter新增方法 |
| len参数的值为负,则不会写入任何字符。 |
| line.separator定义,并不一定是单个换行符('\ n')字符。 |
以下是用缓冲字符流实现文件复制的示例代码:
package com.ex.io;
import java.io.*;
//用缓冲流实现文件复制
public class BufferedStreamTest {
public static void main(String[] args) {
BufferedReader bufferedReader=null;
BufferedWriter bufferedWriter=null;
try {
//创建基本流,指定源和目的地
FileReader reader = new FileReader("D:\\doc_words.txt");
FileWriter writer = new FileWriter("e.txt");
//创建缓冲流,以基本流作为参数
bufferedReader = new BufferedReader(reader);
bufferedWriter = new BufferedWriter(writer);
//进行读写操作
char[] chars=new char[1024];
int len=-1;
while ((len=bufferedReader.read(chars)) != -1){
bufferedWriter.write(chars,0,len);
}
} catch (java.io.IOException e) {
e.printStackTrace();
}finally {
//关闭流
try {
if (bufferedWriter!=null){
bufferedWriter.close();
}
if (bufferedReader!=null) {
bufferedReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:缓冲流创建是从内到外创建,关闭则是从外到内关闭,关闭了缓冲流,作为其参数的基本流也会自动关闭,内层流的关闭可以省略。
七、转换流
如果不了解字符编码可以先看这篇文章大概了解一下:常见字符编码了解:ASCII、GBK、IS0-8859-1、UTF-8。
Reader和Writer,直接子类为FileReader和FileWriter。
作用:转换流是字节和字符转换的桥梁,将读取到的字节数据按照指定编码转换成字符,将要写出的字符按照指定编码转换成字节。
FileReader和FileWriter的底层也是构建的InputStreamReader和OutputStreamWriter,按照平台默认编码进行转换,比如在IDEA运行程序,IDEA的默认编码为UTF-8,那么读取和写出就按照这个编码来转 换。
转换流可以用在我们想要字节控制读取字符和写出字符的编码时,或者想要将文件的编码进行指定转换时。
读取文件时要选择和文件本身一致的编码才不会乱码。
以下是示例代码:
//按照指定编码读取字符:要保证选取的编码和文件本身的编码一致
public class InputStreamReaderTest {
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("b.txt"), "UTF-8"));
String str=null;
while ((str=bufferedReader.readLine()) != null){
System.out.println(str);
}
bufferedReader.close();
}
}
//按照指定编码写出字符
public class OutputStreamWriterTest {
public static void main(String[] args) throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("v.txt"),"UTF-8"));
bufferedWriter.write("今天是很棒的一天");
bufferedWriter.close();
}
}
//转换文件的编码:将gbk编码的文件转换成utf-8编码的文件,并写出到另一个文件中
public class CharsetConvertTest {
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"), "gbk"));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("v.txt"),"UTF-8"));
String str=null;
while ((str = bufferedReader.readLine()) != null) {
bufferedWriter.write(str);
}
bufferedWriter.close();
bufferedReader.close();
}
}
八、对象序列化流和对象反序列化流
(1)理解序列化和反序列化的概念
java提供了一种对象序列化的内存机制,可以通过将对象转换成一个字节序列来持久保存到文件中,字节序列包括对象的类型、对象的数据、对象中存储的数据属性等信息。
反之,可以将该字节序列从文件中读取回来,通过反序列化获取对象的类型、对象的数据等信息,然后在内存中重构对象。
简而言之,对象序列化就是将对象转换成字节序列保存到文件中,对象反序列化就是从文件中读取字节序列解析对象信息然后根据这些信息在内存中创建对象。
- 序列化:对象——>字节序列
- 反序列化:字节序列——>对象
(2)理解Java中利用流进行序列化和反序列的过程及使用
序列化需要通过objectOutputStream的writeObject方法实现,反序列化需要通过ObjectInputStream的readObject方法实现。
对象在进行序列化时会做这两件事:
- 检测该对象所在的类是否支持 java.io.Serializable
- 按照对象的默认序列化机制写入对象的类型信息、属性类型以及所有非瞬态和非静态属性的值(不序列化的静态和瞬态字段的值为默认值(0,...)),而对象中的方法,不管有多少,都不会进行序列化。
对象在进行反序列化时会做这两件事:
- 判断此时找到的类的serialVersionUID和写入时的serialVersionUID是否一样,一样才能反序列化,才能重构对象。
- 将对象的类,类签名以及所有非瞬态和非静态字段的值等信息的字节序列解析重构为对象。
解读以上过程:
- 只有支持 java.io.Serializable 接口的对象才能写入流中,否则会抛出序列化异常NotSerializableException。java.io.Serializable接口是一个标记接口(标识接口没有需要实现的方法),类通过实现此接口来启用序列化功能,当我们进行序列化和反序列化操作时,就会检测是否有这个标记,有这个标记进行才能序列化和反序列化操作。
- 被static修饰的成员变量是不能序列化的,序列化的都是非静态变量。因为静态变量优是先于对象加载到内存中的,静态变量与类相关,而序列化和反序列化是针对对象的。
- 被transient修饰的成员变量不能序列化。transient关键字(瞬态关键字),当只需要避免该变量序列化时,用该关键字。
- 如果一个对象在被写入文件后,在读取文件时无法查找到此类,会报ClassNotFoundException。
- 如果一个对象在被写入文件后,它所在的类中有内容被修改,会报InvalidClassException异常。因为序列化运行时将每个可序列化的类与称为serialVersionUID的版本号相关联,该序列号在反序列化期间用于验证序列化对象的发送者和接收者是否已加载与该序列化兼容的对象的类。 如果接收方加载了一个具有不同于相应发件人类的serialVersionUID的对象的类(修改后的类和修改前的类的serialVersionUID不同),则反序列化将导致
InvalidClassException
。 一个可序列化的类可以通过声明一个名为"serialVersionUID"
的字段来显式地声明它自己的serialVersionUID,该字段必须是static finallong
类型,这样无论类有没有在序列化后被修改都不会有影响。 - 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。即使父类没有实现序列化,子类也可以是可序列化的。
- 如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存。
什么时候需要序列化?(参考Java基础学习总结——Java对象的序列化和反序列化)
- 硬盘上,通常存放在一个文件中。 在很多应用中,需要让数据离开内存,保存到物理硬盘中,以便长期保存,
- 在网络上传送对象的字节序列。当两个进程在进行远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
以下为几个示例代码:
//将一个对象转换为字节序列并写入文件
public class OneObjectTest {
public static void main(String[] args) throws IOException {
//创建对象
Student student = new Student("王丽丽", 15);
//创建对象序列化流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
//写出
objectOutputStream.writeObject(student);
//关闭流
objectOutputStream.close();
}
}
//通过集合一次写入多个对象到文件中
public class ManyObjectTest {
public static void main(String[] args) throws IOException {
//创建对象
List<Student> list=new ArrayList<>();
Student student1 = new Student("王丽丽", 15);
Student student2 = new Student("秦凯丽", 16);
Student student3 = new Student("李小时", 20);
Student student4 = new Student("张右右", 18);
list.add(student1);
list.add(student2);
list.add(student3);
list.add(student4);
//创建对象序列化流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
//写出
objectOutputStream.writeObject(list);
//关闭流
objectOutputStream.close();
}
}
//从文件中读取将字节序列并转换为对象
public class ReadObjectTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建对象反序列化流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
//读取
Object object = objectInputStream.readObject();
System.out.println(object);
//关闭流
objectInputStream.close();
}
}
九、打印流
打印流为其他输出流添加了功能,可以通过打印流包装其他输出流,调用print或println来实现各种数据类型的打印。
与其他输出流不同,打印流从不抛出IOException,异常情况只是设置一个内部标志,可以通过checkError方法进行测试。
打印流可以通过包装其他输出流来设定输出目的地,也可以直接在构造函数里设置目的地,打印流可以开启自动刷新,可以设置字符编码。
- 打印字节流:java.io.PrintStream
- 不包装缓冲流:PrintStream的所有写出方法包括print、println、append、format,无需关心是否自动刷新的,无需手动flush或在构造函数里设置autoFlush=true,甚至是忘记关闭打印流,也不会丢失数据。
- 包装缓冲流:使用构造方法设置autoFlush=true或手动flush,否则数据将丢失。
- 对于写入原始字节的方法,程序应该使用未编码的字节流。
- 我们经常用的System.out.print();和System.out.print();就是System类里有一个PrintStream类型的单例对象out,调用的print和println都是PrintStream里的方法。
- System.out的目的地可以修改,原本目的地是控制台,可以修改成文件,通过 System.setOut(PrintStream out) 方法来设置即可。但是不建议这么做,因为这样会影响这个程序这行代码之后的所有System.out语句,而且明明可以通过打印流包装输出流来打印到文件里,这么做真的没啥好处。
- 打印字符流:java.io.PrintWriter
- PrintWriter和PrintStream不同,无论是否包装缓冲流,写出方法都没有实现自动刷新,这意味着写出方法必须至少保证手动flush或关闭流或在构造函数里设置autoFlush=true中的至少一项,才能保证数据不丢失。
- 打印字符用PrintWriter类,将会把字节按照平台的默认编码进行转换。
示例代码如下:
//格式化输出的使用
public class PrintStreamTest {
public static void main(String[] args) throws IOException {
PrintWriter printWriter = new PrintWriter(new FileWriter("print.txt"));
printWriter.format("姓名:%s 年龄:%d","pick",18);
printWriter.close();
}
}
//用打印流包装输出流,方便地输出各种数据
public class PrintStreamTest {
public static void main(String[] args) throws IOException {
PrintWriter printWriter = new PrintWriter(new FileWriter("print.txt"));
printWriter.println(true);
printWriter.println(12);
printWriter.print(new Student("旺旺",23));//会调用对象的toString()
printWriter.close();//此处刷新
}
}