黑马程序员--第二阶段Java高级IO--第19天

来源:互联网 发布:linux安装wine 编辑:程序博客网 时间:2024/04/27 22:42
-------------------- android培训java培训、期待与您交流!--------------------

 

节点流

理解流的概念

         流是字节序列的抽象概念

         文件是数据的静态存储形式,而流是指数据传输时的形态
         文件是一些具有永久存储及特定顺序的字节组成的一个有序的具有名称的一些数据的集合,而流提供了一种向IO设备写入字节和从IO设备中读取字节的方式,文件只是流操作的IO设备之一,除了文件流,还存在网络流、内存流等等。

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

我们将IO流类分为两个大类,节点流类和过滤类(也叫处理流类、包装类)。程序用于直接操作目标设备所对应的类叫节点流类,程序也可以通过一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据,这个间接流类就是过滤流类。

 

InputStream与OutputStream

         程序可以从中连续读取字节的对象叫输入流,用InputStream类完成,在Java中,用InputStream类来描述所有输入流的抽象概念。程序能向其中连续写入字节的对象叫输出流,用OutputStream类完成。在Java中,用OutputStream类来描述所有输出流输出流的抽象概念。InputStreamOutputStream对象是两个抽象类,还不能表明具体对应哪种IO设备。它们下面有许多子类,包括网络、管道、内存、文件等具体的IO设备,如FileInputStream类对应的就是文件输入流,是一个节点流类,我们将这些节点流类所对应的IO源和目标称为流节点(Node)。

        很多人搞不清程序要将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 skiplong n)跳过输入流上的n个字节并返回实际跳过的字节数
  • int availabale( )返回当前输入流中可读的字节数。
  • void mark(int readlimit)在输入流的当前位置处放上一个标志,允许最多再读入readlimit个字节
  • void reset() 把输入指针返回到以前所做的标志处。
  • boolean mark()如果当前流支持mark/reset操作就返回true.
  • void close() 在操作完一个流后要使用此方法将其关闭,系统就会释放与这个流相关的资源。

  InputStream是一个抽象类,程序中实际使用的是它的各种子类对象。不是所有的子类都会支持InputStream中定义的某些方法的,如skip,mark,reset等,这些方法只对某些子类有用。

  一个对象在没有引用变量指向它时会变成垃圾,最终会被垃圾回收器从内存中清除。对于我们创建的流对象,干嘛还要“调用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中有哪些关键字等这些死板的东西,只要有个印象就足够了。

 

FileInputStream与FileOutputStream

  这两个流节点用来操作磁盘文件,通过它们的构造函数指定文件的路径和名字,当然这个文件应当是存在的和可读的。在创建一个FileOutputStream对象时指定文件如果存在将要被覆盖。

  下面对同一个磁盘文件创建FileInputStream对象的两种方式。其中用到的两个构造函数都可以引发FileNotFoundExcepiton异常:

    FileInputStream inOne=new FileInputStream(“hello.test”);

    File f=new File(“heelo.test”)

    FileInputSteam inTwo =new FileInputSteam(f);

  尽管第一个构造函数更简单,但第二个构造函数允许在把文件连接到输入流之前对文件做进一步分析。

  FileOutputStream对象也有两个和FileInputStream对象具有相同参数的构造函数,创建一个FileOutputStream对象时,可以为其指定还不存在的文件名,但不能是存在的目录名,也不能是一个已被其他程序打开了的文件。FileOutputStream先创建输出对象,然后再准备输出。

实例:    向文件中写入一串字符,并用FileInputStream读出写入的内容:

程序清单:FileStream.java

import java.io.*;

public class FileStream {

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

       {

              FileOutputStream out=new FileOutputStream("hello.txt");

              out.write("www.it315.org".getBytes());

              out.close();

             

              byte[] buf=new byte[1024];

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

              FileInputStream in =new FileInputStream(f);

              int len=in.read(buf);

              System.out.println(new String(buf,0,len));

              in.close();             

       }

}

 

Reader与Writer

 

  java中的字符是Unicode编码,是双字节的,而InputStreamOutputStream是用来处理字节的,在处理字符文本是不太方便,需要编写额外的程序代码。Java为字符文本的输入输出专门提供了一套单独的类,ReaderWriter两个抽象类与InputStreamOutputStream两个类相对应,同样,ReaderWriter下面也有许多子类,对具体IO设备进行字符输入输出,如FileReader就是用来读取文件流中的字符。ReaderWriter是所有字符流类的抽象基类,用于简化对字符串的输入输出编程,即用于读写文件数据。

二进制文件和文本文件的区别:

  如果一个文件专用于存储文本字符,而没有包含文本字符之外的其他数据就称之为文本文件,除此之外的文件就是二进制文件。

  对于ReaderWriter,大体的功能和InputStreamOutputStream两个类相同,但是并不是它们的代替者,只是在处理字符串时简化了我们的编程。上面的程序用FileWriterFileReader来实现:

import java.io.*;

public class FileStream2

{

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

       {

              FileWriter out=new FileWriter("hello2.txt");

              out.write("www.it315.org");

              out.close();

             

              char [] buf = new char[1024];

              FileReader in = new FileReader("hello2.txt");

              int len = in.read(buf);

              System.out.println(new String(buf,0,len));

       }    

}

 

  我们将程序中的out.close( );语句注释掉后编译远行,在hello.txt文件中没有看到out.write语句写入的字符串,这可能就是缓冲区的原因,将out.close( )改为out.flush后编译运行,在hello.txt文件中又能够看到out.write语句写入的字符串了。这更加证明了FileWriter使用了缓冲区。在使用FileOutputStream的例子程序中,我们同样注释掉out.close( );语句,编译运行后,在hello.txt文件中能够看到out.write语句写入的字符串,这说明FileOutputStream没有使用缓冲区。

FileWriter可以往文件中写入字符串,我们不用将字符串转换为字节数组。相对于FileOutputStream来说,使用FileReader读取文件中的内容并没有简化我们的编程工作,FileReaderr优势,要结合包装类才能体现出来。

 

 

PipedInputStream与PipedOutputStream类
(用于在应用程序中创建管道通信)

 

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

实例:

Sender.java

import java.io.*;//PipedOutputStream类在此包中

public class Sender extends Thread

{

       private PipedOutputStream out=new PipedOutputStream();

       public PipedOutputStream getPipedOutputStream()

       {

              return out;

       }

       public void run()

       {

              try

              {

                     String strInfo=new String("接收器,你好!");

                     out.write(strInfo.getBytes());

                     out.close();

              }

              catch(Exception e)

              {

                     e.printStackTrace();

              }

       }

}

 

Receiver.java

import java.io.*;//PipedInputStream类在此包中

public class Receiver extends Thread

{

       private PipedInputStream in = new PipedInputStream();    

       public PipedInputStream getPipedInputStream()

       {

              return in;

       }

       public void run()

       {

              try

              {

                     byte [] buf=new byte[1024];

                     int len=in.read(buf);

                     System.out.println("从发送器取得的数据:"+new String(buf,0,len));

                     in.close();

              }

              catch(Exception e)

              {

                     e.printStackTrace();

              }

       }    

}

 

PipedStreamTest.java

import java.io.*;

public class PipedStreamTest

{

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

       {

              Sender out = new Sender();

              Receiver in = new Receiver();

              PipedOutputStream a = out.getPipedOutputStream();

              PipedInputStream b = in.getPipedInputStream();

              a.connect(b);//这里可以用PipedOutputStream对象连接PipedInputStream对象,也可以用PipedInputStream对象连接PipedOutputStream对象

              out.start();

              in.start();

       }    

}

 

 

 

  JDK还提供了PipedWriter和PipedReader这两个类用于字符文本的管道通信,使用方法和PipedOutputStream和PipedInputStream的使用类似。

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

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

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

 

ByteArrayInputStream与ByteArrayOutputStream

 

  这两个类用于以IO流的方式来完成对字节数组内容的读写,来支持类似内存虚拟文件或者映像文件的功能。

  ByteArrayInputStream是输入流的一种实现,它有两个构造函数,每个构造函数都需要一个字节数组来作为数据源:
  •        ByteArrayInputStreambyte[ ] buf
  •        ByteArrayInputStreambyte[ ] buf,int offset , int length

       第二个构造函数指定使用数组buf中的从offset开始的length个元素作为数据源。

       ByteArrayOutputStream是输出流的一种实现,它也有两个构造函数:
  •        ByteArrayOutputStream()
  •        ByterArrayOutputStreamint

       第一种形式的构造函数创建一个32字节的缓冲区,第二种形式则是根据参数指定大小创建缓冲区。这两个构造函数创建的缓冲区的大小在数据过多时能够自动增长。创建缓冲区以后就可以像虚拟文件似的往它里面写入内容。

       这两个流的作用在于,用IO流的方式来完成对字节数组的内容的读写。爱思考的读者一定有过这样的疑问:对数组的读写非常简单,我们为什么不直接读写字节数组呢?在什么情况下该使用这两个类呢?

       有的人可能听说过内存虚拟文件或者是内存映像文件,它们是把一块内存虚拟成一个硬盘上的文件,原来该写到硬盘文件上的内容会被写到这个内存中,原来该从一个硬盘文件上读取内容可以改为从内存中直接读取。如果程序在运行过程中要产生一些临时文件,就可以用虚拟文件的方式来实现,我们不用访问硬盘,而是直接访问内存,会提高应用程序的效率。

       假设别人已经写好了一个压缩函数,这个函数接收两个参数,一个输入流对象,一个输出流对象,它从输入流对象中读取数据,并将压缩后的结果写入输出流对象中。我们的程序要将一台计算机的屏幕图像通过网络不断地传送到另外的计算机上,为了节省网络带宽,我们需要对一副屏幕图像的数据进行压缩后,再通过网络发送出去的。如果没有内存虚拟文件,我们就必须先将一副屏幕图像的像素数据写入到硬盘上的一个临时文件,再以这个文件为为输入流对象去调用那个压缩函数,接着又从压缩函数生成的压缩文件中读取压缩后的数据,再通过网络发送出去,最后删除压缩前的所生成的两个临时文件。可见这样的效率是非常低的。我们要在程序分配一个存储数据的内存块,通常都定义一个字节数组来实现,JDK中提供了ByteArrayInputStreamByteArrayOutputStream两个类可实现类似内存虚拟文件的功能,我们将抓取到的计算机屏幕图像的所有像素数据保存在一个数组中,然后根据这个数组创建一个ByteArrayInputStream流对象,同时创建一个用于保存压缩结果的ByteArrayOutputStream流对象,将这两个对象作为参数传递给压缩函数,最后从ByteArrayOutputStream流对象中返回包含有压缩结果的字节数组,这样的效率比较高。

       通过下面的例子程序来模拟上面的过程,我们并没有真正压缩输入流中的内容,只是把输入流中的所有英文字母变成对应的大写字母写入到输出流中。

import java.io.*;

public class ByteArrayStreamTest

{

       public static void main(String[] args)

       {

              String tmp="abcdefghijk";

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

              ByteArrayInputStream input=new ByteArrayInputStream(src);

              ByteArrayOutputStream output=new ByteArrayOutputStream();

              transform(input,output);

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

              System.out.println(new String(result));//直接把数组名作为参数表示将所有的字节数组元素都转化为为字符串

       }    

       public static void transform(InputStream in,OutputStream out)

       {

              int ch=0;

      

              try

              {

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

                     {

                            int upperCh=/*(int)*/Character.toUpperCase((char)ch);

                            out.write(upperCh);

                     }

              }

              catch(Exception e)

              {

                     e.printStackTrace();

              }

             

       }

}

 

       与ByteArrayInputStream和ByteArrayOutputStream类对应的字符串读写类分别是StringReader和StringWriter。

 

 

IO程序代码的复用

  由于没有编码为-1的字符,所以操作系统就使用-1作为硬盘上的每个文件的结尾标记,对于文本文件,我们的程序只要从文件中读取到了一个-1的字符值时,就可以确定已经到了这个文件结尾。注意。这种方式只能用于判断文件文件是否结束,不能判断一个二进制文件是否结束。尽管二进制文件的结尾标记也是-1,因为二进制文件中的每个字节可以是-128127之间的任意取值,当程序读取到一个正好为-1的字节时,就难以判定是文件结尾还是文件中的有效数据。对于标准的二进制文件,在文件开始部分,都有一个文件头指定文件的大小,程序就是凭借文件头中的这个大小来读取文件中的所有内容的。

  许多有一定软件开发经验的人也没完全搞清楚两者的区别。内存中的一个字节中数据可以是-128127之间的任意值,实际上是以二进制形式存放的,文件就是一片内存的数据在硬盘上的另外一种存放形式,也都是二进制数据,所以,可以说每个文件都是二进制的。我们现在的每个字符由一个或多个字节组成,每个字节都是用的-128127之间的部分数值来表示的,也就是说,-128127之间还有一些数据没有对应任何字符的任何字节。如果一个文件中的每个字节中的内容都是可以表示成字符的数据,我们就可以称这个文件为文本文件,可见,文本文件中是二进制文件中的一种特例,为了与文本文件相区别,人们又把除了文本文件以外的文件称之为二进制文件。由于很难严格区分文本文件和二进制文件的概念,所以我们可以简单地认为,如果一个文件专用于存储文本字符的数据,没有包含字符之外的其他数据,我们就称之为文本文件,除此之外的文件就是二进制文件。

  不管各种底层物理设备用什么方式实现数据的终止点,InputStreamread方法总是返回-1来表示输入流的结束。

  为了支持标准输入输出设备,Java定义了两个特殊的流对象:System.inSystem.outSystem.in对应键盘,是InputStream类型的,程序使用System.in可以读取键盘上输入的数据。System.out对应显示器,是PrintStream类型的,PrintStreamOutputStream的一个子类,程序使用System.out可以将数据输出到显示器上。键盘可以被当作一个特殊的输入文件,显示器可以被当作一个特殊的输出文件。当我们把键盘作为输入文件处理时,在Windows下,我们可以按下Ctrl+Z组合键来输入-1,作为文件的结束标记,在Linux下,可以按下Ctrl+D组合键来输入-1.

  在编写流的程序时,应尽量考虑到程序代码的复用性,对于上面的程序代码可以直接调用上面的transform方法,将键盘上输入的内容转变成大写字母后打印在屏幕上,程序代码如下:

import java.io.*;

public class test

{

       public static void main(String[] args)

       {

              transform(System.in,System.out);

       }    

       public static void transform(InputStream in,OutputStream out)

       {

              int ch=0;

      

              try

              {              

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

                     {

                            int upperCh=/*(int)*/Character.toUpperCase((char)ch);

                            out.write(upperCh);

                     }

              }

              catch(Exception e)

              {

                     e.printStackTrace();

              }

             

       }

}

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

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

 

 

-------------------- android培训java培训、期待与您交流!--------------------
                    详情请查看:http://edu.csdn.net/heima/