第 7 章 IO/ 输入输出
大多数应用程序都需要与外部设备进行数据交换,最常见的外部设备包含磁盘和网络, IO就是指应用程序对这些设备的数据输入与输出,在程序中,键盘被当作输入文件,显示器被当作输出文件使用。 Java语言定义了许多类专门负责各种方式的输入输出,这些类都被放在 java.io包中。
7.1 File 类
File类是 IO包中唯一代表磁盘文件本身的对象, File类定义了一些与平台无关的方法来操纵文件,通过调用 File类提供的各种方法,我们能够创建、删除文件,重命名文件,判断文件的读写权限及是否存在,设置和查询文件的最近修改时间。
在 Java中,目录也被当作 File使用,只是多了一些目录特有的功能——可以用 list方法列出目录中的文件名。在 Unix下的路径分隔符为( /),在 Dos下的路径分隔符为( /), Java可以正确处理 Unix和 Dos的路径分隔符,即使我们在 Windows环境下使用( /)作为路径分隔符, Java仍然能够正确处理。
我们用下面的一个简单应用来演示一下 File类用法,判断某个文件是否存在,存在则删除,不存在则创建,读者可以在 Windows的资源管理器下观察到这个变化。
程序清单: FileTest.java
import java.io.*;
public class FileTest
{
public static void main(String[] args)
{
File f=new File("c://1.txt");
if(f.exists())
f.delete();
else
try
{
f.createNewFile();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
System.out.println("File name:"+f.getName());
System.out.println("File path:"+f.getPath());
System.out.println("Abs path:"+f.getAbsolutePath());
System.out.println("Parent:"+f.getParent());
System.out.println(f.exists()?"exists":"does not exist");
System.out.println(f.canWrite()?"is writeable":"
is not writeable");
System.out.println(f.canRead()?"is readable":"is not readable");
System.out.println(f.isDirectory()?"is ":"is not"+" a directory");
System.out.println(f.isFile()?"is normal file":"might be a named pipe");
System.out.println(f.isAbsolute()?"is absolute":"
is not absolute");
System.out.println("File last modified:"+f.lastModified());
System.out.println("File size:"+f.length()+" Bytes");
}
}
当运行这个程序时会因为文件 1.txt的存在和不存在而出现两种结果:
结果 1 :
File name:1.txt
File path:c:/1.txt
Abs path:c:/1.txt
Parent:c:/
exists
is writeable
is readable
is not a directory
is normal file
is absolute
File last modified:1051755103126
File size:0 Bytes
结果 2 :
File name:1.txt
File path:c:/1.txt
Abs path:c:/1.txt
Parent:c:/
does not exist
is not writeable
is not readable
is not a directory
might be a named pipe
is absolute
File last modified:0
File size:0 Bytes
注: delete 方法删除由 File 对象的路径所表示的磁盘文件。它只能删除普通文件,而不能删除目录,即使是空目录也不行。
关于 File类的其它方法,是没法死记硬背的,读者在需要时自己查看 JDK文档,应该能够明白怎么使用。初步接触了 File类,我们发现 File类不能访问文件的内容,即不能够从文件中读取数据或往文件里写数据,它只能对文件本身的属性进行操作。
7.2 RandomAccessFile 类
RandomAccessFile类可以说是 Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。 RandomAccessFile类支持“随机访问”方式,我们可以跳转到文件的任意位置处读写数据。 在你访问一个文件的时候 ,不想把文件从头读到尾 ,并希望像访问一个数据库一样的访问一个文本文件,使用 RandomAccessFile类就是你的最佳选择。
RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,当读写 n 个字节后,文件指示器将指向这 n个字节后的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,我们可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。 RandomAccessFile在等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,但该类仅限于操作文件,不能访问其他的 IO设备,如网络,内存映象等。
有关 RandomAccessFile类中的成员方法及使用说明,请参阅 JDK文档。
下面是一个使用 RandomAccessFile的例子,往文件中写入三名员工的信息,然后按照第二名员工,第一名员工,第三名员工的先后顺序读出。 RandomAccessFile可以以只读或读写方式打开文件,具体使用哪种方式 取决于我们创建 RandomAccessFile类对象的 构造方式:
new RandomAccessFile(f,"rw"); // 读写方式
new RandomAccessFile(f,"r"); // 只读方式
注:当我们的程序需要以读写的方式打开一个文件时,如果这个文件不存在,程序会为你创建它。
我们还需要设计一个类来封装员工信息。 一个员工信息就是文件中的一条记录,我们必须保证每条记录在文件中的大小相同,也就是每个员工的姓名字段在文件中的长度是一样的,我们才能够准确定位每条记录在文件中的具体位置。假设 name中有八个字符,少于八个则补空格(这里我们用 "/u0000"),多于八个则去掉后面多余的部分。由于年龄是整型数,不管这个数有多大,只要它不超过整型数的范围,在内存中都是占 4个字节大小。
程序清单: RandomFileTest.java
import java.io.*;
public class RandomFileTest
{
public static void main(String [] args) throws Exception
{
Employee e1 = new Employee("zhangsan",23);
Employee e2 = new Employee("Lisi",24);
Employee e3 = new Employee("Wangwu",25);
RandomAccessFile ra=new RandomAccessFile("c://1.txt","rw");
ra.write(e1.name.getBytes());
ra.writeInt(e1.age);
ra.write(e2.name.getBytes());
ra.writeInt(e2.age);
ra.write(e3.name.getBytes());
ra.writeInt(e3.age);
ra.close();
RandomAccessFile raf=new RandomAccessFile("c://1.txt","r");
int len=8;
raf.skipBytes(12); // 跳过第一个员工的信息,其中姓名 8 字节,年龄 4 字节
System.out.println(" 第二个员工信息: ");
String str="";
for(int i=0;i
str=str+(char)raf.readByte();
System.out.println("name:"+str);
System.out.println("age:"+raf.readInt());
System.out.println(" 第一个员工的信息: ");
raf.seek(0); // 将文件指针移动到文件开始位置
str="";
for(int i=0;i
str=str+(char)raf.readByte();
System.out.println("name:"+str);
System.out.println("age:"+raf.readInt());
System.out.println(" 第三个员工的信息: ");
raf.skipBytes(12); // 跳过第二个员工信息
str="";
for(int i=0;i
str=str+(char)raf.readByte();
System.out.println("name:"+str.trim());
System.out.println("age:"+raf.readInt());
raf.close();
}
}
class Employee
{
String name;
int age;
final static int LEN=8;
public Employee(String name,int age)
{
if(name.length()>LEN)
{
name = name.substring(0,8);
}
else
{
while(name.length()
name=name+"/u0000";
}
this.name=name;
this.age=age;
}
}
运行结果:
第二个员工信息:
name:Lisi
age:24
第一个员工的信息:
name:zhangsan
age:23
第三个员工的信息:
name:Wangwu
age:25
c 盘还多了个文件 1.txt :
图 7.1
上面的这个程序完成了我们想要的功能,演示了 RandomAccessFile类的作用。 String.substring(int beginIndex,int endIndex)方法可以用于取出一个字符串中的部分子字符串,要注意的一个细节是:子字符串中的第一个字符对应的是原字符串中的脚标为 beginIndex处的字符,但最后的字符对应的是原字符串中的脚标为 endIndex-1处的字符,而不是 endIndex处的字符。在实际生活中,我们常用的数据库和数据库管理工具实际上就是这种原理。我们的 1.txt就相当于数据库的数据文件,而我们这个程序提供了往这个数据文件写入和读取数据的功能。
7.3 节点流
7.3.1 理解流的概念
数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。
我们将 IO流类分为两个大类,节点流类和过滤流类(也叫处理流类)。程序用于直接操作目标设备所对应的类叫节点流类,程序也可以通过一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据,这个间接流类就是过滤流类(也叫处理流类),我更喜欢称之为包装类。不管叫什么,都只是一个代名词而已,读者不要太在意,你可以根据自己的习惯和喜好来定。
7.3.2 InputStream 与 OutputStream
程序可以从中连续读取字节的对象叫输入流,用 InputStream类完成,程序能向其中连续写入字节的对象叫输出流,用 OutputStream类完成。 InputStream与 OutputStream对象是两个抽象类,还不能表明具体对应哪种 IO设备。它们下面有许多子类,包括网络,管道,内存,文件等具体的 IO设备,如 FileInputStream类对应的就是文件输入流,是一个节点流类,我们将这些节点流类所对应的 IO源和目标称为流节点 (Node) 。
F 指点迷津:
很多人搞不清程序要将 A 文件的内容写入 B 文件中,程序对 A 文件的操作所用的是输出类还是输入类这个问题。读者也先自己想想,再记住下面的话,输入输出类是相对程序而言的,而不是代表文件的,所以我们应该创建一个输入类来完成对 A 文件的操作,创建一个输出类来完成对 B 文件的操作。
InputStream定义了 Java的输入流模型。该类中的所有方法在遇到错误的时候都会引发 IOException异常,下面是 InputStream类中方法的一个简要说明:
ü int read()返回下一个输入字节的整型表示 , , 如果返回 -1表示遇到流的末尾,结束。
ü int read(byte[] b)读入 b.length个字节放到 b中并返回实际读入的字节数。
ü int read(byte[] b,int off,int len) 这个方法表示把流中的数据读到 ,数组 b中 ,第 off个开始的 len个数组元素中 .
ü long skip(long n) 跳过输入流上的 n个字节并返回实际跳过的字节数。
ü int availabale() 返回当前输入流中可读的字节数。
ü void mark(int readlimit)在输入流的当前位置处放上一个标志,允许最多再读入 readlimit个字节。
ü void reset() 把输入指针返回到以前所做的标志处。
ü boolean markSupported() 如果当前流支持 mark/reset操作就返回 true。
ü void close() 在操作完一个流后要使用此方法将其关闭 , 系统就会释放与这个流相关的资源。
InputStream是一个抽象类,程序中实际使用的是它的各种子类对象。不是所有的子类都会支持 InputStream中定义的某些方法的,如 skip,mark,reset等,这些方法只对某些子类有用。
F 指点迷津:
一个对象在没有引用变量指向它时会变成垃圾,最终会被垃圾回收器从内存中清除。对于我们创建的流对象,干嘛还要“调用 close 方法将它关闭,以释放与其相关的资源”呢?这相关的资源到底是些什么呢?我们在程序中创建的对象都是对应现实世界中有形或无形的事物,计算机操作系统所产生的东西当然也是现实世界中的事物,也就是说,程序中的对象也可以对应计算机操作系统所产生的一个其他东西,专业地说,这些东西叫资源,流就是操作系统产生的一种资源。当我们在程序中创建了一个 IO 流对象,同时系统内也会创建了一个叫流的东西,在这种情况下,计算机内存中实际上产生了两个事物,一个是 Java 程序中的类的实例对象,一个是系统本身产生的某种资源,我们以后讲到的窗口, Socket 等都是这样的情况。 Java 垃圾回收器只能管理程序中的类的实例对象,没法去管理系统产生的资源,所以程序需要调用 close 方法,去通知系统释放其自身产生的资源。
OutputStream是一个定义了输出流的抽象类,这个类中的所有方法均返回 void,并在遇到错误时引发 IOException异常。下面是 OutputStream的方法:
ü void write(int b) 将一个字节写到输出流。注意,这里的参数是 int型,它允许 write使用表达式而不用强制转换成 byte型。
ü void write(byte[] b) 将整个字节数组写到输出流中。
ü void write(byte [] b,int off,int len) 将字节数组 b中的从 off开始的 len个字节写到输出流。
ü void flush彻底完成输出并清空缓冲区。
ü void close关闭输出流。
& 多学两招:
计算机访问外部设备,要比直接访问内存慢得多,如果我们每一次 write 方法的调用都直接写到外部设备(如直接写入硬盘文件), CPU 就要花费更多的时间等待外部设备;如果我们开辟一个内存缓冲区,程序的每一次 write 方法都是写到这个内存缓冲区中,只有这个缓冲区被装满后,系统才将这个缓冲区的内容一次集中写到外部设备。使用内存缓冲区有两个方面的好处,一是有效地提高了 CPU 的使用率,二是 write 并没有马上真正写入到外设,我们还有机会回滚部分写入的数据。使用缓冲区,能提高整个计算机系统的效率,但也会降低单个程序自身的效率,由于有这么一个中间缓冲区,数据并没有马上写入到目标中去,例如在网络流中,就会造成一些滞后。对于输入流,我们也可以使用缓冲区技术。在程序与外部设备之间到底用不用缓冲区,是由编程语言本身决定的,我们通常用的 C 语言默认情况下就会使用缓冲区,而在 Java 语言中,有的类使用了缓冲区,有的类没有使用缓冲区,我们还可以在程序中使用专门的包装类来实现自己的缓冲区。
flush 方法就是用于即使在缓冲区没有满的情况下,也将缓冲区的内容强制写入到外设,习惯上称这个过程为刷新。可见, flush 方法不是对所有的 OutputStream 子类都起作用的,它只对那些使用缓冲区的 OutputStream 子类有效。如果我们调用了 close 方法,系统在关闭这个流之前,也会将缓冲区的内容刷新到硬盘文件的。
作者开发过一个邮件服务器程序,需要 7*24 小时不间断工作,这个服务器程序要面对 internet 上各种可能的非法格式的数据输入和攻击,而我的程序正好又没考虑到某种非法格式的数据,一旦碰到这样的情况,程序就会崩溃。有经验的人都知道,为了找出服务器程序崩溃的原因,我们可以将程序每次接收到的数据都记录到一个文件中,当服务器程序崩溃后,我们便打开这个记录文件,查看最后记录的那条数据,这个数据就是让我的程序毙命的罪魁祸首,然后拿着这条数据一步步测试我们的程序,就很容易找出程序中的问题了。遗憾的是,我每次用最后记录的这条数据测试我的程序,程序均安然无恙。最后,我发现就是因为有缓冲区的原因,缓冲区的内容还没来得及刷新到硬盘文件,程序就崩溃了,所以,文件中并没有记录最后接收到的那些数据,我在文件中看到的最后以条记录并不是真正最后接收到的那条数据。发现了这个原因,我修改程序,在每一次调用 write 语句后,都立即调用 flush 语句,这样,我就终于找到了肇事元凶,并修复了程序的这个漏洞。
尽管我以前从来没有真正认真思考和编程试验过缓冲区问题,但是正因为还有那么一点点概念和印象,所以,在出现问题时,我才能从多方面去思考并最终解决问题。我建议读者花更多的时间去开阔自己的知识面和思维,了解更多的原理,而不是去花大量时间去死记硬背某些细节和术语,特别是一个类中的每个函数名的具体拼写、具体的参数形式, Java 中有哪些关键字等这些死板的东西,只要有个印象就足够了。
7.3.3 FileInputStream 与 FileOutputStream
这两个流节点用来操作磁盘文件 ,在创建一个 FileInputStream对象时通过构造函数指定文件的路径和名字 ,当然这个文件应当是存在的和可读的。在创建一个 FileOutputStream对象时指定文件如果存在将要被覆盖。
下面是对同一个磁盘文件创建 FileInputStream对象的两种方式。其中用到的两个构造函数都可以引发 FileNotFoundException异常:
FileInputStream inOne=new FileInputStream (" hello.test" );
File f = new File("hello.test");
FileInputStream inTwo = new FileInputStream (f);
尽管第一个构造函数更简单,但第二个构造函数允许在把文件连接到输入流之前对文件做进一步分析。
FileOutputStream对象也有两个和 FileInputStream对象具有相同参数的构造函数,创建一个 FileOutputStream对象时,可以为其指定还不存在的文件名,但不能是存在的目录名,也不能是一个已被其他程序打开了的文件。 FileOutputStream先创建输出对象,然后再准备输出。
其实在上一章中讲 Properties类的时候,我们已经使用过这两个类。在下面的例子中,我们用 FileOutputStream类向文件中写入一串字符,并用 FileInputStream读出。
程序清单: FileStream.java
import java.io.*;
public class FileStream
{
public static void main(String[] args)
{
File f = new File("hello.txt");
try
{
FileOutputStream out = new FileOutputStream(f);
byte buf[]="www.it315.org".getBytes();
out.write(buf);
out.close();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
try
{
FileInputStream in = new FileInputStream(f);
byte [] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
}
编译运行上面的程序, 我们能够看到当前目录下产生了一个 hello.txt的文件,用记事本程序打开这个文件,能看到我们写入的内容。随后,程序开始读取文件中的内容,并将读取到的内容打印出来。在这个例子中,我们演示了怎样用 FileOutputStream往一个文件中写东西和怎样用 FileInputStream从一个文件中将内容读出来。有一点不足的是,这两个类都只提供了对字节或字节数组进行读取的方法,对于字符串的读写,我们还需要进行额外的转换。
7.3.4 Reader 与 Writer
Java中的字符是 unicode编码,是双字节的,而 InputStream与 OutputStream是用来处理字节的,在处理字符文本时不太方便,需要编写额外的程序代码。 Java为字符文本的输入输出专门提供了一套单独的类, Reader、 Writer两个抽象类与 InputStream、 OutputStream两个类相对应,同样, Reader、 Writer下面也有许多子类,对具体 IO设备进行字符输入输出,如 FileReader就是用来读取文件流中的字符。
对于 Reader和 Writer,我们就不过多的说明了,大体的功能和 InputStream、 OutputStream 两个类相同,但并不是它们的代替者,只是在处理字符串时简化了我们的编程。我们上面的程序改为使用 FileWriter和 FileReader来实现,修改后的程序代码如下:
import java.io.*;
public class FileStream
{
public static void main(String[] args)
{
File f = new File("hello.txt");
try
{
FileWriter out = new FileWriter(f);
out.write("www.it315.org");
out.close();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
try
{
FileReader in = new FileReader(f);
char [] buf = new char[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
}
我们发现编译运行后的结果与先前没有什么两样,由于 FileWriter可以往文件中写入字符串,我们不用将字符串转换为字节数组。相对于 FileOutputStream来说,使用 FileReader读取文件中的内容,并没有简化我们的编程工作, FileReader的优势,要结合我们后面讲到的包装类才能体现出来。
$ 独家见解 :
我们将程序中的 out.close(); 语句注释掉后编译运行,在 hello.txt 文件中没有看到 out.write 语句写入的字符串,这可能就是我们前面谈到的缓冲区的原因,我们将 out.close() 改为 out.flush 后编译运行,在 hello.txt 文件中又能够看到 out.write 语句写入的字符串了,这更加证明了 FileWriter 使用了缓冲区。在使用 FileOutputStream 的例子程序中,我们同样注释掉 out.close() ; 语句,编译运行后,在 hello.txt 文件中能够看到 out.write 语句写入的字符串,这说明 FileOutputStream 没有使用缓冲区。
7.3.5 PipedInputStream 与 PipedOutputStream
一个 PipedInputStream对象必须和一个 PipedOutputStream对象进行连接而产生一个通信管道, PipedOutStream可以向管道中写入数据, PipedInputStream可以从管道中读取 PipedOutputStream写入的数据。这两个类主要用来完成线程之间的通信,一个线程的 PipedInputStream 对象能够从另外一个线程的 PipedOutputStream对象中读取数据。请看下面的例子:
程序清单: PipeStreamTest.java
import java.io.*;
public class PipeStreamTest
{
public static void main(String args[])
{
try
{
Thread t1=new Sender();
Thread t2=new Receiver();
PipedOutputStream out = t1.getOutputStream();
PipedInputStream in = t2.getInputStream();
out.connect(in);
t1.start();
t2.start();
}
catch(IOException e)
{
System.out.println(e.getMessage());
}
}
}
class Sender extends Thread
{
private PipedOutputStream out=new PipedOutputStream();
public PipedOutputStream getOutputStream()
{
return out;
}
public void run()
{
String s=new String("hello,receiver ,how are you");
try
{
out.write(s.getBytes());
out.close();
}
catch(IOException e)
{
System.out.println(e.getMessage());
}
}
}
class Receiver extends Thread
{
private PipedInputStream in=new PipedInputStream();
public PipedInputStream getInputStream()
{
return in;
}
public void run()
{
String s=null;
byte [] buf = new byte[1024];
try
{
int len =in.read(buf);
s = new String(buf,0,len);
System.out.println("the following message comes from sender:/n"+s);
in.close();
}
catch(IOException e)
{
System.out.println(e.getMessage());
}
}
}
运行结果:
the following message comes from sender:
hello,receiver ,how are you
JDK还提供了 PipedWriter和 PipedReader这两个类来用于字符文本的管道通信,读者掌握了 PipedOutputStream和 PipedInputStream类,自然也就知道如何使用 PipedWriter和 PipedReader这两个类了。
$ 独家见解 :
使用管道流类,可以实现各个程序模块之间的松耦合通信,我们可以灵活地将多个这样的模块的输出流与输入流相连接,以拼装成满足各种应用的程序,而不用对模块内部进行修改。
就象家庭的供水系统一样,我们可以把进水表的出水管与净化过滤器的进水管连在一起,然后,把净化过滤器的出水管同水箱的进水管连在一起来拼凑成我们的供水管道系统。我们可以在这个供水管道系统中增加其他的水处理装置,也可以更换一个更大的水箱,甚至可以将进水表与水箱直连,而不经过净化过滤器,这一切都只需要各个水处理装置带有标准输入输出管道。
可见,使用管道流进行通信的模块具有“强内聚,弱耦合”的特点,一个模块被替换,或被拆卸不会影响其他模块 。 假设有一个使用了管道流的压缩或加密的模块,我们的调用程序只管向该模块的输入流中送入数据,从该模块的数据流中取得数据,就完成了我们数据的压缩或加密,这个模块完全就象黑匣子一样,我们根本不用去了解它的任何细节。
7.3.6 ByteArrayInputStream 与 ByteArrayOutputStream
ByteArrayInputStream是输入流的一种实现,它有两个构造函数,每个构造函数都需要一个字节数组来作为数据源:
ByteArrayInputStream(byte[] buf)
ByteArrayInputStream(byte[] buf, int offset, int length)
第二个构造函数指定仅使用数组 buf中的从 offset开始的 length个元素作为数据源。
ByteArrayOutputStream是输出流的一种实现,它也有两个构造函数。
ByteArrayOutputStream()
ByteArrayOutputStream(int)
第一种形式的构造函数创建一个 32字节的缓冲区,第二种形式则是根据参数指定的大小创建缓冲区,缓冲区的大小在数据过多时能够自动增长。
这两个流的作用在于,用 IO流的方式来完成对字节数组内容的读写。爱思考的读者一定有过这样的疑问:对数组的读写非常简单,我们为什么不直接读写字节数组呢?我在什么情况下该使用这两个类呢?
有的读者可能听说过内存虚拟文件或者是内存映像文件,它们是把一块内存虚拟成一个硬盘上的文件,原来该写到硬盘文件上的内容会被写到这个内存中,原来该从一个硬盘文件上读取内容可以改为从内存中直接读取。如果程序在运行过程中要产生一些临时文件,就可以用虚拟文件的方式来实现,我们不用访问硬盘,而是直接访问内存,会提高应用程序的效率。
假设有一个别人已经写好了的压缩函数,这个函数接收两个参数,一个输入流对象,一个输出流对象,它从输入流对象中读取数据,并将压缩后的结果写入输出流对象中。我们的程序要将一台计算机的屏幕图像通过网络不断地传送到另外的计算机上,为了节省网络带宽,我们需要对一副屏幕图像的像素数据进行压缩后,再通过网络发送出去的。如果没有内存虚拟文件,我们就必须先将一副屏幕图像的像素数据写入到硬盘上的一个临时文件,再以这个文件作为输入流对象去调用那个压缩函数,接着又从压缩函数生成的压缩文件中读取压缩后的数据,再通过网络发送出去,最后删除压缩前后所生成的两个临时文件。可见这样的效率是非常低的。我们要在程序分配一个存储数据的内存块,通常都用定义一个字节数组来实现的,
JDK中提供了 ByteArrayInputStream和 ByteArrayOutputStream这两个类可实现类似内存虚拟文件的功能,我们将抓取到的计算机屏幕图像的所有像素数据保存在一个数组中,然后根据这个数组创建一个 ByteArrayInputStream流对象,同时创建一个用于保存压缩结果的 ByteArrayOutputStream流对象,将这两个对象作为参数传递给压缩函数,最后从 ByteArrayOutputStream流对象中返回包含有压缩结果的数组。
我们通过下面的例子程序来模拟上面的过程,我们并没有真正压缩输入流中的内容,只是把输入流中的所有英文字母变成对应的大写字母写入到输出流中。
程序清单: ByteArrayTest.java
import java.io.*;
public class ByteArrayTest
{
public static void main(String[] args) throws Exception
{
String tmp="abcdefghijklmnopqrstuvwxyz";
byte [] src =tmp.getBytes();//src 为转换前的内存块
ByteArrayInputStream input = new ByteArrayInputStream(src);
ByteArrayOutputStream output = new ByteArrayOutputStream();
new ByteArrayTest().transform(input,output);
byte [] result = output.toByteArray();//result 为转换后的内存块
System.out.println(new String(result));
}
public void transform(InputStream in,OutputStream out)
{
int c=0;
try
{
while((c=in.read())!=-1)//read 在读到流的结尾处返回 -1
{
int C = (int)Character.toUpperCase((char)c);
out.write(C);
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
运行结果为:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
与 ByteArrayInputStream和 ByteArrayOutputStream类对应的字符串读写类分别是 StringReader和 StringWriter。读者可以将上面的程序修改成由这两个类来完成,具体的程序代码就不在这里多说了。
7.3.7 IO 程序代码的复用
由于没有编码为 -1的字符,所以,操作系统就使用 -1作为硬盘上的每个文件的结尾标记,对于文本文件,我们的程序只要从文件中读取到了一个 -1的字符值时,就可以确定已经到了这个文件结尾。注意,这种方式只能用于判断文本文件是否结束,不能判断一个二进制文件是否结束。尽管二进值文件的结尾标记也是 -1,因为二进制文件中的每个字节可以是 -128到 127之间的任意取值,其中就包括 -1,当程序读取到一个 -1的字节是,就难以判定是文件结尾还是文件中的有效数据。对于标准的二进值文件,在文件开始部分,都有一个文件头指定文件的大小,程序就是凭借文件头中的这个大小来读取文件中的所有内容的。
我本人曾经为二进制文件和文本文件的区别困惑过很久,后来发现许多有一定软件开发经验的人也没完全搞清楚两者的区别。我们知道内存中的一个字节中数据可以是 -128到 127之间的任意值,实际上是以二进制形式存放的,文件就是一片内存的数据在硬盘上的另外一种存放形式,也都是二进制数据,所以,可以说每个文件都是二进制的。我们现在的每个字符由一个或多个字节组成,每个字节都是用的 -128到 127之间的部分数值来表示的,也就是说, -128到 127之间还有一些数据没有对应任何字符的任何字节。如果一个文件中的每个字节中的内容都是可以表示成字符的数据,我们就可以称这个文件为文本文件,可见,文本文件只是二进制文件中的一种特例,为了与文本文件相区别,人们又把除了文本文件以外的文件称之为二进制文件。由于很难严格区分文本文件和二进制文件的概念,所以我们可以简单地认为,如果一个文件专用于存储文本字符的数据,没有包含字符之外的其他数据,我们就称之为文本文件,除此之外的文件就是二进制文件。
为了支持标准输入输出设备, Java定义了两个特殊的流对象, System.in和 System.out。 System.in对应键盘,是 InputStream类型的,程序使用 System.in可以读取从键盘上输入的数据。 System.out对应显示器,是 PrintStream类型的, PrintStream是 OutputStream的一个子类,程序使用 System.out可以将数据输出到显示器上。 键盘可以被当作一个特殊的输入文件,显示器可以被当作一个特殊的输出文件。当我们把键盘作为输入文件处理时,在 Windows下,我们可以按下 ctrl+z组合键来输入 -1作为文件结束标记,在 linux下,我们可以按下 ctrl+d组合键来输入 -1。
我们在编写流的程序时,应尽量考虑到程序代码的复用性,对于我们上面的程序代码,我们可以直接调用上面的 transform方法,将键盘上输入的内容转变成大写字母后打印在屏幕上,程序代码如下:
import java.io.*;
public class ByteArrayTest
{
public static void main(String[] args) throws Exception
{
new ByteArrayTest().transform(System.in,System.out);
}
public void transform(InputStream in,OutputStream out)
{
int c=0;
try
{
while((c=in.read())!=-1)
{
int C = (int)Character.toUpperCase((char)c);
out.write(C);
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
我们没有修改 transform方法中的任何代码,就利用它完成了我们期望的功能。我们还可以使用 transform方法将一个文件中的内容全部变成大写字母后写入另外一个文件,也可以将键盘上输入的内容转变成大写字母后写入另外一个文件,这就是因为我们在 tranform方法中使用的是 InputStream和 OutputStream这两个抽象基类,而不是直接使用某个具体的子类,这样就达到了以不变应万变的效果。
如果我们平时从键盘上读取内容的程序代码也放在一个类似 transform方法的函数中去完成,也是用 -1来作为键盘输入的结束,在该函数中不直接使用 System.in,只是在调用该函数时,将 System.in作为参数传递进去。这样,我们以后要从某个文件中读取数据,来代替手工键盘输入时,我们可以直接使用这个函数,程序就不用作太多的修改了。
7.4 过滤流与包装类
7.4.1 理解包装类的概念与作用
在前面的部分,我们接触到了许多节点流类,就以 FileOutputStream和 FileInputStream为例吧,这两个类只提供了读写字节的方法,我们通过它们只能往文件中写入字节或从文件中读取字节。在实际应用中,我们要往文件中写入或读取各种类型的数据,我们就必须先将其他类型的数据转换成字节数组后写入文件或是将从文件中读取到的字节数组转换成其他类型,这给我们的程序带来了一些困难和麻烦。如果有人给我们提供了一个中间类,这个中间类提供了读写各种类型的数据的各种方法,当我们需要写入其他类型的数据时,只要调用中间类中的对应的方法即可,在这个中间类的方法内部,它将其他数据类型转换成字节数组,然后调用底层的节点流类将这个字节数组写入目标设备。我们将这个中间类叫做过滤流类或处理流类,也叫包装类,如 IO包中有一个叫 DataOutputStream的包装类,下面是它所提供的部分方法的列表。
public final void writeBoolean(boolean v) throws IOException
public final void writeShort(int v) throws IOException
public final void writeChar(int v) throws IOException
public final void writeInt(int v) throws IOException
public final void writeLong(long v) throws IOException
public final void writeFloat(float v) throws IOException
public final void writeDouble(double v) throws IOException
public final void writeBytes(String s) throws IOException
大家从上面的方法名和参数类型中,就知道这个包装类能帮我们往 IO设备中写入各种类型的数据。包装类的调用过程如图 7.2所示:
图 7.2
使用输出包装类的过程,就好比我们要给某个市长送礼,该市长向来不接受陌生人的礼品,但其夫人则是来者不拒的。我们只要将礼品送到市长夫人手中,就等于送到了市长的手中。市长夫人就是我们用到的输出包装类。
使用输入包装类过程,好比我们要想借用市长大人的一点权力,承揽一个假竞标的工程项目,我们是没法直接请动市长大人来替我们说话的,但我们可以让市长公子来替我们办好这件事,我们就间接借用了市长的权力。市长公子就是我们用到的输入包装类。
我们还可以用包装类去包装另外一个包装类,创建包装类对象时,必须指定它要调用的 那个底层流对象,也就是这些包装类的构造函数中,都必须接收另外一个流对象作为参数。如 DataOutputStream包装类的构造函数为:
public DataOutputStream(OutputStream out)
参数 out就是 DataOutputStream要调用的那个底层输出流对象。
7.4.2 BufferedInputStream 与 BufferedOuputStream
对 I/O进行缓冲是一种常见的性能优化。缓冲流为 I/O流增加了内存缓冲区。增加缓冲区有两个基本目的:
ü 允许 Java的 I/O一次不只操作一个字节,这样提高了整个系统的性能。
ü 由于有缓冲区,使得在流上执行 skip、 mark和 reset方法都成为可能。
BufferedInputStream
Java的 BufferedInputStream类可以对任何的 InputStream进行带缓冲区的封装以达到性能的改善。 BufferedInputStream有两个构造函数:
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in,int size)
第一种形式的构造函数创建了一个带有 32字节缓冲区的缓冲流,第二种形式的构造函数按指定的大小来创建缓冲区。通常缓冲区大小是内存、磁盘扇区或其它系统容量的整数倍,这样就可以充分提高 I/O的性能。一个最优的缓冲区的大小,取决于它所在的操作系统、可用的内存空间以及机器的配置。
对输入流进行缓冲可以实现部分字符的回流。除了 InputStream中常用的 read和 skip方法, BufferedInputStream还支持 mark和 reset方法。 mark方法在流的当前位置作一个标记,该方法接收的一个整数参数用来指定从标记处开始,还能通过 read方法读取的字节数, reset方法可以让以后的 read方法重新回到 mark方法所作的标记处开始读取数据。
M 脚下留心: mark 只能限制在建立的缓冲区内。
BufferedOutputStream
往 BufferedOutputStream输出和往 OutputStream输出完全一样,只不过 BufferedOutputStream有一个 flush方法用来将缓冲区的数据强制输出完。与缓冲区输入流不同,缓冲区输出流没有增加额外的功能。在 Java中使用输出缓冲也是为了提高性能。它也有两个构造函数:
BufferedOutputStream(OutputStream out)
BufferedOutputStream(OutputStream out,int size)
第一种形式创建一个 32字节的缓冲区,第二种形式以指定的大小来创建缓冲区。
7.4.3 DataInputStream 与 DataOutputStream
这两个类提供了可以 读写各种基本数据类型的数据的各种方法, 这些方法是使用非常简单,在前面我们已经看到了 DataOutputStream类的大部分方法,在 DataInputStream类有与这些 write方法对应的 read方法, 读者可以在 JDK文档帮助查看详细的信息。
DataOutputStream类提供了三种写入字符串的方法,分别是
public final void writeBytes(String s) throws IOException
public final void writeChars(String s) throws IOException
public final void writeUTF(String str) throws IOException
这三种方法有什么区别呢? Java中的字符是 unicode编码,是双字节的, writeBytes只将字符串中的每一个字符的低字节的内容写入目标设备中,而 writeChars将字符串中的每一个字符的两个字节的内容都写入到目标设备中。 writeUTF对字符串按照 UTF格式写入目标设备, UTF是带有长度头的,最开始的两个字节是对字符串进行 UTF编码后的字节长度,然后才是每个字符的 UTF编码。字符的 UTF编码对应下列规则:
Ø 假如字符 c的范围在 /u0001和 /u007f之间,对应的 UTF码占一个字节,内容为 :(byte)c。
Ø 假如字符 c是 /u0000或其范围在 /u0080和 /u07ff之间,对应的 UTF码占两个字节,内容为 :(byte)(0xc0|(0x1f&(c>>6))),(byte)(0x80|(0x3f&c))。
Ø 假如字符 c的范围在 /u0800和 uffff之间,对应的 UTF码占三个字节,内容为 : (byte)(0xe0|(0x0f&(c>>12))), (byte)(0x80|(0x3f &(c>>6))), (byte)(0x80|(0x3f& c ))
在与 DataOutputStream类对应的输入流 DataInputStream类中只提供了一个 readUTF方法返回字符串,也就是 DataInputStream类中没有直接读取到 DataOutputStream类的 writeBytes和 writeChars方法写入的字符串,这又是为什么呢?我们要在一个连续的字节流读取一个字符串(只是流中的一段内容),如果没有特殊的标记作为一个字符串的结尾,而且和我们事先也不知道这个字符串的长度,我们是没法知道读取到什么位置才是这个字符串的结束。在 DataOutputStream类中只有 writeUTF方法向目标设备中写入了字符串的长度,所以,我们也只能准确地读回这个方法写入的字符串。
我们下面的程序使用了多个流对象来进行文件的读写,这多个流对象形成了一个链,我们称之为流栈,如图 7.3所示:
图 7.3
import java.io.*;
public class DataStreamTest
{
public static void main(String[] args)
{
try
{
FileOutputStream fos = new FileOutputStream("hello.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
DataOutputStream dos = new DataOutputStream(bos);
dos.writeUTF("ab 中国 ");
dos.writeBytes("ab 中国 ");
dos.writeChars("ab 中国 ");
dos.close();
FileInputStream fis = new FileInputStream("hello.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
System.out.println(dis.readUTF());
/*byte [] buf=new byte[1024];
int len = dis.read(buf);
System.out.println(new String(buf,0,len));*/
fis.close();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
}
如果正在使用一个流栈,程序关闭最上面的一个流也就自动关闭了栈中的所有底层流,所以程序中只调用了 DataInputStream与 DataOutputStream这两个流对象的 close方法。我们用记事本程序打开 hello.txt文件,显示内容如下
图 7.4
我们能看出其中的大概,就如同作者直接在上面的图中进行标注的那样, writeChars写入的 a字符都占用两个字节。尽管我们在记事本程序中看不出 writeUTF写入的字符串是“ ab中国 ”,但程序通过 readUTF读回后显示在屏幕上的仍是“ ab中国 ”,这个过程就好比一个写入函数把字符串加密后写入文件,我们用记事本程序是看不出其实际写入的内容的,但对应的读取函数却能正确返回先前写入的字符串,因为读取函数内部知道如何解密。 writeChars和 writeBytes方法写入的字符串,我们要想读取回来,就没这么幸运了,读者可以借鉴作者在程序中注释掉的那段代码,运行后没有把我们写入的字符串打印出来,你就能够明白我们要将 writeChars和 writeBytes方法写入的字符串正确读取回来,实在太很难了,所以, io包中专门提供了各种 Reader和 Writer类来操作字符串。
如果读者想仔细研究上面几个 write方法写入的字符串在 hello.txt文件中到底以何种形式存在的,可以使用 UltraEdit打开 hello.txt文件,显示的内容如下:
图 7.5
通过 UltraEdit,我们看到了每个字节所对应的具体数值,所以,有经验的人士经常用 UltraEdit来查看和研究二进制文件的内容信息。
7.4.4 PrintStream
PrintStream类提供了一系列的 print和 println方法,可以实现将基本数据类型的格式化成字符串输出。在前面,我们在程序中大量用到“ System.out.println”语句中的 System.out就是 PrintStream类的一个实例对象,读者已经多次使用到这个类了。 PrintStream有 3个构造函数:
PrintStream(OutputStream out)
PrintStream(OutputStream out,boolean auotflush)
PrintStream(OutputStream out,boolean auotflush , String encoding)
其中 autoflush控制在 Java中遇到换行符 (/n)时是否自动清空缓冲区, encoding是指定编码方式,关于编码方式,我们在本章后面部分有详细的讨论。
println方法与 print方法的区别是:前者会在打印完的内容后再多打印一个换行符 (/n),所以 println()等于 print("/n")。
Java的 PrintStream对象具有多个重载的 print和 println方法,它们可输出各种类型(包括 Object)的数据。对于基本数据类型的数据, print和 println方法会先将它们转换成字符串的形式后再输出,而不是输出原始的字节内容,如:整数 123的打印结果是字符‘ 1’、‘ 2’、‘ 3’所组合成的一个字符串,而不是整数 123在内存中的原始字节数据。对于一个非基本数据类型的对象, print和 println方法会先调用对象的 toString方法,然后再输出 toString方法返回的字符串。
IO包中提供了一个与 PrintStream对应的 PrintWriter类, PrintWriter即使遇到换行符 (/n)也不会自动清空缓冲区,只在设置了 autoflush模式下使用了 println方法后才自动清空缓冲区。 PrintWriter相对 PrintStream最有利的一个地方就是 println方法的行为,在 Windows的文本换行是 "/r/n",而 Linux下的文本换行是 "/n",如果我们希望程序能够生成平台相关的文本换行,而不是在各种平台下都用 "/n"作为文本换行,我们就应该使用 PrintWriter的 println方法时, PrintWriter的 println方法能根据不同的操作系统而生成相应的换行符。
F 指点迷津:
格式化输出是指将一个数据用其字符串格式输出,如我们使用 print 方法把 97 这个整数打印到一个文件中,该方法将把 ‘ 9’ 和 ‘ 7’ 这两个字符的 ASCII 码写入到文件中 , 也就是文件中会被写入两个字节,这两个字节中的数字分别为 57 (十六进制的 0x39 )和 55 (十六进制的 0x37 ) ,在记事本程序中显示为 ‘ 9’ 和 ‘ 7’ 这两个字符。如果我们使用 write 方法把 97 这个整数写到一个文件中,只有一个字节会写入到这个文件中,字节中的数字就是 97 ,正好是字符 ‘ a’ 的 ASCII码,所以在记事本程序中显示为一个字符‘ a’。
7.4.5 ObjectInputStream 与 ObjectOutputStream
这两个类是用于存储和读取对象的输入输出流类,不难想象,我们只要把对象中的所有成员变量都存储起来,就等于保存了这个对象,我们只要读取到一个对象中原来保存的所有成员变量的取值,就等于读取到了一个对象。 ObjectInputStream与 ObjectOutputStream类,可以帮我们完成保存和读取对象成员变量取值的过程,但要读写或存储的对象必须实现了 Serializable接口, Serializable接口中没有定义任何方法,仅仅被用作一种标记,以被编译器作特殊处理。 ObjectInputStream与 ObjectOutputStream类不会保存和读取对象中的 transient和 static类型的成员变量 ,使用 ObjectInputStream与 ObjectOutputStream类保存和读取对象的机制叫序列化 ,如下面定义了一个可以被序列化的 MyClass类:
public class MyClass implements Serializable
{
public transient Thread t;
private String customerID;
private int total;
}
在 MyClass类的实例对象被序列化时,成员变量 t不会被保存和读取。
序列化的好处在于:它可以将任何实现了 Serializable接口的对象转换为连续的字节数据,这些数据以后仍可被还原为原来的对象状态,即使这些数据通过网络传输也没问题。序列化能处理不同操作系统上的差异,我们可以在 Windows上产生某个对象,将它序列化存储,然后通过网络传到 Linux机器上,该对象仍然可以被正确重建出来,在这期间,我们完全不用担心不同机器上的不同的数据表示方式。
下面我们就创建一个学生对象,并把它输出到一个文件( mytext.txt)中,然后再把该对象读出来,将其还原后打印出来:
程序清单: Serializatioan.java
import java.io.*;
public class serialization
{
public static void main(String args[])
throws IOException,ClassNotFoundException
{
Student stu=new Student(19,"dintdding",50,"huaxue");
FileOutputStream fos=new FileOutputStream("mytext.txt");
ObjectOutputStream os=new ObjectOutputStream(fos);
try
{
os.writeObject(stu);
os.close();
}catch(IOException e)
{
System.out.println(e.getMessage());
}
stu=null;
FileInputStream fi=new FileInputStream("mytext.txt");
ObjectInputStream si=new ObjectInputStream(fi);
try
{
stu=(Student)si.readObject();
si.close();
}catch(IOException e)
{
System.out.println(e.getMessage());
}
System.out.println("ID is:"+stu.id);
System.out.println("name is:"+stu.name);
System.out.println("age is:"+stu.age);
System.out.println("department is:"+stu.department);
}
}
class Student implements Serializable
{
int id;
String name;
int age;
String department;
public Student(int id,String name,int age,String department)
{
this.id=id;
this.name=name;
this.age=age;
this.department=department;
}
}
运行结果:
ID is:19
name is:dintdding
age is:50
department is:huaxue
从运行结果上看,我们刚刚读出来并还原的内容和我们原来创建时是一样的。我们到底写了些什么内容到 mytext.txt文件中呢?我们用记事本程序打开 mytext.txt文件时所看到的内容如图 7.6所示:
图 7.6
我们不用了解其中的详细细节,只要能够通过相应的方式正确地读取回来就足够了。
F 指点迷津:
一个学员曾经问过我,他们公司买了一套美国人的地理信息系统,这个系统将采集到的地理数据存放在一个文件中,他有没有办法读取到这个文件中的内容?看来,他还没有完全明白这些 IO 类能帮助我们做些什么。我告诉他,用我们的前面讲的 FileInputStream 类就能够读取到这个文件中的所有字节的数据,只是我们不明白这些数据代表的是什么意思罢了,也就是说我们不知道美国人存储数据的格式,读到了这些数据也是白读!只有开发那个系统的美国人自己知道这些数据的意义,他们才能正确地使用文件中保存的数据。就象 ObjectOutputStream 保存的数据一样,是专门给 ObjectInputStream 来读取的,我们通过别的方式读取到的数据毫无意义。
7.4.6 字节 流与字符流的转换
前面我们讲过, Java支持字节流和字符流,我们有时需要字节流和字符流之间的转换。
InputStreamReader 和 OutputStreamWriter
这两个类是字节流和字符流之间转换的类, InputStreamReader可以将一个字节流中的字节解码成字符, OuputStreamWriter将写入的字符编码成字节后写入一个字节流。其中 InputStreamReader有两个主要的构造函数:
InputStreamReader(InputStream in) //用默认字符集创建一个 InputStreamReader对象
InputStreamReader(InputStream in,String CharsetName) //接受以指定字符集名的字符串,并用 //该字符集创建对象
OutputStreamWriter也有对应的两个主要的构造函数:
OutputStreamWriter(OutputStream in) //用默认字符集创建一个 OutputStreamWriter对象
OutputStreamWriter(OutputStream in,String CharsetName) //接受以指定字符集名的字符串,
//并用该字符集创建 OutputStreamWriter对象
为了达到最好的效率,避免频繁的字符与字节间的相互转换,我们最好不要直接使用这两个类来进行读写,应尽量使用 BufferedWriter类 包装 OutputStreamWriter类,用 BufferedReader类包装 InputStreamReader。例如:
BufferedWriter out=new BufferedWriter(newOutputStreamWriter(System.out));
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
我们接着从一个更实际的应用中来熟悉 InputStreamReader的作用,怎样用一种简单的方式一下就读取到键盘上输入的一整行字符?只要用下面的两行程序代码就可以解决这个问题:
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
String strLine = in.readLine();
我们不可能什么时候都提前掌握了正好可以解决我们问题的各个小知识点,作者在第一次碰到这种需求时,就不知道可以用这种方式,但作者在以前从没有接触的情况下,也写出了上面的代码。首先,要读取一行,我马上想到在 chm格式的 JDK文档中去查类似 readLine这样的英文单词的拼写组合,查询的界面如图 7.7所示:
图
7.7
我们找到了 BufferedReader这个类,查看 BufferedReader类的构造方法,如图 7.8所示:
图
7.8
可见,构建 BufferedReader对象时,必须传递一个 Reader类型的对象作为参数,而键盘对应的 System.in是一个 InputStream类型的对象,解决问题的关键是,我们还需要找到将 InputStream类型的流对象包装成 Reader类型的包装类。作者眼尖(其实是作者读文档的一个习惯,也可以说是作者查文档的一点小经验吧),就在 BufferedReader的帮助界面中,我看了如下的一段信息:
See Also:
FileReader, InputStreamReader
在这里,我看到了 InputStreamReader这个关键的类,阅读其帮助后,最终写出了上面的程序代码,轻松解决了我从未碰到过的问题。在查阅文档时,经常顺便看看 See Also部分也是很重要的,在那里往往都有解决相关问题的超链接。如果在 See Also部分也没有提及 InpustStreamReader,那我们只能去查 IO包的帮助,浏览其中列出的每个类,也能发现 InpustStreamReader这个类就是我们所要找的类的。
BufferedReader类可以读取一行文本,对应的 BufferedWriter类也提供了一个 newLine方法来向字符流中写入不同操作系统下的换行符,如果我们要向字符流中写入与平台相关的文本换行,就可以考虑使用 BufferedWriter这个包装类了。
我们在前面用到的 FileWriter和 FileReader实际上都是包装类, FileReader是 InputStreamReader的子类, FileWriter是 OutputStreamWriter的子类。
7.4.7 IO 包中的类层次关系图
1. 字节输入流类:
图 7.9
2. 字节输出流类:
图 7.10
3. 字符输入流类:
图 7.11
4. 字符输出流类:
图 7.12
7.5 IO 中的高级应用
7.5.1 字符集的编码问题
计算机里只有数字,我们在计算机软件里的一切都是用数字来表示,屏幕上显示的一个个字符也不例外,最初的计算机的使用是在美国,当时所用到的字符也就是我们现在键盘上的一些符号和少数几个特殊的符号,每一个字符都用一个数字来表示,一个字节所能表示的数字范围内足以容纳所有的这些字符,实际上表示这些字符的数字的字节最高位 (bit) 都为 0,也就是说这些数字都在 0到 127之间,如字符 a对应数字 97,字符 b对应数字 98等,这种字符与数字对应的编码固定下来后,这套编码规则被称为 ASCII(美国标准信息交换码)。
随着计算机逐渐在其他国家的应用和普及,许多国家都把本地的字符集引入了计算机,大大扩展了计算机中字符的范围。一个字节所能表示的数字范围是不能容纳所有的中文汉字的,中国大陆将每一个中文字符都用两个字节的数字来表示,原有的 ASCII字符的编码保持不变,仍用一个字节表示,为了将一个中文字符与两个 ASCII码字符相区别,中文字符的每个字节的最高位 (bit) 都为 1,中国大陆为每一个中文字符都指定了一个对应的数字,并作为标准的编码固定下来,这套编码规则称为 gbk(国标码) ,后来又在 gbk的基础上对更多的中文字符(包括繁体)进行了编码,新的编码系统就是 gb2312,可见 gbk是 gb2312的子集。使用中文的国家和地区很多,同样的一个字符,如“中国”的“中”字,在中国大陆的编码是十六进制的 D6D0,而在中国台湾的编码是十六进制的 A4A4,台湾地区对中文字符集的编码规则称为 big5(大五码)。
在一个国家的本地化系统中出现的一个字符,通过电子邮件传送到另外一个国家的本地化系统中,看到的就不是那个原始字符了,而是另外那个国家的一个字符或乱码,因为计算机里面并没有真正的字符,字符都是以数字的形式存在的,我们通过邮件传送一个字符,实际上传送的是这个字符对应的编码数字,同一个数字在不同的国家和地区代表的很可能是不同的符号,如十六进制的 D6D0在中国大陆的本地化系统中显示为“中”这个符号,但在伊拉克的本地化系统就不知对应的是一个什么样的伊拉克字符了,反正人们看到的不是“中”这个符号。随着世界各国的交往越来越密切,全球一体化的趋势越来越明显,人们不可能完全忘记母语,都去使用英文在不同的国家和地区间交换越来越多的电子文档,特别是人们开发的应用软件都希望能走出国门、走向世界,可见,使用各个国家和地区的本地化字符编码,已经给我们的生活和工作带来了很多的不方便,严重制约了国家和地区间在计算机使用和技术方面的交流。
为了解决各个国家和地区使用本地化字符编码带来的不利影响,人们将全世界所有的符号进行了统一编码,称之为 unicode编码,所有字符不再区分国家和地区,都是人类共有的符号,如“中国”的“中”这个符号,在全世界的任何角落始终对应的都是一个十六进制的数字 4e2d,如果所有的计算机系统都使用这种编码方式,在中国大陆的本地化系统中显示的“中”这个符号,发送到伊拉克的本地化系统中,显示的仍然是“中”这个符号,至于那个伊拉克能不能认识这个符号,就不是我们计算机所要解决的问题了。 Unicode编码的字符都占用两个字节的大小,也就是说全世界所有的字符个数不会超过 2的 16次方 (65536),我想一定是 unicode编码中没有包括诸如中国的藏文和满文这些少数民族的文字。
长期养成的保守习惯不可能一下子就改变过来,特别是不可能完全推翻那些已经存在的运行良好的系统,新开发的软件要做到瞻前顾后,既能够在存在的系统上运行,又便于以后的战略扩张和适应新的形式。 unicode一统天下的局面暂时还难以形成,在相当长的一段时期内,人们看到的都是本地化字符编码与 unicode编码共存的景象。既然本地化字符编码与 unicode编码共存,那就少不了涉及两者之间的转化问题,在 Java中的字符使用的都是 unicode编码, Java技术在通过 Unicode保证跨平台特性的前提下也支持了全扩展的本地平台字符集,而我们显示输出和键盘输入都是采用的本地编码。
作者在上面的讲解中,写出了“中国”的“中”字在 gbk, big5,unicode编码中分别对应的数字,读者是否对此感到奇怪过,是作者记忆力超群吗?非也!作者就是通过下面的实验而得到的这几个数字并借此帮助读者完全理解字符编码的问题。
步骤 1 , 在 UtralEdit中,输入“中国”,再按下工具栏上的“ H”样的按钮,用十六进制方式查看“中国”这两个字符在本地系统编码中所对应的字节数字,如图 7.13所示:
图 7.13
步骤 2 ,编写并运行下面的程序代码
public class CharCode
{
public static void main(String [] args) throws Exception
{
String strChina = " 中国 ";
for(int i=0;i
{
System.out.println(Integer.toHexString((int)strChina.charAt(i)));
}
byte [] buf=strChina.getBytes("gb2312");
for(int i=0;i
{
System.out.println(Integer.toHexString(buf[i]));
}
System.out.println(strChina);
for(int i=0;i
{
System.out.write(buf[i]);
}
System.out.println();// 试试没有这一句的效果
}
}
运行的结果如下:
4e2d
56fd
ffffffd6
ffffffd0
ffffffb9
fffffffa
中国
中国
Java中的字符采用的是 unicode编码,每个字符都占用两个字节,我们直接把每个字符中的内容对应着的整数打印出来,显示的结果就是这个字符的 unicode码。 String类中的 getBytes方法,并不是简单地将字符串中的每个字节数据存放到一个字节数组中去,而是将 unicode码的字符串中的每个字符数字,转换成该字符在指定的字符集下的数字,最后将这些数字存放到一个字节数组中返回。将一个字符的 unicode码转换成某种本地字符集码的过程叫编码,将 unicode码成功地转换到本地字符集码,在 JDK包中必须有对应的字符集编码器类。
打印出编码转换后的每个字节,我们就可以看到字符在该字符集下的编码。由于我们要将字节转换成整数后才能以十六进制的形式打印出来,从程序打印的结果上,我们看到 d6以 ffffffd6的形式打印出来,这说明如果字节的最高位 (bit)为 1,转换后的整数的三个高字节的每个 bit位的内容也都是 1,如果大家明白在计算机中是如何表示负数的,就不难想明白其中的道理了 ,如程序中打印的 ffffffd6所对应的字节在内存中实际上是一个字节的数据 d6。我们只要取打印结果的最低字节的数字,就是我们原始字节中的数字,将这些数字与我们在 步骤 1看到的数字对比,我们就看到了 getBytes成功地将 unicode码转换成了 gb2312码。
如果程序中没有最后的 System.out.println语句,屏幕上不会打印出最后的“中国”,这是为什么?前面讲过, System.out是包装类 PrintStream的一个实例对象,包装类都是有缓冲的, PrintStream类在调用了 println方法后会自动刷新缓冲区的内容。从最后屏幕上正常打印出“中国”这两个字符,我们又可以得到这么一个结论:要正确地在屏幕上打印中文字符,我们写入屏幕输出流的字节内容必须是该中文字符的 gb2312码,要将中文字符正确的存入硬盘文件也是一样的道理。我们还可以进而推断: System.out.println("中国 ")中的 println方法实际上是先把 "中国 "转换成其 gb2312码的字节数组,然后调用 write方法将这个字节数组写入到输出流中。那么, println方法怎么知道要将 "中国 "转换成 gb2312码,而不是 big5码或其他的字符集呢?前面讲过,在创建 PrintStream实例对象的构造方法中可以指定一个编码参数,在我们使用的中文版的 Windows操作系统上, System.out对象就是 Java系统按照 gb2312编码方式创建出来的 PrintStream实例对象,这是一个非常底层的问题,有兴趣的读者可以去研究 JDK中的源码。
步骤 3 , String类中有一个没有参数的 getBytes方法, 它将使用系统缺省的编码器对字符编码,我们将上面程序中的
byte [] buf=strChina.getBytes("gb2312");
修改为
byte [] buf=strChina.getBytes();
重新编译后运行的结果和修改前的结果一样,这说明我们系统的 缺省编码方式就是 gb2312。在程序的开始处,我们增加下面的一条语句:
System.getProperties().list(System.out);
程序运行的结果如图 7.14所示:
图 7.14
如图中的箭头所指,作者所用系统的缺省编码方式为 gbk,也就是 gb2312。
接着,我们修改系统的缺省编码方式 ,修改后的程序代码如下:
public class CharCode
{
public static void main(String [] args) throws Exception
{
System.getProperties().put("file.encoding","iso8859-1");
System.getProperties().list(System.out);
String strChina = " 中国 ";
for(int i=0;i
{
System.out.println(Integer.toHexString((int)strChina.charAt(i)));
}
byte [] buf=strChina.getBytes();
for(int i=0;i
{
System.out.println(Integer.toHexString(buf[i]));
}
System.out.println(strChina);
for(int i=0;i
{
System.out.write(buf[i]);
}
System.out.println();// 试试没有这一句的效果
}
}
重新运行后,程序最后在屏幕上打印的几行如下:
……
4e2d
56fd
3f
3f
中国
??
我们成功地将系统的缺省编码方式修改成了 iso8859-1, iso8859-1标准的西方英语国家字符集码,在 iso8859-1字符集中是没有“中国”这样的字符的,编码的结果当然也就没有意义了,打印的结果也就不正常了。
步骤 4 , 我们将键盘字节输入流中的每个字节读取到一个字节数组中,然后将字节数组中的数据当作某种本地字符集码转换成 unicode码的字符串,这个过程叫解码,要能够正确地完成解码工作,在 JDK包中必须有对应的字符集解码器类。实验并观察下面的程序代码的运行结果。
import java.io.*;
public class CharDecoder
{
public static void main(String [] args) throws Exception
{
System.out.println("please enter a Chinese String : ");
byte [] buf=new byte[1024];
int ch=0;
int pos=0;
String strInfo=null;
while(true)
{
ch =System.in.read();
System.out.println(Integer.toHexString(ch));
switch(ch)
{
case '/r':
break;
case '/n':
strInfo= new String(buf,0,pos,"gb2312");
for(int i=0;i
{
System.out.println(Integer.toHexString((int)strInfo.charAt(i)));
}
System.out.println(strInfo);
for(int i=0;i
System.out.write(buf[i]);
System.out.println();// 想想为什么要这一句
return;
default:
buf[pos++]=(byte)ch;
}
}
}
}
编译运行后的效果如下:
please enter a Chinese String:
中国
d6
d0
b9
fa
d
a
4e2d
56fd
中国
中国
从运行的结果上,我们可以看出通过键盘输入的中文字符,在键盘输入流中的字节数据是这个字符的 gb2312码对应的数字。另外,上面打印出的 d和 a分别是字符 '/r'和 '/n'的十六进制数据。
如果 String类的构造函数中没有指定解码方式, 它将使用系统缺省的解码器将字节数组中的数据解码成 unicode码的字符串,我们将上面程序中的
strInfo= new String(buf,0,pos,"gb2312");
修改为
strInfo= new String(buf,0,pos);
重新编译后运行的结果和修改前的结果一样,这说明我们系统的 缺省解码方式就是 gb2312。
步骤 5 ,我们将 系统的缺省解码方式修改成了 iso8859-1或在 String类的构造函数中明确指定用 iso8859-1解码,来观察程序运行的结果,修改后的程序代码如下:
import java.io.*;
public class CharDecoder
{
public static void main(String [] args) throws Exception
{
System.getProperties().put("file.encoding","iso8859-1");
System.out.println("please enter a Chinese String");
byte [] buf=new byte[1024];
int ch=0;
int pos=0;
String strInfo=null;
while(true)
{
ch =System.in.read();
System.out.println(Integer.toHexString(ch));
switch(ch)
{
case '/r':
break;
case '/n':
strInfo= new String(buf,0,pos);
for(int i=0;i
{
System.out.println(Integer.toHexString((int)strInfo.charAt(i)));
}
System.out.println(strInfo);
for(int i=0;i
System.out.write(buf[i]);
System.out.println();// 想想为什么要这一句
return;
default:
buf[pos++]=(byte)ch;
}
}
}
}
编译运行后打印的结果如下:
please enter a Chinese String
中国
d6
d0
b9
fa
d
a
d6
d0
b9
fa
???ú
中国
可见,装有“中国”这两个字符的 gb2312码的字节数组,使用 iso8859-1解码后的 unicode字符串中的字符并不是“中国”这两个字符的 unicode码,而是被解码成了四个字符,每个字符的低字节的内容都是原来字节数组中的数据,而高字节都是 0,如程序中打印的 d6所对应的字符在内存中实际上是两个字节 00d6。
步骤 6 ,我们在实际的开发中,经常会遇到诸如 步骤 5 中出现的 字符编码问题,假设别人给我们提供的某个方法返回的字符串是用的 iso8859-1解码而成的,在遇到中文也会出现程序中 System.out.println(strInfo);打印出的是乱码的问题,就拿上面的程序来说,我们有没有办法修改程序中的打印语句,让其能打印出正确的中文字符呢?要注意到我们的前提条件:返回字符串的函数是别人提供的,我们不能修改生成字符串的那部分程序代码。字符串用的是 iso8859-1解码而成的,我们只要将其按 iso8859-1又编码成字节数组,字节数组中的内容就是中文字符最初的那个字符集 (这里就以中国大陆地区的 gb2312为例 )的编码值,然后,我们对这个字节数组再按 gb2312解码成 unicode的字符串就行了。
我们只要将
System.out.println(strInfo);
修改成
String strChina = new String(strInfo.getBytes("gb2312"),"iso8859-1");
System.out.println(strChina);
打印中文的结果就正常了。
作者从上面的实验结果还得到了另外一些启发:先将字节数组解码成字符串,以后还可以将这个字符串又反向编码成最初的字符数组;但先将一个字符串的内容先编码成字节数组,却不一定能够反向解码成最初的字符串。其实细心的读者在 步骤 3中,就已经 看到了 把“中国”这个字符串按 iso8859-1编码成字节数组后打印出的数据 ,不可能反向解码回去的。因为一个字符占两个字节,字符串中的原来两个字节的内容 按 iso8859-1 编码后只有一个字节,两个字节能表示 65536个数字,而一个字节只能表示 256个数值,这个编码过程显然不可能一一对应,会有数据的丢失。
F 指点迷津:
字符编码在很多人眼里是一个复杂的问题,很多“高手”也尽量避开与人交谈这个问题。如果你掌握了本节的内容,在遇到字符编码问题时,按照本节所演示的一些手段和程序代码,一定能分析出问题的原因的。例如有学员问我,他的 Java 程序从 oracle 数据库中读取到的中文显示为乱码,他通过 Java 程序写入 oracle 数据库的中文也为乱码,该怎么解决这个问题呢?虽然我以前没有碰到这样的问题,但我可以帮他分析问题的原因,首先用 oracle 自带的管理工具在数据库中插入“中国”这两个字符,然后用 Java 程序读出这个字符串,按前面所讲的方法打印出每个字节的内容,与我们这节程序打印出的结果对比,看看这个字符串是按什么编码生成的,最后将这个字符串转换成 gb2312 编码。 oracle 数据库的中文写入问题,用相反的方式去做实验就可以解决了。
字节用于表示计算机内存中最原始的数据,不会涉及到编码问题,只有把字节中的内容当做字符来处理时,才会涉及编码问题,所以 InputStreamReader, InputStreamWriter,PrintStream,String中都有一个可以指定字符集编码参数的构造函数,而 Inputstream的构造函数中则不存在这样的参数。
缺省的情况下 ,如果你构造与流相连的 Reader 和 Writer,字节和字符之间的转换规则使用缺省的平台字符编码和 Unicode,比如在英语国家字节码是用 ISO 8859-1,用户也可以指定编码格式,具体的格式参考 Sun公司的 JDK文档首页中的 Internationalization超链接部分,在 JDK文档首页中,你还能看到有关 java的各种特性讲解的超链接,有空进去看看,一定会让你受益匪浅。如图 7.15所示。
图 7.15
其中 Java 2 Platform API Specification部分就是 JDK中提供的各种类的帮助文档, Java程序员在实际编码的过程中会经常来查阅这一部分,但作者现在使用 F.Allimant整理成的 chm格式的文档,也就是我们前面多次使用的那个帮助系统,会更方便,更有效。
我们再来看看为 InputStreamReader指定其他字符集参数,也就是用 iso8859-1替代缺省的 gb2312字符集,分析观察程序的运行结果,来作为对本节讲解的结束和检查读者对本节内容的掌握情况。程序代码如下:
import java.io.*;
public class InputReader
{
public static void main(String [] args) throws Exception
{
InputStreamReader isr=
new InputStreamReader(System.in, "iso8859-1");
BufferedReader br = new BufferedReader(isr);
String strLine=br.readLine();
for(int i=0;i
{
System.out.println(Integer.toHexString((int)strLine.charAt(i)));
}
isr.close();
System.out.println(strLine);
}
}
输入“中国”后,程序的运行结果如下:
中国
d6
d0
b9
fa
???ú
为了达到一种体验的效果,读者最好自己修改程序代码,让程序能够正常打印出输入中文字符。作者提前给出答案,照顾一下学得还不是很好的读者,读者可以用下面两种方式中的任意一种。
一 . 将
InputStreamReader isr =new InputStreamReader(System.in,"iso8859-1");
修改成
InputStreamReader isr =new InputStreamReader(System.in,"gb2312");
在中国大陆的计算机系统上一般都是简体中文版的操作系统,缺省字符集为 gb2312,所以,我们也可以不指定字符集参数。
InputStreamReader isr =new InputStreamReader(System.in);
二 .不修改上面的部分,而是将
System.out.println(strLine);
修改成
System.out.println(new String(strLine.getBytes("iso8859-1"),"gb2312"));
广东地区的台商和港商的工厂较多,他们使用的系统的缺省字符集就不一定是 gb2312,使用这样系统的读者就不能照搬照套我本节的内容了。
7.5.2 Decorator 设计模式
通过包装类,就可以用一个对象 (the Decorators)包装另外的一个对象,比如 : 可以用 BufferedReader来包装一个 FileReader, FileReader仅仅提供了底层的读操作,比如 read(char[] buffer)。 BufferedReader实现了一个更高层次上的操作 ,比如 readLine,完成读取文件中的一个行。其实,这是一种被称着 Decorator的设计模式。
我们要设计自己的 IO包装类,需要继承 FilterXXX命名的 类 ,从而扩展了对输入输出流的支持。比如我们设计一对类包装类: RecordInputStream和 RecordOutputStream,来 完成从数据库中读取记录和往数据库中写入记录。以后程序中就可以用它们来包装 InputStream 和 OutputStream,从而完成对数据库的操作。
包装类的使用非常灵活,我们来看看一个巧妙使用包装类的例子,从而借鉴一些思想。 Exception类从 Throwable类继承的三个 printStackTrace方法的定义如下:
public void printStackTrace()
public void printStackTrace(PrintStream s)
public void printStackTrace(PrintWriter s)
它们分别用把异常的详细信息打印到标准输出流(屏幕上),或者其他的 PrintStream和 PrintWriter流中。有时候,我们的应用需要把异常的详细信息放到一个字符串中,然后将这个包含异常详细信息的字符串通过网络发送出去,该怎么实现呢?我们先看看程序代码:
import java.io.*;
public class TestPrintWriter
{
public static void main(String [] args)
{
try
{
throw new Exception("test");
}
catch(Exception e)
{
StringWriter sw=new StringWriter();
e.printStackTrace(new PrintWriter(sw));
String strException = sw.toString();
System.out.println(strException);
}
}
}
在上面的程序中,用 System.out.println将字符串打印在屏幕上来简单模拟通过网络将这个字符串发送出去。 printStackTrace方法只能将异常详细信息写入一个 PrintWriter对象中,写入 PrintWriter对象的数据实际上会写入它所包装的一个 Writer对象中,而写入 StringWriter对象(一种 Writer对象)的内容可以当作字符串取出来,所以用 PrintWriter对象去包装一个 StringWriter对象就可以解决我们的需求。解决问题的关键在于,我们如何能想到将这些流有机地串联起来。
7.5.3 Java 虚拟机 读写其他进程的数据
我们在 Java程序中可以产生其他的应用程序的进程,在 Java程序中启动的进程称为子进程,启动子进程的 Java程序称为父进程。子进程没有键盘和显示器,子进程的标准输入和输出不再连接到键盘和显示器,而是以管道流的形式连接到父进程的一个输出流和输入流对象上,调用 Process类的 getOutputStream和 getInputStream方法可以得到这个输出流和输入流对象。子进程从标准输入读取到的内容是父进程通过输出流对象写入管道的数据,子进程写入标准输出的数据通过管道传送到了父进程的输入流对象中,父进程从这个输入流对象中读取到的内容就是子进程写入到标准输出的数据。
如 javac.exe和 java.exe这两个文件本身都是应用程序,我们在 Java程序中也可以启动它们。请看下面的例子:
程序清单 : TestInOut.java
import java.io.*;
public class TestInOut implements Runnable
{
Process p=null;
public TestInOut()
{
try
{
p=Runtime.getRuntime().exec("java MyTest");
new Thread(this).start();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
public void send()
{
try
{
OutputStream ops=p.getOutputStream();
while(true)
{
ops.write("help/r/n".getBytes());
}
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
public static void main(String [] args)
{
TestInOut tio=new TestInOut ();
tio.send();
}
public void run()
{
try
{
InputStream in = p.getInputStream();
BufferedReader bfr=new BufferedReader(
new InputStreamReader(in));
while(true)
{
String strLine=bfr.readLine();
System.out.println(strLine);
}
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
}
class MyTest
{
public static void main(String [] args) throws IOException
{
while(true)
{
System.out.println("hi:"+
new BufferedReader(new InputStreamReader(System.in)).readLine());
}
}
}
为了方便,我们将类 MyTest与类 TestInOut 放在同一个源文件中编译, 运行后的结果如下:
......
hi:help
hi:help
hi:lp
hi:
hi:help
hi:lp
hi:
hi:help
......
可见, TestInOut类中通过子进程的 Process对象获得的输出和输入流对象,分别充当了 MyTest类的“键盘”( System.in)和“显示器”( System.out)。从运行的结果上,我们还看到由数据丢失的情况发生,这是因为管道是有一定大小的 ,这个大小其实就是 PipedInputStream类中的缓冲区的大小,如果缓冲区满后,程序还没及时读取数据,就会发生数据丢失。我们对 MyTest类的程序代码进行修改,提高其运行效率。
class MyTest
{
public static void main(String [] args) throws IOException
{
BufferedReader bfr=new BufferedReader(
new InputStreamReader(System.in));
while(true)
{
System.out.println("hi:"+bfr.readLine());
}
}
}
重新编译运行后的结果如下:
hi:help
hi:help
hi:help
hi:help
hi:help
可见,在不同的地方定义变量和创建对象,用不同的代码顺序和结构,程序的运行效率大不一样。
& 多学两招:
其实,我们在很多细微之处都可以提高程序的运行效率的,如
for(int i=0;i
的效率就不如改写成
int len=strlength();for(int i=0;i
的效率高,因为,上面的语句在每次循环事都要调用 str.length() 方法,而下面的语句只调用了一次。
我们在程序中还会经常碰到类似下面的应用,
byte [] buf= new byte[1024];while(true) { 对 buf 元素的操作语句 }
while(true){ byte [] buf= new byte[1024]; 对 buf 元素的操作语句 }
上面程序结构比下面的程序结构效率高,因为 buf 数组只被产生了一次。
在上面的程序中,我们没有考虑程序的退出方式,就只有用 ctrl+c强制进程的结束这一招了。读者将这个程序多运行和退出几次,你会发现你的计算机慢如蜗牛了,查看 Windows进程信息,如图 7.16所示:
图 7.16
我们在进程列表中看到了多个 java.exe程序,这说明我们每次启动的子进程仍在运行。在实际应用中,我们编写的程序应考虑退出方式,并调用 Process类的 destroy方法结束子进程的运行。
其实,我们所用的 JCreator和 JBuilder这样的集成开发环境,都是调用 JDK中的 javac.exe和 java.exe来编译和运行我们编写 Java程序的,它们把 javac.exe和 java.exe做成了它们的一个子进程,并通过自己的图形界面中对子进程进行输入和显示其输出。
第 7 章 IO/ 输入输出 ........................................................................................ 203
7.1 File 类 ...................................................................................................... 203
7.2 RandomAccessFile 类 ................................................................................. 205
7.3 节点流 ..................................................................................................... 207
7.3.1 理解流的概念 ................................................................................. 207
7.3.2 InputStream 与 OutputStream............................................................ 208
指点迷津: 1. 如何选择输入与输出
2. 为什么要调用 close 方法
多学两招: IO 中的缓冲区
7.3.3 FileInputStream 与 FileOutputStream................................................. 210
7.3.4 Reader 与 Writer.............................................................................. 211
独家见解 :隐含的缓冲区
7.3.5 PipedInputStream 与 PipedOutputStream............................................ 212
独家见解:管道流类的作用
7.3.6 ByteArrayInputStream 与 ByteArrayOutputStream.............................. 214
7.3.7 IO 程序代码的复用 ......................................................................... 216
7.4 过滤流与包装类 ....................................................................................... 218
7.4.1 理解包装类的概念与作用 ............................................................... 218
7.4.2 BufferedInputStream 与 BufferedOuputStream................................... 219
脚下留心:使用 mark 时应考虑的问题
7.4.3 DataInputStream 与 DataOutputStream.............................................. 219
7.4.4 PrintStream..................................................................................... 222
指点迷津:何谓格式化输出
7.4.5 ObjectInputStream 与 ObjectOutputStream......................................... 223
指点迷津:文件中的数据可读但不见得可用
7.4.6 字节 流与字符流的转换 ................................................................... 225
7.4.7 IO 包中的类层次关系图 .................................................................. 227
7.5 IO 中的高级应用 ...................................................................................... 228
7.5.1 字符集的编码问题 .......................................................................... 228
指点迷津:如何处理字符乱码问题
7.5.2 Decorator 设计模式 ......................................................................... 238
7.5.3 Java 虚拟机 读写其他进程的数据 ..................................................... 239
多学两招:提高程序的运行效率