Java I/O学习笔记
一. Java I/O的几个基本概念
1. “流”的概念:Java I/O在设计的过程中,最核心的概念就是“流”(Stream)。所谓的“流”是完全可以被比拟成生活中的自然现象,比如水流。在获取某个数据的过程中,数据的来源相当于水源;数据被一个具体的类读取时,就相当于水源被从水管中引导到我们所需要的地方,处理数据的类就相当于水管。当多个类套接起来处理该数据源的数据时,就相当于多个水管连接在一起,套接的类增加了处理数据的方式,连接的水管可以改变水流的方向,这是一个道理。
2. input和output:
在Java I/O中,接口和类被分成input和output两大块。无论是以字节形式处理还是以字符的形式处理,按照输入还是输出就能快速找到想要的处理类。
3. 装饰模式:Java I/O大量采用装饰模式。所谓装饰模式也就是在概念1中提到的将多个类套接起来处理数据源流出的数据。
比如:DecoratorB decoratorB = new DecoratorB(new DecoratorA(object));
DecoratorB接装饰了DecoratorA,在提供和DecoratorA具有相同名称但是功能更多的方法以外,DecoratorB也可以增加自己的方法来丰富对object的操作。在学习I/O的过程中会逐渐明白装饰模式的使用,只要真正理解装饰模式,I/O的操作其实就是选择合适类来进行组合的问题了。
二. Java I/O包的基本结构
Java I/O包可以分为三部分。
1. 流式部分:在流式部分,又可以分为字节流和字符流。字节流包含了InputStream,OutputStream,FilterInputStream,FilterOutputStream四块,字符流包含了Reader和Writer两块(具体结构参见后面的字节流和字符流部分)。
2.非流式部分:主要是一些I/O处理所需要的类,最常用的就是File类和RandomAccessFile类。
3.其他:比如用于序列化的Serializable接口等等。
三. 字节流
所谓字节流,意思就是说以字节为单位来从数据源处读取数据,并向输出端按字节方式输出。
输入端:
处理字节流读取的类都继承了InputStream抽象类,比如FileInputStream用来读取文件中的字节。
用法是 InputStream input = new FileInputStream(new File("xxx"));
如果将上述语句中FileInputStream替换成其他继承InputStream的类,我们就可以读取来自其他数据源的字节数据。
当我们想在读出字节的时候提供更多的操作时,比如我们想从FileInputStream里面读出以二进制形式所存储的整数(注意二进制所存储的整数和以字符表示整数是不一样的概念。比如'20',在二进制存储时占4个字符,在ASCII编码字符表示是占两个字符),那么就会用到DataInputStream类来装饰。
用法是 InputStream input new DataInputStream(new FileInputStream(new File("xxx"))); 然后再调用DataInputStream的readInt()方法。
所有的装饰类都继承自FilterInputStream类,而FilterInputStream是InputStream的子类。
综上所述,一个完整的字节流输入可以按如下方法读取:
InputStream input = new InputStreamDecoratorClass(new InputStreamSubClass(resource object));
字节输入端的常用类包结构如下:
InputStream
----FileInputStream 读取文件
----ByteArrayInputStream 读取JVM运行内存中的byte数组,免除存储和删除的操作
----SequenceInputStream 合并多个InputStream流
----PipedInputStream 线程间通讯用
----ObjectInputStream 序列化对象读取
----FilterInputStream InputStream所有装饰类的父类
----DataInputStream 可以读取各种Java的primitive type
----BufferedInputStream用来以缓存的方式读取流,极大提高读取性能。要注意的是,在调用close()方法前记得调用flush()方法。
输出端:
原理和输入端一样,只不过是将JVM内存中的数据以字节的形式输出。
使用方法:
OutputStream input = newOutputStreamDecoratorClass(newOutputStreamSubClass(resource object));
字节输出端的常用类包结构如下:
OutputStream
----FileOutputStream 输出文件
----ByteArrayOutputStream输出JVM运行内存中的byte数组,免除存储和删除的操作
----SequenceOutputStream 合并多个OutputStream流
----PipedOutputStream 线程间通讯用
----ObjectOutputStream 序列化对象输出
----FilterOutputStreamOutputStream所有装饰类的父类
----DataOutputStream 可以输出各种Java的primitive type
----BufferedOutputStream 用来以缓存的方式输出流,极大提高输出性能
----PrintStream 提供格式化输出的方式
在字节输出端值得一提的是PrintStream类,这个类也是我们常用的System.out对象的类型。
所谓的格式化输出,就是能够在字节输出的基础上添加一些格式。比如最常用的System.out.println(a string”),就是在输出一个String的同时,添加了一个换行字符。更多的格式化输出方式可以参考Java API。
PrintStream类还有两个特点:一个是,该类任何写入方法都调用了它所装饰的OutputStream类的flush()方法,这也就意味着我们不需要显示地调用flush()方法了。二个是:该方法不会抛出异常,所有的被装饰的OutputStream类如果抛出InterruptedIOException,则按Thread.currentThread().interrupt(); 处理,对于其他抛出的IOException,PrintStream用一个私有布尔型trouble标注,通过checkError()方法来反馈。
四. 字符流
字符流是以面向字符的方式来处理I/O流的。所谓面向字符,就是说在程序读入读出的过程中,操作的不再是一个个字节,而是以字符char作为基本操作单元。这样通过字符流相关的类,我们就可以方便的读取和操作文本文件,并且可以使用String类等所提供的便捷方法。字符流是对字节流的一个封装,也就是说,在字节流的基础上,Java提供相关类的相关方法通过对不同编码方式文本文件的内容按字节读取然后转换成Java的Unicode表示方法。在输出端则是将Java的Unicode表示的字符,通过相关方法的转换成字节,然后一个一个写入文件。
同字节流一样,字符流也分为input和output两个部分。
输入端:
所有输入端的字符流处理类都继承Reader抽象类,包结构如下:
Reader
----InputStreamReader
----FileReader
----CharArrayReader
----StringReader
----PipedReader
----BufferedReader
其中InputStreamReader最为关键。因为它是将字节流转换成字符流的中间桥梁,这可以通过它的构造方法看出。而其他类在构造的时候都基于一个现成的Reader对象(也就是由InputStreamReader转化所得的Reader对象)。具体解释参见下一节。
输出端:
所有输出端的字符流处理类都继承了Writer抽象类,包结构如下:
Writer
----OutputStreamWriter
----FileWriter
----CharArrayWriter
----StringWriter
----PipedWriter
----BufferedWriter
在输出端作为桥梁的类是OutputStreamWriter。其余类的工作方式和输入端相同。
五. 字节流与字符流的转换
字节流可以通过某些类转变成字符流。最常用的有:
输入端 InputStreamReader input=new InputStreamReader(new FileInputStream(new File("D:/input.txt")));
输出端 OutputStreamWriter output=new OutputStreamWriter(new FileOutputStream(new File("D:/output.txt")));
在学习整理的过程中,我一直在想究竟字节流是怎样被转换成字符流的。经过一番查询和实验,这个问题终于有了一个比较清晰的回答。
第一点:一个byte是8个bit,一个char是两个byte。所有文件都是由byte组成的,无论是文本文件还是图像文件等等。而char则是java的一个基本类型,它是java在类存中存储字符的表现形式。
第二点:Java存储在内存中的char采用的是Unicode字符集,而文本文件存储在硬盘中的时候会被系统用指定的编码存储。字符集和编码是不同的概念。比如说Unicode定义了不同字符对应不同的数值,而UTF-8, UTF-16等是实现Unicode的编码形式。
第三点:Java在实现字节流与字符流转换的过程中,其实是将输入端不同的编码方式翻译成Unicode字符集所映射的数值,并在内存中对这些数值(即char)进行操作,最后在输出端将Unicode映射的数值转化成具体的编码方式输出。若没有指定输入输出的字符集,则Java默认采用系统默认编码方式。字符的概念仅存在于Java运行内存当中,与实际文件的存储没有关系。
实例:
在hello.txt中只包含了一个“好”字,我的系统默认编码是UTF-8,并且hello.txt采用无BOM的UTF-8。
public class EncodingTest {
public static void main(String[] args) throws IOException {
File file = new File("D:/hello.txt");
System.out.println(file.getName() + "长度为: " + file.length() + " bytes");
// 以字节流方式读入文件
InputStream input = new FileInputStream(file);
byte[] b = new byte[10];
input.read(b);
for (int i = 0; i < file.length(); i++) {
System.out.println("字节 " + i + ": " + Integer.toHexString(b[i]));
}
input.close();
System.out.println();
// 以字符流方式读入文件
input = new FileInputStream(file);
Reader reader = new InputStreamReader(input);
char c = (char) reader.read();
System.out.println(file.getName() + "内容为: " + c);
input.close();
System.out.println();
// ‘好’字的Unicode为22909
System.out.println("'好'的Unicode二进制: " + Integer.toBinaryString(22909));
}
}
输出为:
hello.txt长度为: 3 bytes
字节 0: ffffffe5
字节 1: ffffffa5
字节 2: ffffffbd
hello.txt内容为: 好
'好'的Unicode二进制: 101100101111101
在以字节流读入的时候,程序读出3个字节。在UTF-8中,“好”字对应的编码是 1110
0101 10
10 0101 10
11 1101。
在以字符流读入的时候,程序将UTF-8编码转变成“好”字的Unicode所对应的数值。“好”字在Unicode中对应的数值为22909,
二级制表示为 0101 1001 0111 1101。
通过UTF-8编码方式,将Unicode带入验证,所得结果即为所显示的三个字节组合。
至此,字节流转换成字符流的过程基本解释完毕。
六. I/O中有关buffer的使用
buffer的作用通过将数据暂时存放在内存中,从而降低程序和物理存储的读写次数来提高程序的效率。
字节流中操作buffer的相关类为BufferedInputStream和BufferedOutputStream。在这两个类中作为缓存的是一个byte数组。
字符流中操作buffer的相关类为BufferedReader和BufferedWriter。在这两个类中作为缓存的是一个char数组。
七. File类和RandomAccessFile类
在Java I/O范围类,还有一些非流式操作的类也很重要。比如File类和RandomAccessFile类。
File类:
File类是文件系统中所有文件的一个抽象。当File类被实例化的时候,它描述了一个由file path(文件路径)所指向的文件系统上的一个文件。通过调用File类的方法可以对文件进行添加,删除,获取长度等等操作。注意File类的作用是获取某个文件的相关信息,而不表示文件的具体内容。对应文件具体内容的操作,还要参照Java I/O中关于流的造作类。
RandomAccessFile类:
RandomAccessFile类对于文本文件的读写操作非常方面。
首先,该类允许对一个文件同时进行读与写的操作。也就是说将input和output操作整合到一起。
其次,该类即可以操作字节流也可以操作字符流,提供各种对Java基本类型的读入与输出的方法。
最后,该类提供文件内置指针操作,可以从指定的offset处读入或写入数据。比如seek(long pos)方法,就允许将指针设定在具体的位置,下一次read或者write就从该指针处进行。
八. 对象序列化
Java I/O关于对象序列化设计两个内容,一个是什么样的对象可以被序列化,二个是如何读入和输出被序列化的对象。
1. 创建可以被序列化的对象
一般情况下,当一个class实现了Serializable接口,那么这个class的实例就可以被序列化了。语法如下:
class XXX implements Serializable { }
在对象序列化的过程中,只有该对象的属性被序列化类。如果想要读取出这个序列化的对象,那么在读取程序的类路径中一定要包含该class,否则自然没有办法还原这个对象了。如果对于一个要进行的序列化的class,我们只希望序列化它的一部分属性,忽略某些敏感信息(比如 String password),那么我们就在这些不想被序列化的属性前加上transient关键字。
当程序员想能够自由的掌控序列化的过程,也就是说自定义序列过程的输入输出,那么就要实现Externalizable接口。语法如下:
class XXX implements Externalizable { }
实现Externalizable接口,意味着要实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)这两个方法。在这两个方法中,我们可以定义要序列的属性。这两个方法分别会被ObjectInputStream中的readOrdinaryObject()和ObjectOutputStream中的writeOrdinaryObject()方法所调用。
两点注意:
实现序列化的class要显示的写出无参数构造方法,这样可以避免在反序列化过程中抛出异常。
在实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)这两个方法时,读入和输出属性的顺序一定要一致,否则也会抛出异常。这是因为属性在被序列化成二进制码的过程中,都是按顺序输出和读入的。
2. 序列化对象的读入和输出
序列化对象的输出需要用到ObjectOutputStream类,调用writeObject()方法。
序列化对象的读入需要用到ObjectInputStream类,调用readObject()方法。
九. 参考