`
mywebcode
  • 浏览: 997835 次
文章分类
社区版块
存档分类
最新评论

IO/输入输出

 
阅读更多

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("Abspath:"+f.getAbsolutePath());

System.out.println("Parent:"+f.getParent());

System.out.println(f.exists()?"exists":"doesnot exist");

System.out.println(f.canWrite()?"iswriteable":"

is notwriteable");

System.out.println(f.canRead()?"isreadable":"is not readable");

System.out.println(f.isDirectory()?"is ":"isnot"+" a directory");

System.out.println(f.isFile()?"is normalfile":"might be a named pipe");

System.out.println(f.isAbsolute()?"isabsolute":"

is notabsolute");

System.out.println("File lastmodified:"+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 lastmodified: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类对象的构造方式:

newRandomAccessFile(f,"rw"); //读写方式

newRandomAccessFile(f,"r"); //只读方式

注:当我们的程序需要以读写的方式打开一个文件时,如果这个文件不存在,程序会为你创建它。

我们还需要设计一个类来封装员工信息。一个员工信息就是文件中的一条记录,我们必须保证每条记录在文件中的大小相同,也就是每个员工的姓名字段在文件中的长度是一样的,我们才能够准确定位每条记录在文件中的具体位置。假设name中有八个字符,少于八个则补空格(这里我们用"\u0000"),多于八个则去掉后面多余的部分。由于年龄是整型数,不管这个数有多大,只要它不超过整型数的范围,在内存中都是占4个字节大小。

程序清单:RandomFileTest.java

import java.io.*;

public class RandomFileTest

{

public static void main(String []args) throws Exception

{

Employee e1 = newEmployee("zhangsan",23);

Employee e2 = newEmployee("Lisi",24);

Employee e3 = newEmployee("Wangwu",25);

RandomAccessFile ra=newRandomAccessFile("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=newRandomAccessFile("c:\\1.txt","r");

int len=8;

raf.skipBytes(12); //跳过第一个员工的信息,其中姓名8字节,年龄4字节

System.out.println("第二个员工信息:");

String str="";

for(int i=0;i<len;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<len;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<len;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(Stringname,int age)

{

if(name.length()>LEN)

{

name =name.substring(0,8);

}

else

{

while(name.length()<LEN)

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(intbeginIndex,intendIndex)方法可以用于取出一个字符串中的部分子字符串,要注意的一个细节是:子字符串中的第一个字符对应的是原字符串中的脚标为beginIndex处的字符,但最后的字符对应的是原字符串中的脚标为endIndex-1处的字符,而不是endIndex处的字符。在实际生活中,我们常用的数据库和数据库管理工具实际上就是这种原理。我们的1.txt就相当于数据库的数据文件,而我们这个程序提供了往这个数据文件写入和读取数据的功能。

7.3 节点流

7.3.1 理解流的概念

数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。

我们将IO流类分为两个大类,节点流类和过滤流类(也叫处理流类)。程序用于直接操作目标设备所对应的类叫节点流类,程序也可以通过一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据,这个间接流类就是过滤流类(也叫处理流类),我更喜欢称之为包装类。不管叫什么,都只是一个代名词而已,读者不要太在意,你可以根据自己的习惯和喜好来定。

7.3.2InputStream与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.3FileInputStream与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 classFileStream

{

public static void main(String[] args)

{

File f = new File("hello.txt");

try

{

FileOutputStream out = newFileOutputStream(f);

byte buf[]="www.it315.org".getBytes();

out.write(buf);

out.close();

}

catch(Exception e)

{

System.out.println(e.getMessage());

}

try

{

FileInputStream in = newFileInputStream(f);

byte [] buf = new byte[1024];

int len = in.read(buf);

System.out.println(newString(buf,0,len));

}

catch(Exception e)

{

System.out.println(e.getMessage());

}

}

}

编译运行上面的程序,我们能够看到当前目录下产生了一个hello.txt的文件,用记事本程序打开这个文件,能看到我们写入的内容。随后,程序开始读取文件中的内容,并将读取到的内容打印出来。在这个例子中,我们演示了怎样用FileOutputStream往一个文件中写东西和怎样用FileInputStream从一个文件中将内容读出来。有一点不足的是,这两个类都只提供了对字节或字节数组进行读取的方法,对于字符串的读写,我们还需要进行额外的转换。

7.3.4Reader与Writer

Java中的字符是unicode编码,是双字节的,而InputStream与OutputStream是用来处理字节的,在处理字符文本时不太方便,需要编写额外的程序代码。Java为字符文本的输入输出专门提供了一套单独的类,Reader、Writer两个抽象类与InputStream、OutputStream两个类相对应,同样,Reader、Writer下面也有许多子类,对具体IO设备进行字符输入输出,如FileReader就是用来读取文件流中的字符。

对于Reader和Writer,我们就不过多的说明了,大体的功能和InputStream、OutputStream两个类相同,但并不是它们的代替者,只是在处理字符串时简化了我们的编程。我们上面的程序改为使用FileWriter和FileReader来实现,修改后的程序代码如下:

import java.io.*;

public classFileStream

{

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(newString(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.5PipedInputStream与PipedOutputStream

一个PipedInputStream对象必须和一个PipedOutputStream对象进行连接而产生一个通信管道,PipedOutStream可以向管道中写入数据,PipedInputStream可以从管道中读取PipedOutputStream写入的数据。这两个类主要用来完成线程之间的通信,一个线程的PipedInputStream 对象能够从另外一个线程的PipedOutputStream对象中读取数据。请看下面的例子:

程序清单:PipeStreamTest.java
import java.io.*;

public class PipeStreamTest

{

public static void main(Stringargs[])

{

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 PipedOutputStreamout=new PipedOutputStream();

public PipedOutputStream getOutputStream()

{

return out;

}

public void run()

{

String s=newString("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=newPipedInputStream();

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 fromsender:\n"+s);

in.close();

}

catch(IOException e)

{

System.out.println(e.getMessage());

}

}

}

运行结果:

the followingmessage comes from sender:

hello,receiver,how are you

JDK还提供了PipedWriter和PipedReader这两个类来用于字符文本的管道通信,读者掌握了PipedOutputStream和PipedInputStream类,自然也就知道如何使用PipedWriter和PipedReader这两个类了。

$独家见解:

使用管道流类,可以实现各个程序模块之间的松耦合通信,我们可以灵活地将多个这样的模块的输出流与输入流相连接,以拼装成满足各种应用的程序,而不用对模块内部进行修改。

就象家庭的供水系统一样,我们可以把进水表的出水管与净化过滤器的进水管连在一起,然后,把净化过滤器的出水管同水箱的进水管连在一起来拼凑成我们的供水管道系统。我们可以在这个供水管道系统中增加其他的水处理装置,也可以更换一个更大的水箱,甚至可以将进水表与水箱直连,而不经过净化过滤器,这一切都只需要各个水处理装置带有标准输入输出管道。

可见,使用管道流进行通信的模块具有“强内聚,弱耦合”的特点,一个模块被替换,或被拆卸不会影响其他模块。假设有一个使用了管道流的压缩或加密的模块,我们的调用程序只管向该模块的输入流中送入数据,从该模块的数据流中取得数据,就完成了我们数据的压缩或加密,这个模块完全就象黑匣子一样,我们根本不用去了解它的任何细节。

7.3.6ByteArrayInputStream与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 classByteArrayTest

{

public static void main(String[] args)throws Exception

{

String tmp="abcdefghijklmnopqrstuvwxyz";

byte [] src =tmp.getBytes();//src为转换前的内存块

ByteArrayInputStream input = newByteArrayInputStream(src);

ByteArrayOutputStream output = newByteArrayOutputStream();

newByteArrayTest().transform(input,output);

byte [] result = output.toByteArray();//result为转换后的内存块

System.out.println(new String(result));

}

public void transform(InputStreamin,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 staticvoid main(String[] args) throws Exception

{

newByteArrayTest().transform(System.in,System.out);

}

public voidtransform(InputStream in,OutputStream out)

{

int c=0;

try

{

while((c=in.read())!=-1)

{

intC = (int)Character.toUpperCase((char)c);

out.write(C);

}

}

catch(Exceptione)

{

e.printStackTrace();

}

}

}

我们没有修改transform方法中的任何代码,就利用它完成了我们期望的功能。我们还可以使用transform方法将一个文件中的内容全部变成大写字母后写入另外一个文件,也可以将键盘上输入的内容转变成大写字母后写入另外一个文件,这就是因为我们在tranform方法中使用的是InputStream和OutputStream这两个抽象基类,而不是直接使用某个具体的子类,这样就达到了以不变应万变的效果。

如果我们平时从键盘上读取内容的程序代码也放在一个类似transform方法的函数中去完成,也是用-1来作为键盘输入的结束,在该函数中不直接使用System.in,只是在调用该函数时,将System.in作为参数传递进去。这样,我们以后要从某个文件中读取数据,来代替手工键盘输入时,我们可以直接使用这个函数,程序就不用作太多的修改了。

7.4 过滤流与包装类

7.4.1 理解包装类的概念与作用

在前面的部分,我们接触到了许多节点流类,就以FileOutputStream和FileInputStream为例吧,这两个类只提供了读写字节的方法,我们通过它们只能往文件中写入字节或从文件中读取字节。在实际应用中,我们要往文件中写入或读取各种类型的数据,我们就必须先将其他类型的数据转换成字节数组后写入文件或是将从文件中读取到的字节数组转换成其他类型,这给我们的程序带来了一些困难和麻烦。如果有人给我们提供了一个中间类,这个中间类提供了读写各种类型的数据的各种方法,当我们需要写入其他类型的数据时,只要调用中间类中的对应的方法即可,在这个中间类的方法内部,它将其他数据类型转换成字节数组,然后调用底层的节点流类将这个字节数组写入目标设备。我们将这个中间类叫做过滤流类或处理流类,也叫包装类,如IO包中有一个叫DataOutputStream的包装类,下面是它所提供的部分方法的列表。

public final voidwriteBoolean(booleanv) throws IOException

public final voidwriteShort(intv) throws IOException

public final voidwriteChar(intv) throws IOException

public final voidwriteInt(intv) throws IOException

public final voidwriteLong(longv) throws IOException

public final voidwriteFloat(floatv) throws IOException

public final voidwriteDouble(doublev) throws IOException

public final voidwriteBytes(Strings) throws IOException

大家从上面的方法名和参数类型中,就知道这个包装类能帮我们往IO设备中写入各种类型的数据。包装类的调用过程如图7.2所示:

图7.2

使用输出包装类的过程,就好比我们要给某个市长送礼,该市长向来不接受陌生人的礼品,但其夫人则是来者不拒的。我们只要将礼品送到市长夫人手中,就等于送到了市长的手中。市长夫人就是我们用到的输出包装类。

使用输入包装类过程,好比我们要想借用市长大人的一点权力,承揽一个假竞标的工程项目,我们是没法直接请动市长大人来替我们说话的,但我们可以让市长公子来替我们办好这件事,我们就间接借用了市长的权力。市长公子就是我们用到的输入包装类。

我们还可以用包装类去包装另外一个包装类,创建包装类对象时,必须指定它要调用的那个底层流对象,也就是这些包装类的构造函数中,都必须接收另外一个流对象作为参数。如DataOutputStream包装类的构造函数为:

public DataOutputStream(OutputStreamout)

参数out就是DataOutputStream要调用的那个底层输出流对象。

7.4.2BufferedInputStream与 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,intsize)

第一种形式创建一个32字节的缓冲区,第二种形式以指定的大小来创建缓冲区。

7.4.3 DataInputStream与 DataOutputStream

这两个类提供了可以读写各种基本数据类型的数据的各种方法,这些方法是使用非常简单,在前面我们已经看到了DataOutputStream类的大部分方法,在DataInputStream类有与这些write方法对应的read方法,读者可以在JDK文档帮助查看详细的信息。

DataOutputStream类提供了三种写入字符串的方法,分别是

public final voidwriteBytes(Strings) throws IOException

public final voidwriteChars(Strings) throws IOException

public final voidwriteUTF(Stringstr) 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 classDataStreamTest

{

public static void main(String[] args)

{

try

{

FileOutputStream fos = newFileOutputStream("hello.txt");

BufferedOutputStream bos = newBufferedOutputStream(fos);

DataOutputStream dos = newDataOutputStream(bos);

dos.writeUTF("ab中国");

dos.writeBytes("ab中国");

dos.writeChars("ab中国");

dos.close();

FileInputStream fis = newFileInputStream("hello.txt");

BufferedInputStream bis = newBufferedInputStream(fis);

DataInputStream dis = new DataInputStream(bis);

System.out.println(dis.readUTF());

/*byte [] buf=new byte[1024];

int len = dis.read(buf);

System.out.println(newString(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.4PrintStream

PrintStream类提供了一系列的print和println方法,可以实现将基本数据类型的格式化成字符串输出。在前面,我们在程序中大量用到“System.out.println”语句中的System.out就是PrintStream类的一个实例对象,读者已经多次使用到这个类了。 PrintStream有3个构造函数:

PrintStream(OutputStream out)

PrintStream(OutputStreamout,boolean auotflush)

PrintStream(OutputStreamout,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.5ObjectInputStream与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 classserialization

{

public static void main(String args[])

throwsIOException,ClassNotFoundException

{

Student stu=newStudent(19,"dintdding",50,"huaxue");

FileOutputStream fos=newFileOutputStream("mytext.txt");

ObjectOutputStream os=newObjectOutputStream(fos);

try

{

os.writeObject(stu);

os.close();

}catch(IOException e)

{

System.out.println(e.getMessage());

}

stu=null;

FileInputStream fi=newFileInputStream("mytext.txt");

ObjectInputStream si=newObjectInputStream(fi);

try

{

stu=(Student)si.readObject();

si.close();

}catch(IOException e)

{

System.out.println(e.getMessage());

}

System.out.println("IDis:"+stu.id);

System.out.println("nameis:"+stu.name);

System.out.println("ageis:"+stu.age);

System.out.println("department is:"+stu.department);

}

}

class Studentimplements Serializable

{

int id;

String name;

int age;

String department;

public Student(int id,String name,intage,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(InputStreamin) //用默认字符集创建一个InputStreamReader对象

InputStreamReader(InputStreamin,String CharsetName) //接受以指定字符集名的字符串,并用//该字符集创建对象

OutputStreamWriter也有对应的两个主要的构造函数:

OutputStreamWriter(OutputStreamin) //用默认字符集创建一个OutputStreamWriter对象

OutputStreamWriter(OutputStreamin,StringCharsetName) //接受以指定字符集名的字符串,

//并用该字符集创建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的帮助界面中,我看了如下的一段信息:

SeeAlso:

FileReader,InputStreamReader

在这里,我看到了InputStreamReader这个关键的类,阅读其帮助后,最终写出了上面的程序代码,轻松解决了我从未碰到过的问题。在查阅文档时,经常顺便看看See Also部分也是很重要的,在那里往往都有解决相关问题的超链接。如果在SeeAlso部分也没有提及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(inti=0;i<strChina.length();i++)

{

System.out.println(Integer.toHexString((int)strChina.charAt(i)));

}

byte []buf=strChina.getBytes("gb2312");

for(inti=0;i<buf.length;i++)

{

System.out.println(Integer.toHexString(buf[i]));

}

System.out.println(strChina);

for(inti=0;i<buf.length;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<strChina.length();i++)

{

System.out.println(Integer.toHexString((int)strChina.charAt(i)));

}

byte [] buf=strChina.getBytes();

for(int i=0;i<buf.length;i++)

{

System.out.println(Integer.toHexString(buf[i]));

}

System.out.println(strChina);

for(int i=0;i<buf.length;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 classCharDecoder

{

public static void main(String [] args)throws Exception

{

System.out.println("please enter aChinese 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= newString(buf,0,pos,"gb2312");

for(inti=0;i<strInfo.length();i++)

{

System.out.println(Integer.toHexString((int)strInfo.charAt(i)));

}

System.out.println(strInfo);

for(int i=0;i<pos;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= newString(buf,0,pos,"gb2312");

修改为

strInfo= newString(buf,0,pos);

重新编译后运行的结果和修改前的结果一样,这说明我们系统的缺省解码方式就是gb2312。

步骤5,我们将系统的缺省解码方式修改成了iso8859-1或在String类的构造函数中明确指定用iso8859-1解码,来观察程序运行的结果,修改后的程序代码如下:

import java.io.*;

public classCharDecoder

{

public static void main(String [] args)throws Exception

{

System.getProperties().put("file.encoding","iso8859-1");

System.out.println("please enter aChinese 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= newString(buf,0,pos);

for(inti=0;i<strInfo.length();i++)

{

System.out.println(Integer.toHexString((int)strInfo.charAt(i)));

}

System.out.println(strInfo);

for(int i=0;i<pos;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 classInputReader

{

public static void main(String [] args)throws Exception

{

InputStreamReader isr=

newInputStreamReader(System.in, "iso8859-1");

BufferedReader br = newBufferedReader(isr);

String strLine=br.readLine();

for(int i=0;i<strLine.length();i++)

{

System.out.println(Integer.toHexString((int)strLine.charAt(i)));

}

isr.close();

System.out.println(strLine);

}

}

输入“中国”后,程序的运行结果如下:

中国

d6

d0

b9

fa

???ú

为了达到一种体验的效果,读者最好自己修改程序代码,让程序能够正常打印出输入中文字符。作者提前给出答案,照顾一下学得还不是很好的读者,读者可以用下面两种方式中的任意一种。

一.将

InputStreamReader isr =newInputStreamReader(System.in,"iso8859-1");

修改成

InputStreamReader isr =new InputStreamReader(System.in,"gb2312");

在中国大陆的计算机系统上一般都是简体中文版的操作系统,缺省字符集为gb2312,所以,我们也可以不指定字符集参数。

InputStreamReader isr =newInputStreamReader(System.in);

二.不修改上面的部分,而是将

System.out.println(strLine);

修改成

System.out.println(newString(strLine.getBytes("iso8859-1"),"gb2312"));

广东地区的台商和港商的工厂较多,他们使用的系统的缺省字符集就不一定是gb2312,使用这样系统的读者就不能照搬照套我本节的内容了。

7.5.2Decorator 设计模式

通过包装类,就可以用一个对象(the Decorators)包装另外的一个对象,比如: 可以用BufferedReader来包装一个FileReader, FileReader仅仅提供了底层的读操作,比如read(char[]buffer)。BufferedReader实现了一个更高层次上的操作,比如readLine,完成读取文件中的一个行。其实,这是一种被称着Decorator的设计模式。

我们要设计自己的IO包装类,需要继承FilterXXX命名的 类,从而扩展了对输入输出流的支持。比如我们设计一对类包装类:RecordInputStream和RecordOutputStream,来完成从数据库中读取记录和往数据库中写入记录。以后程序中就可以用它们来包装InputStream 和OutputStream,从而完成对数据库的操作。

包装类的使用非常灵活,我们来看看一个巧妙使用包装类的例子,从而借鉴一些思想。Exception类从Throwable类继承的三个printStackTrace方法的定义如下:

public voidprintStackTrace()

public voidprintStackTrace(PrintStreams)

public voidprintStackTrace(PrintWriters)

它们分别用把异常的详细信息打印到标准输出流(屏幕上),或者其他的PrintStream和PrintWriter流中。有时候,我们的应用需要把异常的详细信息放到一个字符串中,然后将这个包含异常详细信息的字符串通过网络发送出去,该怎么实现呢?我们先看看程序代码:

import java.io.*;

public classTestPrintWriter

{

public static void main(String [] args)

{

try

{

throw newException("test");

}

catch(Exception e)

{

StringWriter sw=new StringWriter();

e.printStackTrace(newPrintWriter(sw));

String strException = sw.toString();

System.out.println(strException);

}

}

}

在上面的程序中,用System.out.println将字符串打印在屏幕上来简单模拟通过网络将这个字符串发送出去。printStackTrace方法只能将异常详细信息写入一个PrintWriter对象中,写入PrintWriter对象的数据实际上会写入它所包装的一个Writer对象中,而写入StringWriter对象(一种Writer对象)的内容可以当作字符串取出来,所以用PrintWriter对象去包装一个StringWriter对象就可以解决我们的需求。解决问题的关键在于,我们如何能想到将这些流有机地串联起来。

7.5.3Java虚拟机读写其他进程的数据

我们在Java程序中可以产生其他的应用程序的进程,在Java程序中启动的进程称为子进程,启动子进程的Java程序称为父进程。子进程没有键盘和显示器,子进程的标准输入和输出不再连接到键盘和显示器,而是以管道流的形式连接到父进程的一个输出流和输入流对象上,调用Process类的getOutputStream和getInputStream方法可以得到这个输出流和输入流对象。子进程从标准输入读取到的内容是父进程通过输出流对象写入管道的数据,子进程写入标准输出的数据通过管道传送到了父进程的输入流对象中,父进程从这个输入流对象中读取到的内容就是子进程写入到标准输出的数据。

如javac.exe和java.exe这两个文件本身都是应用程序,我们在Java程序中也可以启动它们。请看下面的例子:

程序清单: TestInOut.java

import java.io.*;

public classTestInOut implements Runnable

{

Process p=null;

public TestInOut()

{

try

{

p=Runtime.getRuntime().exec("javaMyTest");

new Thread(this).start();

}

catch(Exception e)

{

System.out.println(e.getMessage());

}

}

public void send()

{

try

{

OutputStreamops=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();

BufferedReaderbfr=new BufferedReader(

newInputStreamReader(in));

while(true)

{

String strLine=bfr.readLine();

System.out.println(strLine);

}

}

catch(Exceptione)

{

System.out.println(e.getMessage());

}

}

}

class MyTest

{

public static void main(String [] args) throwsIOException

{

while(true)

{

System.out.println("hi:"+

new BufferedReader(newInputStreamReader(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(

newInputStreamReader(System.in));

while(true)

{

System.out.println("hi:"+bfr.readLine());

}

}

}

重新编译运行后的结果如下:

hi:help

hi:help

hi:help

hi:help

hi:help

可见,在不同的地方定义变量和创建对象,用不同的代码顺序和结构,程序的运行效率大不一样。

&多学两招:

其实,我们在很多细微之处都可以提高程序的运行效率的,如

for(inti=0;i<str.length();i++{…}

的效率就不如改写成

intlen=strlength();for(int i=0;i<len;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.2RandomAccessFile类............................................................................... 205

7.3 节点流.................................................................................................... 207

7.3.1 理解流的概念............................................................................... 207

7.3.2InputStream与OutputStream.......................................................... 208

指点迷津:1.如何选择输入与输出

2.为什么要调用close方法

多学两招:IO中的缓冲区

7.3.3FileInputStream与FileOutputStream................................................ 210

7.3.4 Reader与Writer............................................................................. 211

独家见解:隐含的缓冲区

7.3.5PipedInputStream与PipedOutputStream........................................... 212

独家见解:管道流类的作用

7.3.6ByteArrayInputStream与ByteArrayOutputStream.............................. 214

7.3.7 IO程序代码的复用....................................................................... 216

7.4 过滤流与包装类...................................................................................... 218

7.4.1 理解包装类的概念与作用.............................................................. 218

7.4.2BufferedInputStream与 BufferedOuputStream.................................. 219

脚下留心:使用mark时应考虑的问题

7.4.3DataInputStream与 DataOutputStream............................................ 219

7.4.4PrintStream.................................................................................... 222

指点迷津:何谓格式化输出

7.4.5ObjectInputStream与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

多学两招:提高程序的运行效率

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics