对InputStream,OutputStream,Reader,Writer的详解

1、IO流介绍

基本介绍:常使用的File类是用来描述文件或者文件夹的。File对象可以帮我们获取文件或者文件夹的一些属性数据,但是无法读取文件里面的数据,如果想读取文件的数据,那么需要IO流技术。
IO流技术:解决设备与设备之间数据的传输。如:硬盘---->内存、内存------->硬盘

1.1、IO流技术分类

IO流技术按照不同的功能或者需要可以分成不同的类别。
一、按照数据的流向分:输入流与输出流;
二、按照数据单位划分:字节流与字符流;
字节流的简介:字节流读取的数据都是文件中存储的二进制数据,并且读取的二进制数据不经过任何的加工处理;
字符流的简介:字符流读取的也是文件中的二进制数据,只不过读取的二进制数据按照使用的编码表解码成我们能识别的字符数据。所以可以说:字符流 = 字节流+解码

1.2、输入输出的判断

输入输出的判断,我们可以使用程序本身来当参照物
输入流:从文件中读取数据到程序中。以程序为参照物,数据流入程序内;
输出流:将程序中的数据写到文件中。以程序为参照物,数据流出程序;
下面上图来总结一下:
在这里插入图片描述

2、字节流

2.1、字节流分类

字节流按照数据的流向分为:输入字节流,输出字节流;

2.1.1、输入字节流(InputStream)

InputStream:是一个抽象类(不能实例化),是所有输入输入字节流的超类。它的子类有:ByteArrayInputStream, FileInputStream,ObjectInputStream, SequenceInputStream…这些不同的子类读取的对象不一样的。选着什么样的子类来操作目标对象完全是顾名思义的。举个例子:ByteArrayInputStream(字节数组输入流)是操作字节数组的、ObjectInputStream(对象的输入字节流)用来操作对象的。
接下来,主要以FileInputStream这个类的学习为例,其他的输入字节流的使用都是类似的。

2.1.1.1、文件输入字节流(FileInputStream)

首先看代码,FileInputStream是继承InputStream的。

public class FileInputStream extends InputStream

FileInputStream从文件系统中的某个文件中获取输入字节,至于具体是什么文件,取决于主机环境。用于读取图像之类的数据的原始字节流。
FileInputStream中使用一个read()方法来读取文件中的字节数据,返回一个0~255范围内的int数据,读取的时候是一个字节一个字节的读取。如果读取已经达到文件末尾,那个read方法返回-1
FileInputStream的使用步骤:
1、找到目标文件;
2、建立传输管道;
3、读取目标数据;
4、关闭传输管道;

2.1.1.2、读取文件方式一

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData01();
	}
	
   /**
	 * 缺点:只能读取一个字节的数据,不能读取完文件数据
	 * 	read():从此输入流中读取下一个数据字节。返回一个 0 到 255 范围内的 int 字节值。
	 * 	                  如果因为已经到达流末尾而没有字节可用,则返回 -1
	 * @throws IOException
	 */
	public static void getFileData01() throws IOException {
	   //1、找到目标文件;
		File file = new File("F:\\Web_project\\io_demo.txt");
		//2、建立传输管道;
		FileInputStream fileInputStream = new FileInputStream(file);
		// 3、读取目标数据;
		int content = fileInputStream.read();
		System.out.println("得到的内容是----》"+(char)content);
		// 关闭释放资源
		fileInputStream.close();
	}
}

这是是非常基础的,只返回一个字节的数据。
read()会抛出一个IO异常,是因为怕读取数据过程中传输中断;

int content = fileInputStream.read();

这是句代码是将读取到的字节数据返回到content当中,再经过强转content就可以了;
这个方法有一个致命的缺点:只能读取一个字节,不能读全文件,所以我们来看下一个方法;

2.1.1.3、读取文件方式二

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData02();
	}
	/**
	 * 改良后的方法一,可以读全文件数据,但是在循环中,如果文件过大,效率较低
	 * @throws IOException
	 */
	public static void getFileData02() throws IOException {
		File file = new File("F:\\Web_project\\io_demo.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		int content = 0;
		while ((content = fileInputStream.read()) != -1) {
			System.out.print((char)content);
		}
		// 关闭释放资源
		fileInputStream.close();
	}
}

方式二跟方式一样的地方:

        int content = 0;
		while ((content = fileInputStream.read()) != -1) {
			System.out.print((char)content);
		}

这一片代码,是将读取语句放入循环中读取,并将读取的值赋给content,进而判断content是否为-1来判断是否文件读取完;
read()方法的读取方式,和迭代器有些相识,也是获取一个相应的指针移到下一个元素,读取到末尾时,返回一个标识符;
接下来我们来看看带有缓冲数组的的读取,方式三。

2.1.1.4、读取文件方式三

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData03();
	}
	public static void getFileData03() throws IOException {
		// 1、找到目标文件;
		File file = new File("F:\\Web_project\\io_demo.txt");
		// 2、建立传输管道;
		FileInputStream fileInputStream = new FileInputStream(file);
		// 定义一个字节数组
		byte[] buff = new byte[10];
		// 3、读取目标数据;
		int length = fileInputStream.read(buff);
		System.out.println("读取到的文件内容---->"+new String(buff));
		System.out.println("读取到的长度----->"+length);
		// 4、关闭传输管道;
		fileInputStream.close();
	}
}

然后我们来聊一下这种方式的读取数据:
首先建立了一个缓冲字节数组byte[] buff = new byte[10];长度为10。再将这个数组作为实参放入read()方法当中。这样read()读取的数据就存放在buff数组里。

int length = fileInputStream.read(buff);

这里的返回值length为读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。 也就是lengthread往数组里存放多少个二进制数据,当然长度不会超过数组本身的长度。
读取到的buf二进制数据经过new String(buf),由String本身的解码功能,解码成我们看得懂的字符串;
这种方式同样有致命的缺陷:不能读全文件,因为这个方法能读取文件的多少,是有缓冲数组的长度来决定的,总不能声明一个非常非常大的数组在那里占用空间吧。那么我们来看看,方式四。

2.1.1.5、读取文件方式四

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData04();
	}
    public static void getFileData04() throws IOException {
		File file = new File("F:\\Web_project\\io_demo.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		byte[] buff = new byte[1024];
		int length = 0;
		while ((length = fileInputStream.read(buff))!= -1) {
			System.out.println("解析出的文件内容--->"+new String(buff,0,length));
		}
		fileInputStream.close();
	}

方式四不用说也是方式三的升级版:

        int length = 0;
        byte[] buff = new byte[1024];
		while ((length = fileInputStream.read(buff))!= -1) {
			System.out.println("解析出的文件内容--->"+new String(buff,0,length));
		}

也是将读取数据的代码放入到了循环,如果read(buf)到达文件末尾并且没有文件可读,返回-1
这里要注意的点:

new String(buff,0,length)

一般都是这样子写的。至于原因嘛
是因为read方法将读取的二进制数据放入缓冲数组,当缓冲数组存储满的时候,read方法返回读入缓冲区的字节总数length。进行第二次读取的时候要注意了,read()方法并不是清空之前buf缓冲数组里的数据,而是覆盖缓冲数组里的数据,也就意味着,如果下一次读取时,剩余的文件数据小于缓冲数组长度,那么在缓冲数组的倒数的几个位置会存在上一次读取的文件数据。

2.1.1.6、总结

我们来总结一下,只有方式二和方式四能读全文件,
要点一:方式四的效率是要远大于方式二的
举个例子:好像去井里打水,一个是拿到一瓢水就跑回家,将这一瓢水倒在水缸里后再返回往井里打水。而另一个是将一瓢一瓢的水倒在水桶里,等水桶装满了,才拎着水桶倒家里再返回往井里打水。这当然是用水桶的效率高。
要点二:read()方法使用缓冲数组时,也是一个字节一个字节的读取的,只不过读取的数据是先存放在缓冲数组里而已。
要点三:read方法再次向缓冲数组里读入数据时,并不是先清空缓冲数组里的数据,而是覆盖。这也正是为什么read要返回:读入缓冲区的字节总数

2.1.2、输出字符流(OutputStream)

OutputStream:抽象类是表示输出字节流的所有类的超类,它也有很多的子类ByteArrayOutputStream, FileOutputStream, ObjectOutputStream,也同样了解FileOutputStream的使用,其他子类的使用方式也是差不多的。

2.1.2.1、文件输出字符流(FileOutputStream)

看代码,是继承了OutputStream,是OutputStream的亲儿子。

public class FileOutputStream extends OutputStream

FileOutputStream:文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流, 用于写入诸如图像数据之类的原始字节的流。
FileOutputStream的使用步骤与FileInputStream一样:
1、找到目标文件;
2、建立传输管道;
3、读取目标数据;
4、关闭传输管道;

2.1.2.2、写数据方式一

public class OutStream {
	public static void main(String[] args) throws IOException {
		writeInfo01();
	}
	public static void writeInfo01() throws IOException {
		File file = new File("F:\\Web_project\\out_demo.txt");
		FileOutputStream fileOutputStream = new FileOutputStream(file);
		fileOutputStream.write(97);
		// 释放资源
		fileOutputStream.close();
	}

这方法只写一个int类型的数据97(也就是a)
好了我们看看加了缓冲数组的方式二

2.1.2.3、写数据方式二

public class OutStream {
	public static void main(String[] args) throws IOException {
		writeInfo03();
	}
	public static void writeInfo03() throws IOException {
		File file = new File("F:\\Web_project\\out_demo.txt");
		FileOutputStream fileOutputStream = new FileOutputStream(file,true);
		String msg = "hello word ";
		byte[] buf = msg.getBytes();
		fileOutputStream.write(buf);
		fileOutputStream.close();
	}

看看一片代码:

        String msg = "hello word ";
		byte[] buf = msg.getBytes();
		fileOutputStream.write(buf);

将String的msg转成字节数组,再将字节数组作为实参传给write方法。当然write也是一个字节一个字节的写。

3、字节流的异常处理

上面的所有例子都是直接抛的异常,但这并不太是正确的姿势,下面我上一份拷贝文件的代码:

public class CopyPictureExection {
	public static void main(String[] args) {
		copyPic();
	}
	
	public static void copyPic() {
		FileInputStream fileInputStream = null;
		FileOutputStream fileOutputStream = null;
		try {
			File inFile = new File("F:\\Web_project\\info.txt");
			File outFile = new File("F:\\Web_project\\Bagua\\记事本.txt");
			fileInputStream = new FileInputStream(inFile);
			fileOutputStream = new FileOutputStream(outFile);
			byte[] buf = new byte[1024];
			int length = 0;
			while ((length = fileInputStream.read(buf)) != -1) {
				fileOutputStream.write(buf, 0, length);
			}
		}catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				// 先声明的后关闭,后声明的先关闭
				if (fileOutputStream != null) {
					fileOutputStream.close();
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}finally {
				try {
					if (fileInputStream != null) {
						fileInputStream.close();
					}
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		}
	}
}

代码上完了,接下来分别讲一些要点:
要点一:关闭流时遵循先声明的后关闭,后声明的先关闭

			fileInputStream = new FileInputStream(inFile);
			fileOutputStream = new FileOutputStream(outFile);

分别声明了一个输入流,一个输出流。由于先声明的是fileInputStream输入流,所以关闭流的时候先关闭输,再关闭输出流;
要点二:流使用完是必须得关闭的
如果不关闭的话,将一直占用某个资源,是其他代码无法操作那个资源。

4、编码表的内容

使用字节流来读取中文的时候会出现乱码,这是因为一个中文是两个字节,而字节流读取数据这是一个字节一个字节进行的。也就是说只能读取中文的一般,显现出来的就是乱码了。

4.1、码表

计算机中是只存储二进制的,但是我们看到的却是各种认识的文字,是因为将这些文字一一的使用二进制来表示,这些一一对应所形成的一张表,成为码表。各个国家的语言不太一样,所以码表也不一样。
比如美国的码表为:ASCII,一个英文用一个字节表示。中国的GBK,一个中文用两个字节表示;
一般来说英文是比较通用的,因为在各种不同的码表中,英文都是使用一个字节表示的;
常见的码表
ASCII: 美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1: 拉丁码表。欧洲码表,用一个字节的8位表示。又称Latin-1(拉丁编码)或“西欧语言”。ASCII码是包含的仅仅是英文字母,并且没有完全占满256个编码位置,所以它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,
藉以供使用变音符号的拉丁字母语言使用。从而支持德文,法文等。因而它依然是一个单字节编码,只是比ASCII更全面。
GBK: 中国的中文编码表升级,融合了更多的中文文字符号。
UTF-8: 万国码表,国际性码表,融合了各个国家的码表,英文还是一个字节,最多用三个字节来表示一个中文字符。
UTF-16: 不管英文中文都是占两个字节。

4.2、编码

字符串—>字节数组
String类的getBytes() 方法进行编码,将字符串,转为对映的二进制,并且这个方法可以指定编码表。如果没有指定码表,该方法会使用操作系统默认码表。
注意:中国大陆的Windows系统上默认的编码一般为GBK。在Java程序中可以使用System.getProperty(“file.encoding”)方式得到当前的默认编码。

4.3、解码

字节数组—>字符串
String类的构造函数完成。
String(byte[] bytes) 使用系统默认码表
String(byte[],charset)指定码表
注意:我们使用什么字符集(码表)进行编码,就应该使用什么字符集进行解码,否则很有可能出现乱码(兼容字符集不会)。

5、字符流

下面来看看字符流。我们知道用字节流读取中文会出现乱码,字符流就正好可以用来读取文本的数据。之所以字符流读取不会出现乱码,是因为字符流读取是一个字符一个字符的读取的。
可以完全操作字节流的经验来了解字符流

5.1、字符流分类

字符流可以分为输入字符流和输出字符流。

5.2、输入字符流(Reader)

Reader是输入字符流的超类,其子类有:BufferedReader, CharArrayReader…
这里以FileReader为例:

public class ReaderStream {

	public static void main(String[] args) {
		readMsg();
	}
	
	public static void readMsg() {
		FileReader fileReader = null;
		try {
			File file = new File("F:\\newspace\\IoExectionDeal.java");
			fileReader = new FileReader(file);
			int length = 0;
			char[] buf = new char[9];
			while ((length = fileReader.read(buf)) != -1) {
				System.out.print(new String(buf,0,length));
			}
		}catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				if (fileReader != null) {
					fileReader.close();
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
}

之前讲了字节流无论是读写文件数据都是一个字节一个字节的读取,也就是这样才导致读取的中文有乱码存在。而字符流是一个字符一个字符的读取数据。除此之外,用法也都差不多的,这也是流技术容易学习的地方。

			char[] buf = new char[9];
			while ((length = fileReader.read(buf)) != -1) {
				System.out.print(new String(buf,0,length));
			}

只不过这里的缓冲数组不再是字节数组,而是字符数组。while循环结构和字节流是一样的。

5.3、输出字符流(Writer)

Writer是所有输出字符流的超,这里也以为例:

public class WriterStream {
	public static void main(String[] args) {
		writeMsg();
	}
	
	public static void writeMsg() {
		FileWriter fileWriter = null;
		try {
			File file = new File("F:\\Web_project\\out_demo.txt");
			fileWriter = new FileWriter(file);
			String msg = "大家好啊!";
			fileWriter.write(msg);
			fileWriter.flush();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				if (fileWriter != null) {
					fileWriter.close();
					System.out.println("输出字符流关闭成功...");
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}
}

用法也和输出字节流一样,稍不一样的是,这里是可以直接写字符串的;
来看一下之前的输出字节流的用法:

		String msg = "hello word ";
		byte[] buf = msg.getBytes();

需要将字符串通过msg.getBytes(),getBytes方法编码成字节数据后才能写入数据;
再看输出字符流:

			String msg = "大家好啊!";
			fileWriter.write(msg);

嗯,,,直接写的字符串。
嗯,,,由于篇幅的关系,关于流的部分就愉快的先到这里吧。
下一篇继续接受一下流的一点点延伸的部分:缓冲字节流,以及缓冲字符流,还有序列(SequenceInputStream)

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值