JavaSE-Adventure(V): Java I/O
CONTENTS
Java I/O中常用的类
在整个Java.io包中最重要的就是5个类和1个接口。
- 5个类指的是
File
、OutputStream
、InputStream
、Writer
、Reader
- 1个接口指的是
Serializable
。
Java I/O主要包括如下几个层次,包含三个部分:
-
流式部分
IO的主体部分。 -
非流式部分
主要包含一些辅助流式部分的类,如:File
、RandomAccessFile
、FileDescriptor
等。 -
其他类
文件读取部分的与安全相关的类,如:SerializablePermission
,以及与本地操作系统相关的文件系统的类,如:FileSystem
、Win32FileSystem
、WinNTFileSystem
类。
- File (文件特征与管理)
-
File
将文件系统中的文件和文件夹封装成了对象。 -
File
提供了更多的属性和行为可以对这些文件和文件夹进行操作。这些是流对象办不到的,因为流只操作数据。 -
File
只是文件的抽象,只是在内存中的对象,和磁盘的文件没有联系。
-
InputStream(二进制格式操作)
抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。 -
OutputStream(二进制格式操作)
抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。 -
Reader(文件格式操作)
抽象类,基于字符的输入操作。 -
Writer(文件格式操作)
抽象类,基于字符的输出操作。 -
RandomAccessFile(随机文件操作)
一个独立的类,直接继承至Object.它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
流的概念和作用
流: 代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。
流的本质: 数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流的作用: 为数据源和目的地建立一个输送通道。
Java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流.
IO流的分类
-
根据数据处理的不同类型分为:字节流和字符流
-
根据数据流向不同分为:输入流和输出流
-
按数据来源(去向)分类:
File: FileInputStream / FileOutputStream, FileReader / FileWriter
byte[]: ByteArrayInputStream / ByteArrayOutputStream
Char[]: CharArrayReader / CharArrayWriter
String: StringReader / StringWriter
网络数据流:InputStream / OutputStream, Reader / Writer
字符流和字节流
字符流的由来:因为数据编码的不同,而有了对字符进行高效操作的流对象,本质上其实就是对于字节流的读取时,去查了指定的码表。
字节流和字符流的区别:
- 读写单位的不同:字节流以字节(8bit)为单位。字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
- 处理对象不同:字节流可以处理任何类型的数据,如图片、avi等,而字符流只能处理字符类型的数据。
输入流和输出流
对于输入流只能进行读操作。对于输出流只能进行写操作。
程序中需要对于传输数据的不同特性而使用不用的流。
装饰器模式 (Decorator Pattern)
基本概念
目的
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。
如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。
解决的问题
我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
模式结构
-
抽象构件(Component)
可以是一个接口或者抽象类,充当被装饰类的原始对象,规定了被装饰类的行为。 -
具体构件(ConcreteComponent)
实现/继承 Component的一个具体对象,即被装饰对象。通过装饰角色为其添加一些职责。 -
抽象装饰器(Decorator)
实现/继承 Component。通用的 ConcreteComponent的装饰列,其内部必然有一个属性指向 Component,主要是可以通过其子类扩展具体组件的功能。 -
具体装饰器(ConcreteDecorator)
Decorator的具体实现类,并给具体构件对象添加附加的责任,一般为其特有的功能。
使用场景
- 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。
- 当对象的功能要求可以动态地添加,也可以再动态地被撤销时。
- 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
在框架源码中使用也很广泛,比如:
- JDK中的处理流 IO中包装流类等
- Spring 中处理事务缓存的 TransactionAwareCacheDecorator类
- Mybatis 中处理缓存设计的 Cache类等
优缺点
主要优点:
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
- 装饰器模式完全遵守开闭原则
主要缺点:
会出现更多的代码、更多的类,增加程序的复杂性。
装饰器模式与代理模式
-
装饰器模式强调自身功能的扩展,是代理模式的一个特殊应用。
-
代理模式强调对代理过程的控制。
通用实现
抽象构件
interface Component {
public void operation();
}
具体构件
class ConcreteComponent implements Component {
public ConcreteComponent() {
System.out.println("创建具体构件角色");
}
// 相应的功能处理
@Override
public void operation() {
System.out.println("调用具体构件角色的方法operation()");
}
}
抽象装饰器
class Decorator implements Component {
// 持有的组件对象
private Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
// 处理前后可以加一些附加功能
component.operation();
}
}
具体装饰器
class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}
@Override
public void operation() {
// 在处理父类的方法时,可以在处理前后可以加一些附加功能
// 如果不调用父类的方法,表示完全改写方法,实现新功能
super.operation();
addedFunction();
}
public void addedFunction() {
System.out.println("为具体构件角色增加额外的功能addedFunction()");
}
}
public class DecoratorPattern {
public static void main(String[] args) {
Component p = new ConcreteComponent();
p.operation();
System.out.println("---------------装饰之后------------------");
Component d = new ConcreteDecorator(p);
d.operation();
}
}
Java I/O 中的装饰器模式
示例:
InputStream in = new BufferedInputStream(new FileInputStream(new File("/test.txt")));
Component:InputStream
public abstract class InputStream implements Closeable {
public int read(byte b[], int off, int len) throws IOException {
//...
}
}
Decorator:FilterInputStream
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
}
Concreate Component:FileInputStream
public class FileInputStream extends InputStream {
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
}
Concreate Decorator:BufferedInputStream
public class BufferedInputStream extends FilterInputStream {
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
private void fill() throws IOException {
// ...
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
private InputStream getInIfOpen() throws IOException {
InputStream input = in;
if (input == null)
throw new IOException("Stream closed");
return input;
}
}
Java I/O 继承结构
网上大部分文章基本上都是从字节/字符流来描述JavaIO中的继承结构,下面将从装饰器模式的视角来解读一下,我也是从这个角度最终理解Java I/O结构的。
磁盘I/O的工作机制
因为磁盘设备是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作,读写分别对应read()和write()两个系统调用。系统调用就存在内核空间地址和用户空间地址切换的问题。以下是磁盘IO几种访问文件的方式:
标准访问文件方式
read() -> 内核高速缓存 -> 没有则读取磁盘,然后缓存在系统中
write() -> 内核高速缓存 -> 对用户来说写操作已经完成,至于什么时候写到磁盘由操作系统决定,除非显式调用sync
直接I/O方式
直接I/O就是应用程序直接访问磁盘数据,不经多内核数据缓冲区,目的是减少一次从内核缓冲区到用户程序缓存的数据复制。但是每次都读磁盘,非常缓慢。通常直接I/O与异步I/O结合使用,会得到比较好的性能
同步访问文件方式
读和写都是同步操作,与标准访问不同的是,只有当数据被成功写到磁盘时,才返回给应用程序成功标志
异步访问文件方式
异步访问就是发出请求后,线程会接着去处理其他事情,而不是阻塞等待,当请求数据返回后继续处理下面的操作。
内存映射方式
内存映射方式指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中一段数据时,转换为访问文件的某一段数据。
参考:
JAVA IO简单介绍:https://zhuanlan.zhihu.com/p/265777334
JAVA IO常用操作总结:https://blog.csdn.net/qq_37765808/article/details/109300668
BufferedReader对比FileReader到底哪里进行了优化:https://www.jianshu.com/p/cd86cf6ee25f
结构型设计模式之装饰器模式(Decorator):https://blog.csdn.net/qq_28204635/article/details/124727351