JavaIO详细解说

IO简介

数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。

流序列中的数据可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种规定的特定数据。因此Java中的流分为两种:

  1. 字节流:数据流中最小的数据单元是字节
  2. 字符流:数据流中最小的数据单元式字符,Java中的字符是Unicode编码,一个字符占用两个字节。

Java.io包中最重要的就是5个类和一个接口。五个类指的是FileOutputStreamInputStreamWriter,Reader;一个接口指的是Serizlizable

Java IO主要包括下面3个层次

  1. 流式部分——最主要的部分。如:outputStream,InputStream,Writer,Reader等
  2. 非流式部分——如:File类,RandomAccessFile类和FileDescriptor等类
  3. 其他——文件读取部分与安全相关的类,如:SerialzablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类

image-20240301093047696

DataOutputStream out = new DataOutputStream(
	new BufferedOutputSteam(
  	new FileOutputStream(
    	new File(file)));

在上面代码中:为了向文件中写入数据,需要创建一个FileOutputtream,然后为了提高访问的效率,所以将它发送给具备缓存功能的BufferedOutputStream,而为了实现于机器类型无关的Java基本类型数据的输出,所以我们将缓存的流传递给了DataOutputStream.从上面的关系,我们可以可以看到,其根本目的都是为了outputStream添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码,因此学习流,必须要学好装饰模式。

装饰模式

装饰模式(Decorator Pattern)是一种设计模式,它允许我们在运行时动态地给对象添加新的行为或责任,而不需要修改其原有的代码。这种模式通过创建一个装饰类来包装原有的类,这个装饰类和原有的类有相同的接口,可以在不改变原有类的情况下增加新的功能。

装饰模式主要包含以下几个组成部分:

  1. 抽象组件(Component):抽象构建接口。
  2. 具体组件(Concrete Component):具体的构建对象,实现组件对象接口,通常就是被装饰的原始对象。就对这个对象添加功能。
  3. 抽象装饰类(Decorator):所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,内部持有一个Component对象,就是持有一个被装饰的对象。
  4. 具体装饰类(Concrete Decorator):实际的装饰器对象,实现具体添加功能。

image-20240301093412113

在Java的IO库中就广泛使用了装饰模式,例如BufferedReader和BufferedWriter就是装饰类,它们增加了缓冲的功能。

装饰模式的优点是可以在不修改原有类的情况下增加新的功能,增强了代码的可扩展性。同时,装饰模式可以动态地添加和撤销功能,增加了代码的灵活性。但是,装饰模式会增加系统的复杂性,增加了系统中类的数量,同时在装饰链较长时,调试可能会变得复杂。

IO中的装饰器模式

image-20240301095208826

字节流

image-20240301095251260

字节流的学习过程

为什么要按照一个学习路线来呢?原因是它们的功能决定的

OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream

FilterOutputStream

从学习的角度,我们应该先掌握FilterOutputStream,以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对abstract类OutputStream的扩展,是它的子类。然而伴随着对Stream流的功能的扩展,就出现了DataOutputStream,(将Java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成Java基础类型)。DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器设计模式、避免了类的爆炸式增长。

BufferedOutputStream

为了提高Stream的执行效率,所以出现了bufferedOutputStream。BufferedOutputStream就是将本地添加了一个缓存的数组。在使用BufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘都多少byte的数据,而出现BufferedOutputStream之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。

另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outputStream了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就只需要在使用的时候看看API即可。

字符流

下图是一个关于字符流的图谱,这张图谱比较全面的概括了我们字符流中间的各个类以及它们之间的关系。

image-20240301095328275

字符有一个 readline() ,且对于中文的区别

字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道它们的编码格式也意义不大的文件(png,exe,zip)的时候是采用字节,而面对一些我们知道文件构造就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是我们还是需要以字符的形式来获取,所以就出现了字符流。reader和Stream最大的区别就是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为只有字符才具备行的概念,相反字符流中的行也就是一个字节符号。

字符流的学习历程

Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其他

通过类比着学习Reader相关的类

FilterWriter/FilterReader

字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展,本身是一个抽象类

BufferedWriter/BufferedReader

BufferedWriter是Writer类的一个子类。它的功能是为了传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不但效率低下,也有可能会对存储介质造成一定的破坏、比如我们向磁盘不断的写入字节时、夸张一点,将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、每写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下后、我们可以从程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量时、一次性写入FileReader流中、此时FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量。

OutputStreamWriter/InputStreamReader

输入字符转换流,是输入字节流转向输入字符流的桥梁,用于将输入字节流转换成输入字符流,通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/InputStreamReader只是对StreamDecoder进行了封装。isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamREader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不关心内部实现。

OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁、它们只是外边的一个门面、真正的核心:

OutputStreamWriter中的StreamEncoder:

  1. 使用指定的或者默认的编码集将字符转换为字节
  2. 调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中

InputStreamReader中的StreamDecoder:

  1. 使用指定的或者默认的编码集将字节解释为字符
  2. 调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中

在理解这两个流的时候要注意:java——IO中只有将字符转换成字符的类、没有将字符转换成字节的类,原因很简单——字符流的存在本身就是对字节流进行了装饰、加工处理一般更方便的去使用、在使用这两个流的时候要注意:由于这两个流要频繁的对读取或者写入的字节或者字符进行转码、解码和与底层流的源和目的地进行交互、所以使用的时候要使用BufferedWriter、BufferedReader进行包装、以达到最大效率、和保护存储介质。

FileReader/FileWriter

FileReader和FileWriter继承于InputStreamReader/OutputStreamWriter。

从源码可以发现FileWriter文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话,最好使用BufferedWriter包装一下

FileReader文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字符形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题这样的话很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好,同样使用此流的时候用BufferedReader包装一下,能使用BufferedReader的readline方法,还能提高效率、保护存储介质。

  1. 看下面的代码:
 BufferedWriter bufferedWriter = new BufferedWriter(
 	new OutputStreamWriter(
   	new FileOutputStream(
     	new File("src/testtxt/writerAndStream.txt")),"GBK"));
 bufferedWriter.write("我爱你中国");
 bufferedWriter.flush();
 bufferedWriter.close();

如果只用FileOutputStream fileOutputStream = new FileOutputStream("d:/text.txt");

不是也能输入到"d:text.txt"吗?为什么要用其他两个呢?能起到什么作用?

FileOutputStream:字节流,它一个字节一个字节的向外边送数据

OutPutStreamWriter:是字符流,它一个字符一个字符的向外边送数据

  1. 它们有什么区别?

    英文字符占一个字节,而中文是一个字符,至少站两个字节。如果stream,读出来的中文可能为乱码,如果用Writer就不会有乱码了

  2. BufferedWriter Buffer 是一个缓冲区,为什么要用Buffer?

    如果直接用Stream或者Writer,你的硬盘可能就是度一个字符或者一个字节,就去读写硬盘一次,IO负载巨大。可是用了Buffer,硬盘就是读了一堆数据之后,读写一下硬盘,对硬盘有好处。

字符流最常见用法

在Java中,字节流能转换为字符流,下面是它们的转换关系图

image-20240301112602957

image-20240301112632440

字节流和字符流的区别

字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢?

字节流在操作的时候是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在close的情况下输出内容

开发中使用字节流还是字符流?

在所有的硬盘上保存文件或进行传输的时候都是可以使用字节流的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。

如果要Java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)

字节流和字符流的转换

虽然Java支持字节流和字符流,但又是需要在字节流和字符流两者之间转换。InputStreamReader和OutputStreamWriter,这两个是为字节流和字符流之间相互转换的类。

InputStreamReader用于将一个字节流的字节解码成字符

有两个构造方法:

  • InputStreamReader(InputStream in);

    **功能:**用默认字符集创建一个InputStreamReader对象

  • InputStreamReader(InputStream in, String CharsetName);

    **功能:**接收已指定字符集名的字符串,并用该字符创建对象

OutputStream用于将写入的字符编码成字节后写入一个字节流

同样有两个构造方法:

  • OutputStreamWriter(outputStream out);

    **功能:**用默认字符集创建一个OutputStreamWriter对象;

  • OutputStreamWriter(OutputStream out, String CharSetName);

    **功能:**接收已指定字符集名的字符串,并用该字符集创建OutputStreamWriter对象

为了避免繁琐的转换字节流和字符流,对以上两个类进行了疯转。

BufferedWriter类封装了OutputStreamWriter类;

BufferedReader类封装了InputStreamReader类;

封装格式:

BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in = new BufferedReader(new InputStreamReader(Systerm.in));

利用下面的语句,可以从控制态读取一行字符串:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line = in.readLine();

RandomAccessFile

为什么要有RandomAccessFile?

常用方法
  • 构造方法:RandomAccessFile raf = newRandomAccessFile(File file, String mode);

​ 其中参数mode的值可选"r":可读,“w”:可写,“rw”:可读写

  • 成员方法
    • seek(int index)可以将指针移动到某个位置开始读写;
    • setLength(long len)给写入控件预留空间
优点
  1. 既可以读也可以写

    RandomAccessFile不属于InputStream和OutputStream类系的,它是一个完全独立的类,所有方法(绝大数都只属于它自己)都是自己从头开始规定的,这里面包含读写两种操作。

  2. 可以指定位置读写

    RandomAccessFile能在文件里面前后移动,在文件里移动用的seek(),所以它的行为与其他的IO类有些根本上的不同。总而言之,它是一个直接继承于Object的,独立的类。只有RandomAccessFile才有seek搜寻方法,而这个方法也只适用于文件。

NIO——FileChannel

Channel是对I/O操作的封装

FileChannel配合着ButeBuffer,将读写的数据缓存到内存中,然后以批量/缓存的方式read/write,省去了非批量操作时的重复中间操作,操纵大文件时可以显著提高效率(和Stream以Bytes数组方式有什么区别?经过测试,效率上几乎无区别)。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值