【第26、27天】Java的IO流

1 IO流概述

       IO流(I = Input 输入,O = Output 输出,流 = 数据从源点传输到汇点的管道)。

  • 分类:

· 按方向分 :输入流或输出流
在这里插入图片描述

输入流和输出流以Java程序作为参考,将文件读入Java程序变成一个对象来操作,指的是Input输入流;将对象从Java文件输出到文件系统中,指的是Output输出流。

· 按单位分 :字节流或字符流

bit是位(简称“b”),也叫比特位,是计算机表示数据最小的单位。byte是字节(通常称为Byte,简称“B”),1B=8b(1个字节包含了8个位)。
但需要注意的是,一个字节占有8个位是固定的;但一个字符占有几个位是不确定的,这与其文本选择的编码方式有关:
1.ASCII码:
一个英文字符(不分大小写)占一个字节(8位)的位置,一个中文字符占两个字节(16位)的位置。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数。换算为十进制,最小值0,最大值255。一个ASCII码就是一个字节。
2.UTF-8编码:
一个英文字符(不分大小写)占一个字节,一个中文占三个字节。
3.Unicode编码:
一个英文字符(不分大小写)占两个字节,一个中文字符占两个字节。
符号:英文标点占一个字节,中文标点占两个字节。

字节流可以读各种文件,但不能智能判断字符的结束,只能按字节读;字符流只能读文本文件,但按字符智能识别分隔汉字、英文等字符。

· 按功能分 :节点流或过滤流 (也叫包装流、处理流)

节点流可以直接对文件(这里被当作节点)进行操作,相当于针头;而过滤流只能在参数中传入节点流来操作,给原本的节点流添加新的功能,相当于针管,可以加不同的针管,而针头只有一个且只有针头可以用来接触皮肤(在这里理解为操作文件)。

同一个流按照三个不同的划分方式被划分三种不同的类型,如FileInput/OutputStream就被划分为节点流,输入/出流,字节流。

2 处理字节(字节流)

2.1 InputStream和OutputStream

       InputStream和OutputStream分别定义了所有字节输入流、输出流统一的父类,是两个抽象类,定义了所有字节输入/输出流都要会的方法,本身不能被创建对象。继承这个抽象类的子类可以使用的负责读/写较为重要的方法如下:

int read()//得到下一个字节(数据)的编码值
int read(byte[] data)//传入data,一次取多个字节,返回取到的字节长度
int read(byte[] data, int off, int len)
write(int data)
write(byte[] data)
write(byte[] data, int off, int len)

off意为偏移量,从数组元素第几位开始读/写;len意为步长,要一次性从流中读/向文件中写多长。
off和len之和不能大于data数组的长度。
read()的读取、写入方法和迭代器比较像,在一次循环中使用一次read()即向下读一个字符,
所以一次循环,read()出现一次,将这个变量赋一个变量名接收。

所有实现了InputStream和OutputStream的实现类在使用完之后注意都需要关闭(调用.close()),否则将会一直占用内存。在调用close时,需要抛出或try-catch异常。当有过滤流存在,传入的节点流不需要关闭,过滤流关闭后节点流即关闭。
关闭流的一般顺序是:先打开的后关闭,后打开的先关闭。若流a依赖流b,应该先关闭流a,再关闭流b。

2.1.1 实现类中使用try-catch处理close()

这里以FileInputStream和FileOutputStream为例,使用try-catch的嵌套关闭:

import java.io.*;
public class ExampleFileCopyWithTryCatch{
	public static void main(String[] args){
		FileInputStream fis = null;
		FileOutputStream fos = null;
		try{
			fis = new FileInputStream("a.jpg");
			fos = new FileOutputStream("b.jpg");
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				fis.close();
			}catch(Exception e){
				e.printStackTrace();
			}finally{
				try{
					fos.close();
				}catch(Exception e){
					e.printStackTrace();
				}
			}

		}
	}
}

JDK7.0以后,try-catch出现了带有资源控制的try-catch语法(Try with Resources),只需要在try后面的括号中定义需要控制的资源,在try结束后自动关闭,不需要再执行close()。

import java.io.*;
public class ExampleFileCopyWithTWR{
	public static void main(String[] args){
		try(FileInputStream fis = new FileInputStream("a.jpg");FileOutputStream fos = new FileOutputStream("b.jpg")){
			
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

2.2 FileInputStream和FileOutputStream

       作为节点流,输入/出流,字节流。

FileInputStream fis = new FileInputStream(File f[或String s]);
FileOutputStream fos = new FileOutputStream(File f[或String s]);
  • 构造方法允许传入String或者File。构造方法中只能连接文件,绝对不能连接目录,否则直接出现异常(拒绝访问)。

  • 节点输出流如果连接的文件不存在,也会在创建流时自动创建而不需手动创建,但是如果目录也不存在,不允许被创建,还会报异常(系统找不到指定路径),此时可以使用File类的方法mkdirs()。

  • 注意:在创建节点输出流对象时,即便连接的文件已经存在,也会在创建流的那一刻 被新的空白文件替换。 若不想替换,只要在原有内容之后追加新的内容,可以在构造方法中指定追加模式开启。即

      new FileOutputStream("a.txt",true);
    
  • FileInputStream 最常用的是read(byte[] data),效率较高
    FileOutputStream 最常用的方法write(byte[],int,int),防止出现尾部重复复制出现冗余数据的情况。

  • FileInputStream以 -1作为读取文件结束的标识。

import java.io.*;
//按字节读取abc.txt的内容
public class ExampleFileInputStream{
	public static void main(String[] args)throws Exception{
		FileInputStream fis = new FileInputStream("abc.txt");
		//将字节放入一个数组,按数组读取出来,效率更高
		byte[] data = new byte[3];
		
		//用来接收fis.read(),此时变量代表获取到的数组长度,所以叫length
		int lenth;
		//read()中传入数组完成一次读取多个字节
		//read(data)返回值为一次取字节的长度,当返回长度-1,即表示读取结束
		//使用这个方法后,data数组中包含取出的字节
		while((lenth = fis.read(data)) != -1){
			//读data数组中的内容
			for(int i = 0; i < lenth; i++){
				//只能按字节输出,纯英文和数字可以正常输出
				System.out.print((char)data[i]);
			}
		}
		/*
		//效率太低,逐个字节读取出来,此时fis.read()返回的是字节编码,所以命名为data
		int data;
		while((data = fis.read()) != -1){
			System.out.print((char)data);
		}
		*/
		fis.close();
	}
}
import java.io.*;
//使用FileInputStream和FileOutputStream复制一张图片
public class ExampleFileOutputStream{
	public static void main(String[] args)throws Exception{
		FileInputStream fis = new FileInputStream("a.jpg");
		FileOutputStream fos = new FileOutputStream("b.jpg");
		int length;
		//3B -> 3KB -> 3MB,此时byte数组可以一次从流中读取3MB数据,提高效率
		byte[] data = new byte[3 << 10 << 10]; 
		while((length = fis.read(data))!=-1){
			fos.write(data, 0, length);
		}
		fis.close();
		fos.close();
	}
}
  • 例题
//将secret.jpg追加到b.jpg后面,以完成加密secret的结果
import java.io.*;
public class TestEncrypted{
	public static void main(String[] args)throws Exception{
		FileInputStream fis = new FileInputStream("secret.jpg");
		//在源文件b.jpg的基础上允许追加
		FileOutputStream fos = new FileOutputStream("b.jpg",true);
		byte[] data = new byte[3 << 10 << 10];
		int len;
		while((len = fis.read(data))!=-1){
			fos.write(data,0,len);
		}
		fis.close();
		fos.close();
	}
}
//将secret.jpg从b.jpg中读出,以达到解密的效果
import java.io.*;
public class TestDecrypted{
	public static void main(String[] args)throws Exception{
		FileInputStream fis = new FileInputStream("b.jpg");
		//b.jpg原来的大小为132353字节,先把原来的b.jpg读出,此时光标已经移至b.jpg所占字节的后面
		//再读即可以读出secret.jpg
		byte[] data = new byte[132353];
		fis.read(data);
		data = new byte[3<<10<<10];
		FileOutputStream fos = new FileOutputStream("秘密信息.jpg");
		int len;
		//读秘密信息,此时光标下一个字节即是加密之前的secret.jpg,读出即可
		while((len = fis.read(data))!=-1){
			fos.write(data,0,len);
		}
		fis.close();
		fos.close();
	}
}

2.3 BufferedInputStream和BufferedOutputStream

       给原本的字节流节点流添加将读写数据存入缓冲区提高效率的功能,作为字节流,输入/出流,过滤流。 为了给原本的节点流添加巨大的缓冲空间,从而提高每次读写的吞吐量进而提高效率(每次读写多个字节),这与将FileInputStream和FileOutputStream使用byte数组多字节读取一样,只是不需要再去定义数组,底层自动实现数组(缓冲区)。

BufferedInputStream fis = new BufferedInputStream(InputStream is, int size[可选]);
BufferedOutputStream fos = new BufferedOutputStream(OutputStream os, int size[可选]);

       过滤流中,构造方法只允许传入其它的流(即InputStream、OutputStream的实现类),不能直接连接File、String类型。构造方法第二个参数都可以指定缓冲空间大小,默认传入8192(8KB)。
       请务必注意缓冲区要及时清空,防止数据滞留缓冲空间导致数据丢失。缓冲区在装满时才会执行读写操作,否则不会执行。若用完不关,最后一次读取数据无法充满缓冲区,就会导致无法将文件的内容完整地读写,需要flush()或close()才会将缓存中的数据清理出来。

  • 主动清空缓冲的方法是:flush()
  • 缓冲区自动清空的条件 :
    • 缓冲区如果已满,会自动清空,无需操作
    • 关闭流close()的操作,会触发清空缓冲操作
import java.io.*;
public class ExampleBufferedStream{
	public static void main(String[] args)throws Exception{
		FileInputStream fis = new FileInputStream("1.mp3");//针头
		BufferedInputStream bis = new BufferedInputStream(fis,3<<20);//针管
		FileOutputStream fos = new FileOutputStream("3.mp3");
		BufferedOutputStream bos = new BufferedOutputStream(fos,3<<20);

		int data;
		while((data = bis.read())!=-1){
			bos.write(data);
		}
		//如果此时不关流却想清空缓存,可以使用flush(),但如果使用了close(),则自动清空
		//过滤流关闭了,传入的节点流也就关闭了
		//bos.flush();
		bos.close();
		bis.close()
	}
}
  • IO操作字节需要熟悉的两种复制方式:

    • 使用FileInputStream和FileOutputStream,配合一个大数组完成文件的复制
    • 使用FileInputStream + BufferedInputStream和FileOutputStream + BufferedOutputStream,配合缓冲区完成文件的复制
	try(FileInputStream fis = new FileInputStream("源文件");FileOutputStream fos = new FileOutputStream("目标文件")){
		byte[] data = new byte[8192];
		int len;
		while((len = fis.read(data))!=-1){
			fos.write(data,0,len);
		}
	}catch(Exception e){
		e.printStackTrace();
	}
	try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream("源文件"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("目标文件"))){
		int data;
		while((data = bis.read())!=-1){
			bos.write(data);
		}
	}catch(Exception e){
		e.printStackTrace();
	}

2.4 DataInputStream和DataOutputStream

       给原本的字节流节点流添加读写基本数据类型的功能,作为字节流,输入/出流,过滤流。

DataInputStream dis = new DataInputStream(InputStream is);
DataOutputStream dos = new DataOutputStream(OutputStream os);

       过滤流中,构造方法只允许传入其它的流(即InputStream、OutputStream的实现类),不能直接连接File、String类型。

  • 核心方法:
    DataInputStream : readXxxx() 有返回值
    DataOutputStream : writeXxxx(参数)
    返回值、参数以及Xxxx是四类八种基本数据类型,数据类型需保持一致。

       此时DataInputStream不能再以-1作为读取结束的标识了。若已到达文件结尾,继续读取将会出现EOFException(End Of File)异常。

import java.io.*;
public class ExampleDataStream{
	public static void main(String[] args)throws Exception{
		int time = 1;
		File toRead = new File("openTimes.txt");
		DataInputStream dis = new DataInputStream(new FileInputStream(toRead));
		if(toRead.exists()){
			time = dis.readInt();
			time += 1;
		}
		DataOutputStream dos = new DataOutputStream(new FileOutputStream(toRead));
		dos.writeInt(time);
		System.out.println("已执行" + time + "次");
		dis.close();
		dos.close();
	}
}

在这里插入图片描述
在这里插入图片描述

  • 为何会出现在控制台中执行可以读出文字,在写入的文档中打开却看不到相应的文字?
    内存中各种基本数据类型的对象都靠DataInputStream读出到程序中,DataOutputStream将二进制编码写入文件,操作系统中打开文件时显示的是每八位取二进制编码后转为ASCII码。英文和字符每个字符占一个字节(8位),可以原文本读出;汉字每个字符两个字节(16位),这两个字节被操作系统分割读取展示,出现乱码。

2.5 ObjectInputStream和ObjectOutputStream

       给原本的字节流节点流添加读写引用数据类型的功能,作为字节流,输入/出流,过滤流。

ObjectInputStream ois = new ObjectInputStream(InputStream is);
ObjectOutputStream oos = new ObjectOutputStream(OutputStream os);

       过滤流中,构造方法只允许传入其它的流(即InputStream、OutputStream的实现类),不能直接连接File、String类型。

  • 核心方法:
    ObjectInputStream : readXxxx() 返回Object
    ObjectOutputStream : writeXxxx(Object obj)

       此时ObjectInputStream同样不能再以-1作为读取结束的标识了。若已到达文件结尾,继续读取将会出现EOFException(End Of File)异常。

import java.io.*;
public class ExampleObjectStream{
	public static void main(String[] args)throws Exception{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.data"));
		Teacher tea = new Teacher("Jason",25);
		System.out.println(tea);//name: Jason age:25 pc:Dell
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.data"));
		oos.writeObject(tea);
		Object obj = ois.readObject();
		Teacher teaNew = (Teacher) obj;
		System.out.println(teaNew);//name: Jason age:25 pc:null
		oos.close();
		ois.close();
	}
}
class Computer{
	String logo;
	public Computer(String logo){
		this.logo = logo;
	}
	@Override
	public String toString(){
		return logo;
	}
}
class Teacher implements Serializable{
	String name;
	int age;
	transient Computer pc;
	public Teacher(String name,int age){
		this.name = name;
		this.age = age;
		pc = new Computer("Dell");
	}
	@Override
	public String toString(){
		return "name: " + name + " age:" + age + " pc:" + pc;
	}
}
  • 此时在操作系统读取data.data文件依然会出现乱码,原因与上一节相同:
    ObjectOutputStream将二进制编码写入文件,操作系统中打开文件时显示的是每八位取二进制编码后转为ASCII码。英文和字符每个字符占一个字节(8位),可以原文本读出;汉字每个字符两个字节(16位),这两个字节被操作系统分割读取展示,出现乱码。

  • 想要持久化,必先序列化。 即想要持久地将引用数据类型对象的数据存储到磁盘上(想要被持久化到磁盘上的对象的类型),必须先要对类序列化(实现Serializable接口)。

  • Serializable是一种标志接口,可以不实现任何方法,但编译器看到这个接口后需要对这个类进行一些额外操作, 这里的额外操作指的是对对象数据进行序列化。

  • 什么是序列化?
    把属性逐个写进文件时,JVM需要先测量这个对象的属性。分清属性如何存放,并将类、类属性、成员方法生成一个校验码。生成序列化的唯一版本号serialVersionUID(序列化的作用) 当类的属性出现改变之后,会导致校验版本号校验不一致,出现异常。

  • 若被序列化类中的属性中有引用数据类型,这个引用数据类型也要实现序列化接口。 若类中某些属性无关紧要,不需要参与持久化,可以使用修饰符transient(短暂的,不参与持久化的)修饰。被transient的引用类型不需要持久化。被transient的属性序列化时写入磁盘存为null,即生命周期仅存于调用者的内存中而不会写到磁盘里持久化。被transient的信息往往是一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输。

  • 若想要持久化的是一个集合对象,那么要求集合当中元素的类型必须实现序列化接口。 如果要持久化的是一个使用了比较器的TreeSet或者TreeMap,就连比较器对象都需要进行序列化(其实比较器是TreeSet或者TreeMap的第一个属性)。

3 处理字符(字符流)

字节流可以读写各种文件,但不能智能判断字符的结束,只能按字节读和写。字符流按字符读写,只能读文本文件(.txt文件),智能识别汉字、英文等,智能断开。

3.1 Reader和Writer

       Reader和Writer分别定义了所有字符输入流、输出流统一的父类,是两个抽象类,定义了所有字符输入/输出流都要会的方法,本身不能被创建对象。继承这个抽象类的子类可以使用的负责读/写较为重要的方法如下:

	int read()
	int read(char[] data)
	int read(char[] data,int off,int len)
	write(int data)
	write(char[] data)
	write(char[] data,int off,int len)

方法的使用规范等同于InputStream和OutputStream的读写方法。

3.2 FileReader和FileWriter

       作为节点流,输入/出流,字符流。创建对象的方式、方法的使用与FileInputStream和FileOutputStream一样。只是方法需要传入的数组由byte[](字节数组)变成了char[](字符型数组)。

FileReader fr = new FileReader(File f[或String s]);
FileWriter fw = new FileWriter(File f[或String s]);
//效率低
import java.io.*;
public class ExampleFileReader1{
	public static void main(String[] args)throws Exception{
		FileReader fr = new FileReader("a.txt");
		int data;
		while((data = fr.read()) != -1){
			System.out.print((char)data);
		}
		fr.close();
	}
}
import java.io.*;
public class ExampleFileReader2{
	public static void main(String[] args)throws Exception{
		FileReader fr = new FileReader("a.txt");
		char[] data = new char[3<<20];
		int length;
		while((length = fr.read(data)) != -1){
			for(int i = 0; i < length; i++){
				System.out.print(data[i]);
			}
		}
		fr.close();
	}
}

3.3 BufferedReader和BufferedWriter

       给原本的字符流节点流添加变长的缓冲空间 ,从而在底层实现一次读取多个字符的功能,作为字符流,输入/出流,过滤流。 为了给原本的节点流添加变长的缓冲空间,从而提高每次读写的吞吐量进而提高效率(每次读写多个字节),这与将FileReader和FileWriter使用char数组多字节读取一样,只是不需要再去定义数组,底层自动实现数组(缓冲区)。

BufferedReader fis = new BufferedReader(Reader r, int size[可选]);
BufferedWriter fos = new BufferedWriter(Writer w, int size[可选]);

       过滤流中,构造方法只允许传入其它的流(即Reader、Writer的实现类),不能直接连接File、String类型。构造方法第二个参数都可以指定缓冲空间大小,建议定义按行读取,这里的第二个参数不需要填写,缓存空间长度是不断变化的。

//按行读进来
//BufferedReader - String readLine()
import java.io.*;
public class ExampleBufferedReader{
	public static void main(String[] args)throws Exception{
		BufferedReader br = new BufferedReader(new FileReader("a.txt"));
		String str;
		while((str = br.readLine())!=null){
			System.out.println(str);
		}
		br.close();
	}
}
//BufferedWriter - write(String) + newLine()
//此时BufferedReader一直直读到换行,才是缓冲空间的长度,缓冲空间长度是变化的
import java.io.*;
public class ExampleBufferedWriter{
	public static void main(String[] args)throws Exception{
		BufferedWriter bw = new BufferedWriter(new FileWriter("赴戍登程口占示家人 林则徐.txt"));
		bw.write("力微任重久神疲,再竭衰庸定不支。");
		//换行输入
		bw.newLine();
		bw.write("苟利国家生死以,岂因祸福避趋之?");
		bw.newLine();
		bw.write("谪居正是君恩厚,养拙刚于戍卒宜。");
		bw.newLine();
		bw.write("戏与山妻谈故事,试吟断送老头皮。");
		bw.close();
	}
}
  • Windows识别换行是\r\n,记事本中只能识别\r\n,Linux、Unix的换行为\n。使用newLine()可以跨平台适配。

3.4 最高效的字符流输入输出方式:BufferReader+PrintWriter

       主要使用的是它的println()(相当于BufferWriter的write() + newLine())方法,以行为单位写出文本文件。需要注意的是,PrinterWriter既可以作为节点流,直接传入参数列表File类型对象并指定字符集,又可以作为过滤流传入Writer的继承类对象并指定自动清空缓冲。

PrintWriter pw = new PrintWriter(Writer w, boolean isAutoFlush[可选]);
PrintWriter pw = new PrintWriter(File f, String 编码方式[可选]);
PrintWriter pw = new PrintWriter(String fileName, String 编码方式[可选]);

       与System.out.println()有些相似,println所属的out类继承于PrintStream类。

import java.io.*;
public class ExamplePrintWriter{
	public static void main(String[] args)throws Exception{
		PrintWriter pw = new PrintWriter("赴戍登程口占示家人 林则徐.txt");
		pw.println("力微任重久神疲,再竭衰庸定不支。");
		pw.println("苟利国家生死以,岂因祸福避趋之?");
		pw.println("谪居正是君恩厚,养拙刚于戍卒宜。");
		pw.println("戏与山妻谈故事,试吟断送老头皮。");
		pw.close();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值