Java中IO系统剖析
【一】在Java的I/O系统中的顶级结构中,有InputStream、OutputStream、Reader、Writer、File类;在java的分类中将他们分为“字节流输入输出”和“字符流输入输出“ 两种;我们有必要介绍一下这两种概念的分别:
(1)所谓的“字节流输入输出”,是说这种流是按照一个字节一个字节的、8位8位的进行输入和输出;这有时非常容易让人迷惑,比如下面的这个例子:
while(true) { int i = System.in.read() ; if(i == -1) break ; System.out.println((char)i); }
当我们在控制台上输入一行字符:assdf234时,
程序会输出:assdf234 这和输入的内容一模一样;
但是如果说read()方法是按照字节进行读取的,依次仅仅读取一个字节的话,那么不可能输出完全一样的字符串;因为通过控制台和通过文件读取内容是一样的,内容都是字符型的,而java中规定char类型占用的内存是两个字节,那么也就是说如果输入在控制台上输入一个“s”字符,通过read()方法依次只能够读取一个字节,即按照上面的代码方式,根本不可能将输入的内容完全一样的输出到控制台上;但实际就是一样的,这是为什么呢?
(2)这是因为我们将“字节流”误解了,其中的“字节”和java虚拟机中内存的一个字节,并不是一个概念;我们知道不管是控制台或者文件,其中的一个字符(如“s”)在计算机的内存(并不是java虚拟机)中就是按照其ASCLL码存储的,这就是说在计算机的内存中,一个字符就占用一个字节;所谓的“字节流“中的字节说的就是计算机内存中的字节;
(3)按照②的分析我们就会完全明白read()方法的作用了:
public abstract int read() throws…………..
当我们用某个流调用这个方法时,该方法就返回当前的流中要读取的那个字符在计算机内存中(并非java虚拟机中)占的那8位所对应的十进制数,实际上就是其ASCLL值;这样我们实际上能够知道该方法的具体的执行过程:
①首先将要读的流中的字符从计算机内存加载到java虚拟机中,这个过程中,将原本由计算机中一个字节存储的字符转换成java虚拟机中的两个字节来存储(因为java中的char有两个字节存储)
②当调用该方法时,程序再将java虚拟机中的两个字节表示的字符转化成由一个字节表示时(ASCLL码表示时)对应的8位形式,即又转化回来了,之后将这8位对应的十进制数返回;也就是说java程序好像是用ASCLL码来识别文件字符的
③这就说明了,实际上按照“字节流”的读取方式非常的慢,读取一个文件字符,要转化两次;所以我们想:如果将文件字符转化成java虚拟机中的字符时,如果不按照ASCLL码来识别文件字符,而是在java虚拟机的内存中直接读两个字节,在按照Unicode码照样可以得到文件中字符的实际内容,即我们不用ASCLL识别而是用Unicode 码识别文件字符,而Unicode编码正是java采用的编码方式,java中char正好是两个字节存储的,这样一来我们只需要转换一次,就能够得到(或者说识别)文件字符的内容,非常的快,这就是“字符流”的概念
(4)明白了这个机制的话,我们就会明白实际上用“字节流”是不能够正确的读取汉字的,比如“李”,由于汉字是采用Unicode编码的,在计算机内存中占用两个字节,如果按照“字节流”一位一位的读取,根本不能够按照上面的代码将”李“字还原,如果我们按照
”字符流“来读取的话,不管是字符还是汉字,都能够正确的识别
但是这并不说想InputStream、和OutputStream这些类就已经被淘汰了,在很多方面这些“字节流“是非常的有用的,比如说,我们可以用“字节流”来统计文件内容的(计算机)字节数,虽然不能够将汉字正确识别,但是用read()等方法确实是将“李”读了两次,这样我们通过一个计数器就能够统计文件的(计算机)字节总数
(5)通常情况下,使用FileInputStream和FileOutputStream类,针对图像文件、声音文件、视频文件、配置文件、等读取和写入二进制数据。这些类也可以用于读写基于ASCLL码的文本文件。要是读写基于Unicode的文本文件,可以使用FileReader和FIleWrite类
(6)最值得注意的一点是,“字节流”类对于处理数据是非常有用的(在“字节流”类中有专门用于读写数据的流类,但是”字符流”类中却没有这样的类),而“字符流”对于处理字符是非常方便的,根据实际问题来选用
【二】首先将I/O类层次结构图加以说明:

InputStream 、OutputStream、类是操作“字节流”的流类
Reader、Writer、类是操作“字符流”的流类
1、首先我们知道在java.lang包当中有一个System类,这个类中的成员域中定义了三个静态的流对象的引用分别是 in 、out、err、其中这三个流对象都是负责直接操作控制台、键盘的流对象,我们可以直接使用System.in和System.out来调用对应的流的方法,实现与键盘和控制台的交互;我们没有必要担心用new来定义流对象将这些对象的引用赋予in/out/err,因为这些工作都有java自动完成,而且其中的各种方法都已经有java实现了,这些都是已经完成的工作,值得注意的是,当我们在查看这些对象的JDK源代码时,并不能看到内部的具体实现,因为java将这些内容给屏蔽了,因为那些都是与底层的交互;我们只要会用就行
2、字节流
①我们首先介绍FileInputStream和FileOutputStream两个实际类,他们都已经将继承自抽向类的方法重写了,这两个类对于文件的操作非常方便;至于具体的方法的使用,请查阅官方文档,书中创建流对象时都通过了向上转型:
如: InputStream stream = new FileInputStream(文件名);
OutputStream stream = new FileOutputStream(文件名);
其中值得我们注意的一点是:在从文件中向程序中读入读出内容时,我们可以使用FileOutputStream类中的write(参数)和FileInputStream类中的read(参数),两个方法,参数中都有一个用于参与辅助的byte数组,这数组的设定是非常重要的,由于在对磁盘读写时都是按照磁盘扇区的大小来进行的,而磁盘扇区的大小一般是512个字节或其倍数,所以这个数组的大小设为512,另一方面又避免了和磁盘的频繁的交互,可以加快磁盘的读写速度提高效率
我们会在FileOutputStream流类中发现这样一个方法:
public void write(byte[]b)throws IOException
当我们用这个方法将指定的byte数组中的各个字节元素写入到一个文本文件或者word文件中时,这些字节是以什么形式显示在文档中的呢?答案是字符类型,这是显然的因为输出文档的类型(文本类型)本身就已经限定了,显示的内容必定是字节
除此之外还有一个方法经常使用:
public void flush()throws IOException
这个方法非常的有用,且非常有必要,因为目前的计算机系统为了提高运行效率,常常采用缓存机制,这样,在调用形如方法write后,虽然表面上看好像已经将流中的内容写入了文件,但是实际上操作系统并么有(常常不会)将数据直接写入文件,或控制台,而是将他们保存在缓存中,但数据积累到一定的程度时,才会真正的往外输出数据。调用flush方法的就是为了强制将流中的数据输出到文件中;所以一般在执行写操作时,都要在后面加上这个方法
②在java.io.OutputStream的间接子类中,有一个使用非常方便的,使用频率非常高的类:java.io.PrintStream
这个类具有非常良好的特性:
(1)它包含可以用来直接输出多种类型数据的不同的成员方法
(2)它的大部分成员方法不抛出异常
(3)在创建一个PrintStream流类对象时,能够制定是否采用强制输出特性(flush)
下面将它的一些方法简单的介绍一下
有write/print/println/append/等输出方法,能够提供不同类型的数据的写入;其中还有format/printf方法能够进行格式化的输出,至于多种格式的具体使用方式就不在介绍
③
(1)现在介绍的这些流类都是用于读写字符的,也就是说当利用这些类时,只能直接处理字符;如果我们有这样一个问题,我们想要将一组整形数据读到data.txt文件中,之后我们读取data.txt中的数据为一些整形变量赋值,我们知道比如用PrintStream类将整形数据:12写到了文件中,那么文件中实际上存贮的是字符型的‘12’,如果我们用InputStream类读取这个文件,读出的数据还是一个字符型的‘12’,不是一个整形的12,我们还要通过转换将‘12’转换为整形的12,在赋给一个整形变量,这是非常麻烦的;
(2)幸好在java.io.InputStream类的间接子类中有一对专门用于“数据的输入与输出的流“——DataOutputStream和DataInputStream类,这两个流类必须配合使用才能够发挥作用
(3)流对象的创建:
构造方法:public DataInputStream(InputStream in);
public DataOutputStream(OutputStream out);
可见当创建数据输入输出流对象时,必须借助于InputStream和OutputStream流对象;
(4)DataOutputStream类成员方法简介
这个类中最常用的方法是:
①继承自父类的public void flush()throws IOException
②用于输出各种基本数据类型的成员方法:
public final void writeBoolean(Boolean v)
throws IOException
public final void writeInt(int v) throws IOException
public final void writeDouble(double v) throws IOException
public final vois writeFloat(float v) throws IOException
…………………………………………………
…………………………………………………
需要注意的是,这些方法写入当文件中的不再是对应参数的字符形式了(虽然在文本文件中内容总是以字符形式出现,但是如果上面的方法将整形的12写入文件中,文件中实际上保存的是整形数据12,而不再显示字符‘12‘,文件中显示的实际是ASCLL码值为12 的字符;这样我们就确实将整形的12 写到了文件当中了);还有一点是用这些方法写入到文件中的基本类型数据在计算机内存(就是在文件中占用的字节数)中占用的字节数和java中规定的对用的基本类型数据所占用的java虚拟机中的内存是一样的,如:将一个byte类型的数据写入到文件中,那么文件中就会记录一个占有一个字节的byte型数据、将一个double类型的数据写入到文件中时,那么文件中会记录一个占有8字节的double型的数据…………………….需要注意的是,boolean型的数据实际上存的是1和0,在文件中占一个字节
(5)DataInputStream类成员方法简介
这个流类必须与DataOutputStream类配合使用,当我们用DataOutputStream流对象将基本的数据类型写入到文件中后,我们就能够用DataInputStream流对象,直接从文件当中将这些数据以基本数据类型的形式读出来,可以保证读出来的直接就是DataOutoutStream对象存进去的基本数据类型的对应的数据
public final boolean readBoolean()throws IOException
public final short readShort()throws IOException
…………………………………..
…………………………………..
这些方法的使用要和当初使用DataOutputStream对象存入文件中使用的方法对应起来,要不就会因为读和写时所占字节不一致,而发生错误;
不管是DataInputStream还是DataOutputStream类含有成员方法public void close()throws IOException
④为了提高读取的效率,在java.io包中还有InputStream和OutputStream类的间接子类非常有用,这就是带有缓存的输入和输出流类——BufferInputStream和BufferOutputStream,这两个流类在其中均会开辟一个字节数组的存储单元,用作缓存,这样有利于提高读取的效率,而且这两个类的构造方法都可以自行制定缓存数组的大小,其余之外,这两个类都继承了父类的所有的方法,只是在效率上有提高,没有其他更加复杂的方法了,对于其中各个方法的介绍,可以具体参考官方文档,不再介绍,但是要注意的是,这两个流类的效率是非常的高的,一般如果要存取大型文件的话,用这两个流类是非常有效的
⑤标准输入输出流的重定向
我们知道在java.lang中的System类中有三个静态字段:out 、in、err他们表示的标准的输入输出流,一般用于和键盘或者控制台的交互,但是我们也可以将他们重定向,将这些标准的流类和具体的文件进行绑定,这就要用到System类中的三个方法:
public static void setIn(InputStream in)
public static void setOut(OutputStream out)
public static void setErr(PrintStream err)
如:
System.setIn(new InputStream (“data.txt”));
int a = System.in.read() ;
这时读取的数据就已经是从文件data.txt中读取了
实际上在java开发工具中也有支持标准流的重定向的工具
格式:java 源文件名 < 标准输入流重定向的文件名
在编译的同时,就自动将System.in进行了重定向
也可以是:java 源文件名 0< 标准输入流重定向的文件名
java 源文件名 1> 表准输出流重定向的文件名
就自动将System.out进行了重定向
java 源文件名 2> 标准错误输出流重定向的文件名
⑥在“字节流”中还有一个特殊的数据输出输入流类,能够随机的访问文件——RandomAccessFile类
我们知道之前介绍的流类都是输入流类和输出流类配合使用,但是这个流类非常的特殊,只要创建一个该流类的对象的话,就能够对一个文件进行交替的读和写操作,只要在创建创建对象时指定读写方式就可以了;不仅如此,这个对象还能够通过方法改变在文件中指针位置,从而能够任意指定读写的位置,这是因为这种流类是基于数据输入输出流的,这样在文件中就能够按照java中规定的变量的字节数来存储变量了(之前已经说过了),这样就能够通过简单的计算非常方便的确定当前文件的指针位置了:
常用的方法如下:
《具体的详见参考文档》
(1)public RandomAccessFile(String name , String mode)
throws FileNotFoundException
mode 的内容制定了常见的流对象的读取模式:
“r”——表示以只读的方式打开文件
“rw”——表示可以对文件同时进行读和写操作
(2)public void seek(long pos)
//将文件指针移动到pos指定的位置,pos是指从文件开始(以0为文件的开始)之后多少字节;
(3)public long getFilePoint()
//返回文件中指针当前的位置,即当前指针距离文件开始有多少个字节数;
(4)public void skipBytes(int n)
//将文件指针向后移动指定的字节数
(5)public long length()
//返回当前文件的长度
(6)由于该流类是基于数据输入输出流类的,所以这个类中也有和DataInputStream类中几乎一样的方法如:
各种用于输入的readByte()/reading/readDouble()等方法;
还有用于写入的writeByte()/writeInt()/readDouble()等方法;
(7)还有public void close() throws IOException方法
3、字符流
Reader/Writer层次结构图(常用)

这些都是“字符流”对象,使用起来也非常的方便,注意这些都是直接读写“字符”的,如在PrintWriter中有一个方法
public void write(char[ ] buf)
参数是char类型的数组而不再是byte类型的数组了
①和“字节流”对应,“字符流”也有相应的类
java.io.InputStream——java.io.Reader
java.io.OutputStream——java.io.Writer
java.io.FileInputStream——java.io.FileReader
java.io.FileOutputStream——java.io.FileWriter
java.io.BufferedInputStream——java.io.BufferedReader
java.io.BufferedOutputStream——java.io.BufferedWriter
java.io.PrintStream——java.io.PrintWriter
②由于这些类都非得简单,就不在一一的介绍了,具体的内容查阅官方文档就可以了,下面仅介绍几个类:
(1)对于BufferedReader 和BufferedWrite来说是带有缓存的流类,其中有三个有用的方法:
public String readLine()throws IOException
//按行读取文件中的字符,即每次读取一行字符
pubic int getLineNumber()
//获得当前行的行号
public void newline()throws IOException
//向文件中写入一个换行符
4、现在面临着这样一个问题:如果我们在控制台上输入:
12 34 45 66 78
现在想要将这些字符型的数据赋予一系列的int变量,那么我们应该如何做呢?显然我们直接用上面讲到过的流类是不能够直接解决这个问题的,但是我们可以这样,先用readLine()方法将这行字符整个读入到String str中,之后我们知道在String类中有这样一个方法:public String[ ] split(String regex);
这个方法能够以regex为分隔符将str分割成若干个String对象;那么这时String[ ]中的每个元素正好是单独的整形变量的字符形式;
之后我们用Integer.parseInt(参数)方法,就能够方便的将这些以字符表示的串转换成int型的数据了
5、在java.io包中还有这样的流类是专门用于将“字节流”转换成“字符流”的流类,这就是继承自java.io.Reader和java.io.Writer
的流类——
java.io.InputStreamReader和java.io.OutputStreamWriter类
①构造方法
public InputStreamReader(InputStream in)
pubic OutputStreamWriter(OutputStream out)
这样就能够将字节流转换成字符流了,同时注意到这样一问题;因为这两个流类都是Reader和Writer的子类那么就能够进一步将之转换成BufferedReader和BufferedWriter类了,提高效率:
如:
BufferedReader f =
new BufferedReader(new InputStreamReader(System.in));
注意上面的这种方式是非常常用的,以至于在雍俊海的书中将这两个类直接介绍称“从控制台窗口读入数据”
因为我们知道对于System.in来说,他是基于字节流的对象,那么如果我们想要从控制台上频繁的以字符的形式读入数据的话,那么就非常的不方便,尤其是当我们需要读取汉字时,那么我们就可以通过上面的方法将System.in转换成BufferedReader的字符流,这是非常方便的

浙公网安备 33010602011771号