JAVA-IO(二)
sschrodinger
2019/05/21
引用
JAVA IO 框架
Java IO 框架位于 java.io
包中。
从读和写来看 IO 系统总共分为输出流和输入流:
- 输出流:
OutputStream
和Writer
- 输入流:
InputStream
和Reader
从流处理的类型来看,系统可以分为字符流和字节流:
- 字符流:
Reader
和Writer
- 字节流:
InputStream
和OutputStream
我们在使用时,一般使用上述类的子类来实现自己的功能。
设计模式
Java IO 包是典型的装饰器模式设计(包装模式)。
包装模式
通过使用修饰模式,可以在运行时扩充一个类的功能。原理是:增加一个修饰类包裹原来的类,包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能,但是,在不需要用到新功能的地方,它可以直接调用原来的类中的方法。修饰类必须和原来的类有相同的接口。
UML 类图如下:
image使用装饰器模式可以大量的减少子类的个数。
有如下需求:
我们在打印一串字符串之前,可能有去重、去掉某一特定字符串、在字符串前加入特定操作等操作。考虑到程序不会执行所有的操作,如部分字符串只去重、部分只添加特定字符、某些可能三种操作都有,那么,我们就需要继承 $C_3^1 + C_3^2 + C_3^3$
个类,造成了子类膨胀。
使用装饰器模式就可以很好的避免子类膨胀,装饰器模式如下:
// 基础类
public class Component {
public void printString(String string) {
System.out.println(string);
}
}
// 增加去重的功能
public class DropDuplicate extends Component {
private Component component;
public DropDuplicate(Component component) {
this.component = component;
}
public void printString(String string) {
string = string.dropDuplicate();
component.printString(string);
}
}
// 增加去掉 a 字符的功能
public class DropSpecial extends Component {
private Component component;
public DropSpecial(Component component) {
this.component = component;
}
public void printString(String string) {
string = string.dropSpecial("a");
component.printString(string);
}
}
// 增加特定字符的功能
public class AddSpecial extends Component {
private Component component;
public AddSpecial(Component component) {
this.component = component;
}
public void printString(String string) {
string = string.add("hello");
component.printString(string);
}
}
这时,我们想要什么功能就可以对装饰器进行组合,如下:
// 获得一个过滤重复元素和添加特定前缀的打印
public void static main(String[] args) {
String arg = "xxxxxxiiiieyyyyy";
Component component = new AddSpecial(new DropDuplicate(new Component()));
component.printString(arg);
}
Java IO 利用装饰器模式实现了大量的子类,增加了大量的功能,增加的功能大致如下:
- 文件访问
- 网络访问
- 内存缓存访问
- 线程内部通信(管道)
- 缓冲
- 过滤
- 解析
- 读写文本 (Readers / Writers)
- 读写基本类型数据 (long, int etc.)
- 读写对象
如下显示了 Java IO 框架的大致类关系。
Java IO 框架字节流和字符流可以相互的转化,使用 InputStreamReader 实现。
note
- IO 框架完全是同步阻塞的,在性能上带来了很大的损失。
JAVA NIO 框架
JAVA NIO 框架的复杂度远远大于 IO 框架,不仅仅包含对输入输出的处理,还包括了对文件的处理。
下图展示了 NIO 包所包含的子包和作用:
1. java.nio >>>>>>>>>>>>>>>>>> 主要定义了 Buffer,一个存储基本元素的类
2. java.nio.channels >>>>>>>>> 主要定义了 Channel,用于 IO 之间的数据传输,和 Selector, 用于实现非阻塞的 IO 多路复用协议
3. java.nio.channels.spi >>>>> channels 包的实现类,不需要管
4. java.nio.charset >>>>>>>>>> 定义了编码用包
5. java.nio.charset.spi >>>>>> 编码用包的实现类
6. java.nio.file >>>>>>>>>>>>> 对文件的操作
7. java.nio.file.attribute >>> 对文件属性的操作
8. java.nio.file.spi >>>>>>>>> 文件操作的实现类
在实际使用中,最主要的是使用 1, 2, 4, 6, 7
五个包中的类完成任务。
Java.nio 包
Java.nio
包主要定义了 Buffer
类及其子类,该类主要用来存储基本类型。
常见的子类如下:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
基本父类 Buffer
使用 4 个变量来表示当前位置:
- mark:标识的起点位置
- position:下一个读取元素的位置
- limit:代表第一个不可读写的元素所在的位置
- capacity:该类初始化时确认,代表可以存储多少元素,不可改变
显然,一定有如下关系式成立:
0 <= mark <= position <= limit <= capacity
下图展示了读和写时的以上四个变量的区别:
imageget/put 方法
作为 一个容器,对于 Buffer 的每一个子类都实现了 get 和 put 方法。
以 ByteBuffer 为例,下面的代码展示了 ByteBuffer 的所有 get 和 put 方法。
1. public abstract byte get();
2. public abstract byte get(int index);
3. public ByteBuffer get(byte[] dst, int offset, int length);
4. public ByteBuffer get(byte[] dst);
5. public abstract ByteBuffer put(byte b);
6. public abstract ByteBuffer put(int index, byte b);
7. public ByteBuffer put(ByteBuffer src);
8. public ByteBuffer put(byte[] src, int offset, int length);
9. public final ByteBuffer put(byte[] src);
上面的两种方法,分为相对读写和绝对读写。相对读写不需要显式指定 index,相对读写的起点是当前 Position,每当读取一个字节或者写入 X 个字节, position 就自动增加 x;绝对读写从都开始对 index 进行计数访问,类似于数组的访问。
mark/reset 方法
mark()
用于建立 position 的快照,即记录当前 position 的位置,用 mark
变量记录。
reset()
用于将 position 重新指回 mark
变量所在的位置。
Clearing, flipping, and rewinding
clear()
函数将 Buffer
标记为准备好写或者相对写(put):limit => capacity; position => 0;
flip()
函数将 Buffer
标记为准备好读或者相对读(get):limit => current position; position => 0;
rewind()
函数将 Buffer
标记为准备好重新读:limit => limit; position => 0;
Read-only Buffer
所有的 Buffer
都是可读的,但是不是所有的 Buffer
都可以写,如果对一个只读缓存调用 put 方法,会抛出 ReadOnlyBufferException
异常。
线程安全性
Buffer 是非线程安全的,需要自己做线程安全处理。
设计模式
Buffer 有多个直接子类,如下:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
但是具体的实现逻辑仍然不由这些直接子类实现,观察源码,我们发现,以上的 7 个子类都是抽象类,具体的实现由其子类实现。
以 ByteBuffer
为例,一般使用如下的两种方式获得实例:
Buffer buffer = Buffer.allocate(1024);
Buffer buffer = Buffer.allocateDirect(1024);
观察源码,我们可以发现,两种方式都是返回的 ByteBuffer 的子类:
// 直接在内存上分配空间 (Unsafe 类)
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// charsequence
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
这样的好处是可以实现大量的类实现不同的功能,通过 Buffer 的直接子类作为入口,实现基于内存的、可读的、可写的等各种 Buffer。
java.nio.channels 包
Channel
类是 NIO 的核心。
可以把 Channel
想成全双工通信的 stream,可以设置成非阻塞模式,效率更高。
目前主要实现了四大 Channel:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
每一种 channel 都有它自己的特性:
FileChannel
- 继承自 SeekableByteChannel:包含了一个当前修改位置的指针,文件本身包含一个可变长度的字节序列,可以读写,并且可以查询其当前大小。当写入的字节超过当前大小时,文件的大小会增加;当文件被截断时,文件的大小会减小。该文件还可能具有一些相关联的元数据,例如访问权限、内容类型和上次修改时间;此类不定义元数据访问的方法。
- 支持绝对读写:字节可以在文件中的绝对位置以不影响通道当前位置的方式进行读取或写入。
- 支持内存文件映射:文件的一个区域可以直接映射到内存中;对于大型文件,这通常比调用通常的读或写方法要高效得多。
- 支持区域文件内存映射:文件的一个区域可以直接映射到内存中;对于大型文件,这通常比调用通常的读或写方法要高效得多。
- 支持通道转换:字节可以从一个文件传输到其他通道,反之亦然,这种方式可以被许多操作系统优化成直接到文件系统缓存或从文件系统缓存直接传输到或从文件系统缓存进行非常快速的传输。
- 支持部分上锁:文件的一个区域可能会被锁定,以防止其他程序访问。
- 线程安全的类
SocketChannel
- 面向流的支持 select 的通道
- 支持非阻塞连接
- 支持异步关闭
- 多线程安全
ServerSocketChannel
- 面向流的支持 select 的通道
- 支持非阻塞连接
- 支持异步关闭
- 多线程安全
DatagramChannel
- 面向数据报的支持 select 的通道
Selector
类是 NIO 实现 IO 多路复用的核心。
在下一节会具体讲该类
java.nio.file
该包提供了一套新的方式对文件进行读取,包括遍历文件夹,修改文件,查看文件信息等,用来替代传统的 File 类,更加的方便灵活。
网友评论