1 IO流概述和分类
1.1 IO流概述
Java IO流是用于输入和输出数据的机制。Java提供了丰富的IO类和接口,用于处理不同类型的数据。使用合适的IO流类,可以处理二进制数据、文本数据、对象数据等各种情况,并且可以对数据进行缓冲、处理和操作。同时,IO流涉及数据的读写,所有不需要使用时及时关闭它们,可以手动调用close()方法关闭流,并处理可能抛出的IO异常,或者当有多个流时可以使用try-with-resource语句块,这样就不需要手动关闭流以及处理对应可能的异常。
1.2 IO流分类
首先按流向可以分为:输入流,输出流。
输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流。
输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中去的流称为输入流。
输出流的作用:写数据到文件,或者写数据发送给别人。
输入流的作用:读取数据到内存。
按照流的内容或操作单位分为: 字节流,字符流。
字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流。
字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流。(针对于文本内容)
所以流大体分为四大类:
字节输入流(InputStream):以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流。
字节输出流(OutputStream):以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流。
字符输入流(Reader):以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流。
字符输出流(Writer):以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流。
以上都是抽象的基类,Java中针对不同的内容使用不同的IO流继承类进行处理
常用的IO流分类体系
2 字节流的使用
2.1 以字节流读取数据
2.1.1 以单个字节读取数据
使用字节流从文件一个字节一个字节的形式读出,我在bree.txt文件中已经输入了”byte字节”几个字符。此种读取形式很慢,只是为了测试用,其他情况禁止使用。
public class FileInputStreamDemo01 {
public static void main(String[] args) throws Exception {
// 1.创建一个文件对象bree.txt
File file = new File("src/bree.txt");
// 2.创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream(file);
// 3.读取一个字节的编号返回,
int code1 = is.read(); // 读一个字节
System.out.println("第一个字节:"+(char)code1);
int code2 = is.read(); // 读一个字节
System.out.println("第二个字节:"+(char)code2);
int code3 = is.read(); // 读一个字节
System.out.println("第三个字节:"+(char)code3);
int code4 = is.read(); // 读一个字节
System.out.println("第四个字节:"+(char)code4);
int code5 = is.read(); // 读一个字节
System.out.println("第四个字节:"+(char)code5);
File file1 = new File("src/bree.txt");
// 再次创建一个字节输入流管道与源文件接通,使用while读取字节数
InputStream is1 = new FileInputStream(file);
int ch = 0 ;//定义一个整数变量存储字节编号,读取完毕返回-1
System.out.print("循环输出:");
while((ch = is1.read())!= -1){//从输入流中读取每一个字节给ch,直到读完ch=—1。
System.out.print((char) ch);//将每次读取的字节转成字符型输出。
}
}
}
输出
可以看到当读到第四个字节的时候出现了乱码,没有出现”字”字符,而前面的四个字符都正常打印,又从第二次循环一次一个字节的形式读取字节流,也发现前四个字符正常输出,而到来中文”字节”的时候出现了乱码。这是因为在UTF-8中,中文字符以三个字节形式编码(GBK中是两个),而英文字符和数字都是以一个字节的形式编码。所有当我们以一个字节的形式读取字节流时,所有英文字符正常输出,而中文字符相当于被拆分成了三个字节进行解码,从而出现了乱码。
2.1.2 以数组为单位读取数据
定义一个定长的字节数组buffer,传入read()方法中以每次读取一个字节数组,并返回读取个数。
public int read(byte[] buffer):
//从字节输入流中读取字节到字节数组中去,
//返回读取的字节数量,没有字节可读返回-1。
此时我将bree.txt中的数据改为”byt字节”,并以三个字节的形式读取。
public class FileInputStreamDemo02 {
public static void main(String[] args) throws Exception {
// 需求:读取文件中的数据输出。
// 1.创建一个文件对象
//File srcFile = new File("Day09Demo/src/dlei02.txt");
// 2.创建一个字节输入流管道与源文件对象接通。
//InputStream is = new FileInputStream(srcFile);
// 3.简化写法:直接创建一个字节输入流管道与源文件路径接通。
InputStream is = new FileInputStream("src/bree.txt");
// 4.使用循环读取
byte[] buffer = new byte[3];//每次读3个字节,实际开发一般是1024个,相当于每次读取1kb
int len ; // 存储每次读取的字节数。
while((len = is.read(buffer)) != -1){//从输入流中读取3个字节放入buffer中,并返回len(字节数),直到读完len=—1。
// 读取了多少就倒出多少!
String rs = new String(buffer , 0 , len);//buffer满就全倒,不满有多少倒多少,将字节数组转为字符串
System.out.print(rs);
}
}
}
输出
可以看到正常输出,但是当改为每次读取四个字节时
输出
还是出现了乱码,所有使用字节数组读取文本内容输出,也无法避免中文读取输出乱码的问题。
2.2 以字节流写出数据
public class OutputStreamDemo04 {
public static void main(String[] args) throws Exception {
OutputStream os = new FileOutputStream("src/bree01.txt");//初始化输出流,输出到src下的bree.txt文件,如果,没有会自动创建
//写出一个字节
os.write(97);//字节a
os.write("\r\n".getBytes());//换行用
os.write('b');//字节b
os.write("\r\n".getBytes());//换行用
os.write('万');//三个字节,只会写出第一个字节,乱码
os.flush();//刷新数据到文件中,刷新后还能使用
os.close();//直接关闭,包含刷新,不能再使用
}
}
文件中写入内容
所以字节流并不适合读写文本文件内容输出,读写文件内容建议使用字符流。
3 字符流的使用
3.1 以字符流读取数据
bree.txt文件数据已经改为”char字符”,以一个字符的形式读取。
public class FileReaderDemo01 {
public static void main(String[] args) throws Exception {
// 1.创建一个文件对象定位源文件
// File f = new File("Day10Demo/src/dlei01.txt");
// 2.创建一个字符输入流管道与源文件接通
// Reader fr = new FileReader(f);
// 3.简化写法:创建一个字符输入流管道与源文件路径接通
Reader fr = new FileReader("src/bree.txt");
// 4.while循环一个一个字符读取。
// 定义一个变量存储一个字符的编号
int ch ;
while ((ch = fr.read()) != -1){
System.out.print((char)ch);
}
}
}
输出正常
以一个字符数组的形式读出。
public class FileReaderDemo02 {
public static void main(String[] args) throws Exception {
// a.创建一个字符输入流管道与源文件接通
Reader fr = new FileReader("src/bree.txt");
// b.使用循环按照字符数组读取数据
char[] buffer = new char[3]; // 1K
// c.定义一个整数记录每次读取到数组中的字符数据量。
int len;
while((len = fr.read(buffer)) != -1 ) {
// 读取多少倒出多少
System.out.print(new String(buffer, 0 , len));
}
}
}
同样输出正常
3.2 以字符流写出数据
public class FileWriterDemo03 {
public static void main(String[] args) throws Exception {
// 1.创建一个字符输出流管道通向目标文件路径
//Writer fw = new FileWriter("src/bree.txt");
Writer fw = new FileWriter("src/bree.txt");
// 2.public void write(int c):写一个字符出去
fw.write(77); // 字符
fw.write('b'); // 字符b
fw.write('万'); // 字符万,此时没有任何问题。
fw.write("\r\n"); // 换行
// 3.public void write(String c):写出一个字符串
fw.write("写出一个字符串");
fw.write("\r\n"); // 换行
// 4.public void write(char[] buffer):写出一个字符数组
fw.write("写出一个字符数组".toCharArray());
fw.write("\r\n"); // 换行
// 5.public void write(String c ,int pos ,int len):写出一部分字符串
fw.write("写出一部分字符串",0,9);
fw.write("\r\n"); // 换行
// 6.public void write(char[] buffer ,int pos ,int len):写出一部分字符数组
fw.write("写出一部分字符数组".toCharArray(),0 ,2);
fw.write("\r\n"); // 换行
fw.close();
}
}
输出
4 缓冲流的使用
缓冲流:缓冲流可以提高字节流和字符流的读写数据的性能。
缓冲流分为四类:
(1)BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。
(2)BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。
(3)BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。
(4)BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。
4.1 字节缓冲流
直接进入字节缓存流的性能统计分析,分别是:
- 低级流一个一个字节复制,速度太慢,直接淘汰。
- 低级的字节流按照一个一个字节数组的形式复制,读取较慢。
- 高级的缓冲字节流按照一个一个字节的形式复制,读取较慢。
- 高级的字节缓冲流按照一个一个字节数组的形式复制,速度极快。
为了验证以上,还是在文件bree.txt中写入了250多万个中文字符。通过四种方式复制到四个文件中。测试四种方式复制的运行耗时。
public class CopyDemo {
public static final String SRC_FILE = "src/bree.txt";//读取文件
public static final String DEST_FIlE = "src/bree";//写出文件
public static void main(String[] args) {
copy01(); // 低级流一个一个字节复制,速度太慢,禁止使用!
copy02(); // 低级的字节流按照一个一个字节数组的形式复制 ,读取较慢。
copy03(); // 高级的缓冲字节流按照一个一个字节的形式复制 ,读取较慢。
copy04(); // 高级的字节缓冲流按照一个一个字节数组的形式复制,速度极快。建议使用
}
/** (1)使用低级的字节流按照一个一个字节的形式复制文件。*/
public static void copy01(){
long startTimer = System.currentTimeMillis();
try(
// 1.创建一个低级的字节输入流与源文件接通
InputStream is = new FileInputStream(SRC_FILE);
// 2.创建一个低级的字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream(DEST_FIlE+"01.txt");
){
// 3.定义一个整型变量存储读取的字节。
int ch ;
while((ch = is.read())!=-1){
os.write(ch);
}
}catch (Exception e){
e.printStackTrace();
}
long endTimer = System.currentTimeMillis();
System.out.println("低级的字节流按照一个一个字节的形式复制文件耗时:"+(endTimer-startTimer)/1000.0+"s");
}
/** (2)使用低级的字节流按照一个一个字节数组的形式复制文件。*/
public static void copy02(){
long startTimer = System.currentTimeMillis();
try(
// 1.创建一个低级的字节输入流与源文件接通
InputStream is = new FileInputStream(SRC_FILE);
// 2.创建一个敌机的字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream(DEST_FIlE+"02.txt");
){
// 3.定义一个字节数组存储字节
byte[] buffer = new byte[1024];
// 定义一个变量存储每次读取的字节数量。
int len ;
while((len = is.read(buffer))!=-1){
os.write(buffer,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
long endTimer = System.currentTimeMillis();
System.out.println("低级的字节流按照一个一个字节数组的形式复制文件耗时:"+(endTimer-startTimer)/1000.0+"s");
}
/** (3)使用高级的缓冲字节流按照一个一个字节的形式复制文件。*/
public static void copy03(){
long startTimer = System.currentTimeMillis();
try(
// 1.创建一个低级的字节输入流与源文件接通
InputStream is = new FileInputStream(SRC_FILE);
BufferedInputStream bis = new BufferedInputStream(is);
// 2.创建一个敌机的字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream(DEST_FIlE+"03.txt");
BufferedOutputStream bos = new BufferedOutputStream(os);
){
// 3.定义一个整型变量存储读取的字节。
int ch ;
while((ch = bis.read())!=-1){
bos.write(ch);
}
}catch (Exception e){
e.printStackTrace();
}
long endTimer = System.currentTimeMillis();
System.out.println("高级的缓冲字节流按照一个一个字节的形式复制文件耗时:"+(endTimer-startTimer)/1000.0+"s");
}
/** (4)使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。*/
public static void copy04(){
long startTimer = System.currentTimeMillis();
try(
// 1.创建一个低级的字节输入流与源文件接通
InputStream is = new FileInputStream(SRC_FILE);
BufferedInputStream bis = new BufferedInputStream(is);
// 2.创建一个敌机的字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream(DEST_FIlE+"04.txt");
BufferedOutputStream bos = new BufferedOutputStream(os);
){
// 3.定义一个字节数组存储字节
byte[] buffer = new byte[1024];
// 定义一个变量存储每次读取的字节数量。
int len ;
while((len = bis.read(buffer))!=-1){
bos.write(buffer,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
long endTimer = System.currentTimeMillis();
System.out.println("高级的字节缓冲流按照一个一个字节数组的形式复制文件耗时:"+(endTimer-startTimer)/1000.0+"s");
}
}
输出
可以看出高级的字节缓冲流按照一个一个字节数组的形式复制性能极高。
4.2 字符缓冲流
字符缓冲输入流:BufferedReader
-- 作用:字符缓冲输入流可以把字符输入流包装成一个高级的缓冲字符输入流,
• 可以提高字符输入流读数据的性能。
-- 构造器:public BufferedReader(Reader reader):
-- 原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能。
-- 缓冲字符输入流除了提高了字符输入流的读数据性能,
-- 缓冲字符输入流还多了一个按照行读取数据的功能(重点):
• public String readLine(): 读取一行数据返回,读取完毕返回null;
示例
public class BufferedReaderDemo01 {
public static void main(String[] args) throws Exception {
// 1.定义两个个原始的字符输入流读取源文件
Reader fr = new FileReader("src/bree.txt");
Reader fr1 = new FileReader("src/bree.txt");
// 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道
BufferedReader br = new BufferedReader(fr);
BufferedReader br1 = new BufferedReader(fr1);
long start = System.currentTimeMillis();
// 定义一个字符串变量存储每行数据
String line;
// 使用一个循环读取数据(经典代码)
while((line = br.readLine())!=null){
// System.out.println(line);
}
long end = System.currentTimeMillis();
System.out.println("一次一行的读取耗时:"+(end - start)/1000.0+"s");
br.close();
long start1 = System.currentTimeMillis();
// 2.定义一个字符数组循环读取
char[] buffer = new char[1024];
int len ;
while((len = br1.read(buffer))!=-1){
// System.out.println(new String(buffer , 0 , len));
}
long end1 = System.currentTimeMillis();
System.out.println("一次读取1024个字符耗时:"+(end1 - start1)/1000.0+"s");
br1.close();
}
}
输出
5 转换流的使用
之前用的代码编码和文件编码都是UTF-8编码,字符流读取没有出现乱码。
如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。
如果代码编码和读取的文件编码不一致。字符流读取的时候会乱码。
代码编码(解析) | 文件编码 | 中文 |
---|---|---|
UTF-8 | UTF-8 | 不乱码 |
GBK | GBK | 不乱码 |
UTF-8 | GBK | 乱码 |
GBK | UTF-8 | 乱码 |
解决字符流读取不同编码乱码的问题:
5.1 字符输入转换流
字符输入转换流:InputStreamReader:
-- 作用:可以解决字符流读取不同编码乱码的问题。
• 可以把原始的字节流按照当前默认的代码编码转换成字符输入流。
• 也可以把原始的字节流按照指定编码转换成字符输入流
-- 构造器:
• public InputStreamReader(InputStream is):可以使用当前代码默认编码转换成字符流,几乎不用
• public InputStreamReader(InputStream is,String charset):可以指定编码把字节流转换成字符流
示例
public class InputStreamReaderDemo01 {
public static void main(String[] args) throws Exception {
// 代码:UTF-8 文件:GBK
// 1.提取GBK文件的原始字节流
InputStream is = new FileInputStream("src/bree05.txt");
// 2.把原始字节输入流通过转换流,转换成字符输入转换流InputStreamReader
//Reader isr = new InputStreamReader(is); // 使用当前代码默认编码UTF-8转换成字符流,几乎不用!
Reader isr = new InputStreamReader(is,"utf-8"); // 指定编码把字节流转换成字符流
// 3.包装成缓冲流
BufferedReader br = new BufferedReader(isr);
// 4.定义一个字符串变量存储每行数据
String line;
// 使用一个循环读取数据(经典代码)
while((line = br.readLine())!=null){
System.out.println(line);
}
}
}
输出
5.2 字符输出转换流
字符输出转换流:OutputStreamWriter
-- 作用:可以指定编码把字节输出流转换成字符输出流。
• 可以指定写出去的字符的编码。
-- 构造器:
• public OutputStreamWriter(OutputStream os) : 用当前默认编码UTF-8把字节输出流转换成字符输出流
• public OutputStreamWriter(OutputStream os , String charset):指定编码把字节输出流转换成字符输出流
示例
public class OutputStreamWriterDemo02 {
public static void main(String[] args) throws Exception {
// 1.写一个字节输出流通向文件
OutputStream os = new FileOutputStream("src/bree06.txt");
// 2.把字节输出流转换成字符输出流。
// Writer fw = new OutputStreamWriter(os); // .把字节输出流按照默认编码UTF-8转换成字符输出流。
Writer fw = new OutputStreamWriter(os,"GBK"); // 把字节输出流按照指定编码GBK转换成字符输出流。
fw.write("241633447711maxlec字符输出转换流");
fw.close();
}
}
可以看到,以GBK编码解析转换成字符输出流写到到文件bree06.txt,然后文件以UTF-8解码时发送错误,出现乱码。
更改为UTF-8解析后正常写出
6 对象序列化与反序列化
6.1 序列化
对象序列化流(对象字节输出流):ObjectOutputStream
-- 作用:把内存中的Java对象数据保存到文件中去。
-- 构造器: public ObjectOutputStream(OutputStream out)
-- 序列化方法:public final void writeObject(Object obj)
注意:对象如果想参与序列化,对象必须实现序列化接口Serializable ,否则序列化失败!
定义一个简单的User类,并实现序列化接口Serializable。
public class User implements Serializable {
private String Name;
// transient修饰的成员变量不参与序列化
private transient String Gender;
private int Age;
public User() {
}
public User(String name, String gender, int age) {
Name = name;
Gender = gender;
Age = age;
}
@Override
public String toString() {
return "User{" +
"Name='" + Name + ''' +
", Gender='" + Gender + ''' +
", Age=" + Age +
'}';
}
}
进行序列化
public class SerializeDemo01 {
public static void main(String[] args) throws Exception {
// 1.创建User用户对象
User user = new User("诺里斯","男",20);
// 2.创建低级的字节输出流通向目标文件
OutputStream os = new FileOutputStream("src/obj.dat");
// 3.把低级的字节输出流包装成高级的对象字节输出流ObjectOutputStream
ObjectOutputStream oos = new ObjectOutputStream(os);
// 4.通过对象字节输出流序列化对象:
oos.writeObject(user);
// 5.释放资源
oos.close();
}
}
输出,可以看到obj.dat文件里面已经存有数据了,这里报不能用UTF-8编码加载文件,这不是乱码问题,而是序列化后.dat的存储格式,底层才能识别。
6.2 反序列化
对象反序列化(对象字节输入流):ObjectInputStream
-- 作用:读取序列化的对象文件恢复到Java对象中。
-- 构造器:public ObjectInputStream(InputStream is)
-- 方法:public final Object readObject()
如果一个字段不想参数序列化:
transient修饰该成员变量,它将不参与序列化!
序列化版本号:必须序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化!否则报错!
加入序列版本号:private static final long serialVersionUID = 7L;
public class SerializeDemo02 {
public static void main(String[] args) throws Exception {
// 1.定义一个低级的字节输入流通向源文件
InputStream is = new FileInputStream("src/obj.dat");
// 2.把字节输入流包装成高的对象字节输入流
ObjectInputStream ois = new ObjectInputStream(is);
// 3.反序列化
User user = (User) ois.readObject();
System.out.println(user);
System.out.println("反序列化完成");
}
}
输出,这里报错,因为类的版本不同,原来序列化文件的类与我们反序列化定义的类不相容,不能进行转化。
这里需要在类中加上定义类的版本号(这里设为7),然后再次执行序列化和反序列化。
private static final long serialVersionUID = 7L; 反序列化成功。
反序列化成功
7 打印流
打印流PrintStream / PrintWriter.
打印流的作用:
1.可以方便,快速的写数据出去。
2.可以实现打印啥出去,就是啥出去。
打印流的构造器:
public PrintStream(OutputStream os)
public PrintStream(String filepath)...
public PrintWriter(OutputStream os)
public PrintWriter(String filepath)...
可以看到PrintWriter底层包装了缓冲流
PrintStream中构造器中也包装了缓冲流,其他方法调用进行内部调用。
示例:
public class PrintStreamDemo01 {
public static void main(String[] args) throws Exception {
// 1.打印流PrintStream
PrintStream ps = new PrintStream("src/bree07.txt");
// 2.打印流PrintWriter
PrintWriter pw = new PrintWriter("src/bree08.txt");
ps.println(97);
ps.println(110);
ps.println("打印流PrintStream");
pw.println(true);
pw.println('万');
pw.println("打印流PrintWriter");
ps.close();
pw.close();
}
}
避免被文件被覆盖,分别输出到bree07.txt和bree08.txt文件中。