Java 编程要点之 I/O 流详解

澳门新葡亰平台游戏网站 12

本文详细介绍了 Java I/O 流的基础用法和原理。

[TOC]

  今天刚刚看完java的io流操作,把主要的脉络看了一遍,不能保证以后使用时都能得心应手,但是最起码用到时知道有这么一个功能可以实现,下面对学习进行一下简单的总结:

字节流(Byte Streams)

字节流处理原始的二进制数据
I/O。输入输出的是8位字节,相关的类为 InputStream 和 OutputStream.

字节流的类有许多。为了演示字节流的工作,我们将重点放在文件
I/O字节流 FileInputStream 和 FileOutputStream 上。其他种类的字节流用法类似,主要区别在于它们构造的方式,大家可以举一反三。

字节流

字节流就是每次以8位一个字节的方式执行输入输出。所有字节流都继承自InputStreamOutputStream,包括字符流在内的所有类型的I/O流都是基于字节流构建的。

澳门新葡亰平台游戏网站 1

字节流

  IO流主要用于硬板、内存、键盘等处理设备上得数据操作,根据处理数据的数据类型的不同可以分为:字节流(抽象基类为InPutStream和OutPutStream)和字符流(抽象基类为Reader和Writer)。根据流向不同,可以分为:输入流和输出流。
 其中主要结构可以用下图来表示:                          

用法

下面一例子 CopyBytes, 从 xanadu.txt 文件复制到
outagain.txt,每次只复制一个字节:

public class CopyBytes {
    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("resources/xanadu.txt");
            out = new FileOutputStream("resources/outagain.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

CopyBytes
花费其大部分时间在简单的循环里面,从输入流每次读取一个字节到输出流,如图所示:

澳门新葡亰平台游戏网站 2

字节输入流InputStream

字节输入流基本上都需要实现InputStream这个抽象类的方法:

方法摘要 备注
public abstract int read() throws IOException; 单字节的方式读取,返回值在0~255之间,如果返回-1表示读取结束。
public int read(byte b[], int off, int len) throws IOException; 通过调用read()方法逐字节地读取。返回值表示读取的字节数,该值一定小于等于b数组的长度,返回-1表示读取结束。
public int read(byte b[]) throws IOException; 直接调用read(b, 0, b.length)方法。
public long skip(long n) throws IOException InputStream默认使用read进行读取来实现跳过n个字节
public int available() throws IOException; 返回流中可读的字节数,InputStream默认实现返回0。
public void close(); 关闭流释放资源。InputStream默认提供空实现。
public boolean markSupported(); 该流是否支持标记。InputStream默认返回false。
public synchronized void mark(int readlimit) 标记当前读取的位置,使用reset可以恢复到标记位置。InputStream默认提供空实现。
public synchronized void reset() throws IOException; 恢复到mark标记的位置。InputStream默认抛出异常。

                            
      澳门新葡亰平台游戏网站 3

记得始终关闭流

不再需要一个流记得要关闭它,这点很重要。所以,CopyBytes 使用 finally
块来保证即使发生错误两个流还是能被关闭。这种做法有助于避免严重的资源泄漏。

一个可能的错误是,CopyBytes
无法打开一个或两个文件。当发生这种情况,对应解决方案是判断该文件的流是否是其初始
null 值。这就是为什么 CopyBytes
可以确保每个流变量在调用前都包含了一个对象的引用。

字节输出流OutputStream

字节输出流基本上都需要实现OutputStream这个抽象类的方法:

方法摘要 注意事项
public abstract void write(int b) throws IOException; int长度为32位,该方法会忽略高24位,只写入低8位。
public void write(byte b[], int off, int len) throws IOException; 调用write(b)方法逐字节写入。
public void write(byte b[]) throws IOException; 直接调用write(b, 0, b.length)方法写入。
public void flush() throws IOException; 强制将缓冲区内容写入。OutputStream默认提供空实现。
public void close() throws IOException; 关闭流释放资源。OutputStream默认提供空实现。

      

何时不使用字节流

CopyBytes 似乎是一个正常的程序,但它实际上代表了一种低级别的
I/O,你应该避免。因为 xanadu.txt
包含字符数据时,最好的方法是使用字符流,下文会有讨论。字节流应只用于最原始的
I/O。所有其他流类型是建立在字节流之上的。

字符流

字符流通常是字节流的“包装器”,所有的字符流都继承自Reader和Writer这两个抽象类。字符流底层仍然是使用字节流来执行物理I
/ O。

澳门新葡亰平台游戏网站 4

字符流

 字符流和字节流的主要区别:

字符流(Character Streams)

字符流处理字符数据的 I/O,自动处理与本地字符集转化。

Java 平台存储字符值使用 Unicode 约定。字符流 I/O
会自动将这个内部格式与本地字符集进行转换。在西方的语言环境中,本地字符集通常是
ASCII 的8位超集。

对于大多数应用,字符流的 I/O 不会比 字节流
I/O操作复杂。输入和输出流的类与本地字符集进行自动转换。使用字符的程序来代替字节流可以自动适应本地字符集,并可以准备国际化,而这完全不需要程序员额外的工作。

如果国际化不是一个优先事项,你可以简单地使用字符流类,而不必太注意字符集问题。以后,如果国际化成为当务之急,你的程序可以方便适应这种需求的扩展。见国际化获取更多信息。

字符转换流

前面提到了字节流和字符流,通常我们需要将字节流转换成字符流,而处理字节流到字符流的转换通常使用InputStreamReaderOutputStreamWriter。事实上看类的命名也可才出其作用:

  • InputStreamReader用于将字节输入流转换成字符输入流
  • OutputStreamWriter用于将字节输出流转换成字符输出流

比如说FileReader和FileWriter,其实这两个类什么也没干,仅仅是将FileInputStream包装成InputStreamReader,将FileOutputStream包装成OutputStreamWriter。也就是说:

字符流 等价包装
new FileReader("in.txt") new InputStreamReader(new FileInputStream("in.txt"))
new FileWriter("out.txt") new OutputStreamWriter(new FileOutputStream("out.txt"))

       1.字节流读取的时候,读到一个字节就返回一个字节
 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回

用法

字符流类描述在 Reader 和 Writer。而对应文件 I/O
,在 FileReader 和 FileWriter,下面是一个 CopyCharacters 例子:

public class CopyCharacters {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("resources/xanadu.txt");
            outputStream = new FileWriter("resources/characteroutput.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

CopyCharacters 与 CopyBytes 是非常相似的。最重要的区别在于
CopyCharacters 使用的 FileReader 和 FileWriter 用于输入输出,而
CopyBytes 使用 FileInputStream 和FileOutputStream
中的。请注意,这两个CopyBytes和CopyCharacters使用int变量来读取和写入;在
CopyCharacters,int 变量保存在其最后的16位字符值;在 CopyBytes,int
变量保存在其最后的8位字节的值。

缓冲流

       2.字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流

字符流使用字节流

字符流往往是对字节流的“包装”。字符流使用字节流来执行物理I/O,同时字符流处理字符和字节之间的转换。例如,FileReader
使用 FileInputStream,而 FileWriter使用的是 FileOutputStream。

有两种通用的字节到字符的“桥梁”流:InputStreamReader 和
OutputStreamWriter。当没有预包装的字符流类时,使用它们来创建字符流。在 socket 章节中将展示该用法。

为什么要有缓冲流?

如果使用无缓冲的I/O,这意味着每次读写请求都由底层操作系统直接处理。这个效率是非常低的,因为每次这样的请求通常会触发磁盘访问,网络IO或其他相当耗时的操作。举个例子:大部分磁盘都是使用扫描算法实现磁盘调度,而且磁盘的读写以扇区为基本单位,一个扇区为512字节(新硬盘是4KB),直接使用FileInputStream(或FileOutputStream)的进行小份量的读(写),将会导致磁头在一次扫描的过程中只读取一小部分的数据,如此反复以往,将会降低磁头的扫描的效率。

为了减少这种开销,Java提供了缓冲I/O流,每次读取(写入)请求都是从缓冲区中读取(写入)的,当缓冲区为空(已满)才会调用底层API进行读(写)操作。

     

面向行的 I/O

字符 I/O
通常发生在较大的单位不是单个字符。一个常用的单位是行:用行结束符结尾。行结束符可以是回车/换行序列(“rn”),一个回车(“r”),或一个换行符(“n”)。支持所有可能的行结束符,程序可以读取任何广泛使用的操作系统创建的文本文件。

修改 CopyCharacters 来演示如使用面向行的
I/O。要做到这一点,我们必须使用两个类,BufferedReader 和 PrintWriter 的。我们会在缓冲
I/O 和Formatting 章节更加深入地研究这些类。

该 CopyLines 示例调用 BufferedReader.readLine 和 PrintWriter.println
同时做一行的输入和输出。

public class CopyLines {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("resources/xanadu.txt"));
            outputStream = new PrintWriter(new FileWriter("resources/characteroutput.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

调用 readLine 按行返回文本行。CopyLines 使用 println
输出带有当前操作系统的行终止符的每一行。这可能与输入文件中不是使用相同的行终止符。

除字符和行之外,有许多方法来构造文本的输入和输出。欲了解更多信息,请参阅
Scanning 和 Formatting。

JDK中的缓冲流

我们可以使用I/O流包装类将无缓冲的I/O流包装成相应的缓冲流。这样的包装类有四个:

  1. 字节流:BufferedInputStream,BufferedOutputStream。
  2. 字符流:BufferedReader,BufferedWriter。

比如:

// 字节流
InputStream is = new BufferedInputStream(new FileInputStream("in.txt"));
OutputStream os = new BufferedOutputStream(new FileOutputStream("out.txt"));

// 字符流
Reader reader = new BufferedReader(new FileReader("in.txt"));
Writer writer = new BufferedWriter(new FileWriter("out.txt"));

 IO流主要可以分为节点流和处理流两大类。

缓冲流(Buffered Streams)

缓冲流通过减少调用本地 API 的次数来优化的输入和输出。

目前为止,大多数时候我们到看到使用非缓冲 I/O
的例子。这意味着每次读或写请求是由基础 OS
直接处理。这可以使一个程序效率低得多,因为每个这样的请求通常引发磁盘访问,网络活动,或一些其它的操作,而这些是相对昂贵的。

为了减少这种开销,所以 Java 平台实现缓冲 I/O
流。缓冲输入流从被称为缓冲区(buffer)的存储器区域读出数据;仅当缓冲区是空时,本地输入
API
才被调用。同样,缓冲输出流,将数据写入到缓存区,只有当缓冲区已满才调用本机输出
API。

程序可以转换的非缓冲流为缓冲流,这里用非缓冲流对象传递给缓冲流类的构造器。

inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));

用于包装非缓存流的缓冲流类有4个:BufferedInputStream 和 BufferedOutputStream 用于创建字节缓冲字节流, BufferedReader 和BufferedWriter 用于创建字符缓冲字节流。

数据流

数据流支持基本数据类型( booleancharbyte
shortintlong
floatdouble澳门新葡亰平台游戏网站,)以及字符串(String)类型的读写。所有的数据读写流都实现了DataInput或DataOutput接口,JDK相应地提供了DataInputStream和DataOutputStream这两个实现类。

澳门新葡亰平台游戏网站 5

数据流

和缓冲流一样,DataInputStream和OutputStream都是包装流,并且只能包装字节流。

示例:

// 这里先把文件流包装成缓冲流,然后在包装成数据流
DataInput in = new DataInputStream(
                new BufferedInputStream(
                new FileInputStream("in.txt")));
DataOutput out = new DataOutputStream(
                new BufferedOutputStream(
                new FileOutputStream("out.txt")));
// 然后就可以调用DataInput和DataOutput接口中的方法进行基本数据类型的读写。

     

刷新缓冲流

刷新缓冲区是指在某个缓冲的关键点就可以将缓冲输出,而不必等待它填满。

一些缓冲输出类通过一个可选的构造函数参数支持
autoflush(自动刷新)。当自动刷新开启,某些关键事件会导致缓冲区被刷新。例如,自动刷新
PrintWriter 对象在每次调用 println 或者 format 时刷新缓冲区。查看
Formatting 了解更多关于这些的方法。

如果要手动刷新流,请调用其 flush 方法。flush
方法可以用于任何输出流,但对非缓冲流是没有效果的。

对象流与序列化

前面提到使用DataInputStream和DataOutputStream对基本的数据类型进行读写操作。但是对于Java而言,大多数时候我们遇到的都是Java对象。这个时候就要使用对象流来对对象进行序列化或反序列化。

对象流都实现了ObjectInput或ObjectOutput接口,Java提供给我们两个相应的实现类ObjectInputStream和ObjectOutputStream来进行对象的I/O操作,需要注意我们序列化的对象类需要实现Serializable接口。Serializable接口没有任何方法,只是用来标识该对象能够序列化。相应的还有一个Externalizable接口,用于自定义可继承类的序列化。

澳门新葡亰平台游戏网站 6

序列化与反序列化相关类

对象的序列化与反序列化是一个很深的话题,这篇文章长度有限,想继续深入的朋友可以参考对序列化和反序列化和Java的序列化与反序列化这两篇文章

 一、节点流类型

       该类型可以从或者向一个特定的地点或者节点读写数据。主要类型如下: 

类型 字符流 字节流
File(文件)
FileReader
FileWriter
FileInputStream
FileOutputSream
Memory Array
CharArrayReader
CharArrayWriter
ByteArrayInputStream
ByteArrayOutputSream
Memory String
StringReader
StringWriter
   –
Pipe(管道)
PipedReader
PipedWriter
PipedInputSream
PipedOutputSream

  

扫描(Scanning)和格式化(Formatting)

扫描和格式化允许程序读取和写入格式化的文本。

I/O
编程通常涉及对人类喜欢的整齐的格式化数据进行转换。为了帮助您与这些琐事,Java
平台提供了两个API。scanning API
使用分隔符模式将其输入分解为标记。formatting API
将数据重新组合成格式良好的,人类可读的形式。

标准I/O流

以前学C++的时候,在iostream中声明了std::cinstd::coutstd::cerrstd::clog。这几个变量分别代表标准输入流,标准输出流,标准错误输出流,标准日志输出流。在Java中也有几个类似的流,它们都是System类的静态变量:System.inSystem.out以及System.err,这些对象是由Java平台初始化的流对象,不需要再打开。

通常标准输入流默认是键盘的输入,标准输出流和标准错误输出流默认是输出到显示屏上

看一下这三个流对象的声明:

public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;

看到这个定义,大家肯定和我一样好奇,final类型修饰的变量初始化为null,之后怎么能用呢?

看一看System类的静态初始化代码段:

/* register the natives via the static initializer.
 *
 * VM will invoke the initializeSystemClass method to complete
 * the initialization for this class separated from clinit.
 * Note that to use properties set by the VM, see the constraints
 * described in the initializeSystemClass method.
 */
private static native void registerNatives();
static {
    registerNatives();
}

系统加载System类时会调用registerNatives这个native方法,重点在registerNatives方法的注释,注释中说registerNatives方法底层会调用initializeSystemClass来完成类的初始化。

看看initializeSystemClass方法的关键代码:

private static void initializeSystemClass() {
    ...
    // 很符合UNIX“everything is a file”的设计哲学
    FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
    FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
    FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
    setIn0(new BufferedInputStream(fdIn));
    setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
    setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
    ...
}
private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
   if (enc != null) {
        try {
            return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
        } catch (UnsupportedEncodingException uee) {}
    }
    return new PrintStream(new BufferedOutputStream(fos, 128), true);
}

setIn0,setOut0,setErr0方法都是native方法,底层使用C/C++实现。霸道C++的const_cast关键字可以将常量重定向,所以我们完全可以不追究Java中final类型的怎么能够从新设定值。

而且我们从这段代码中可以看出:System.in是BufferedInputStream对象。

二、处理流类型

       
该类型是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写,处理流的构造方法总是要带一个其他流对象作为参数,一个流对象进过其他流的多次包装,叫做流的链接。主要可以分为以下几种:

 

 1、缓冲流(BufferedInPutStream/BufferedOutPutStream和BufferedWriter/BufferedReader)他可以提高对流的操作效率。

               

  写入缓冲区对象:                 

1 BufferedWriter bufw=new BufferedWriter(new FileWriter("buf.txt")); 

  读取缓冲区对象:                

1 BufferedReader bufr=new BufferedReader(new FileReader("buf.txt"));

   
 该类型的流有一个特有的方法:readLine();一次读一行,到行标记时,将行标记之前的字符数据作为字符串返回,当读到末尾时,返回null,其原理还是与缓冲区关联的流对象的read方法,只不过每一次读取到一个字符,先不进行具体操作,先进行临时储存,当读取到回车标记时,将临时容器中储存的数据一次性返回。

                   

         

2、转换流(InputStreamReader/OutputStreamWriter)

      该类型时字节流和字符流之间的桥梁,该流对象中可以对读取到的字节数据进行指定编码的编码转换。

      构造函数主要有:    

1     InputStreamReader(InputStream);                //通过构造函数初始化,使用的是本系统默认的编码表GBK。
2     InputStreamWriter(InputStream,String charSet);     //通过该构造函数初始化,可以指定编码表。
3     OutputStreamWriter(OutputStream);              //通过该构造函数初始化,使用的是本系统默认的编码表GBK。
4     OutputStreamwriter(OutputStream,String charSet);   //通过该构造函数初始化,可以指定编码表。

     
 注意:在使用FileReader操作文本数据时,该对象使用的时默认的编码表,即FileReader
fr=new FileReader(“a.txt”);      与     InputStreamReader isr=new
InputStreamReader(new FileInputStream(“a.txt”));  
的意义相同。如果要使用指定表编码表时,必须使用转换流,即如果a.txt中的文件中的字符数据是通过utf-8的形式编码,那么在读取时,就必须指定编码表,那么转换流时必须的。即:InputStreamReader
isr=new InputStreamReader(new FileInputStream(“a.txt”),utf-8);

         

 3、数据流(DataInputStream/DataOutputStream)

  • 该数据流可以方便地对一些基本类型数据进行直接的存储和读取,不需要再进一步进行转换,通常只要操作基本数据类型的数据,就需要通过DataStream进行包装。

  • 构造方法:  

    1 DataInputStreamReader(InputStream);  
    2 DataInputStreamWriter(OutputStream);
    
  • 方法举例:          

    1 int readInt();//一次读取四个字节,并将其转成int值
    2 writeInt(int);//一次写入四个字节,注意和write(int)不同,write(int)只将该整数的最低一个8位写入,剩余三个8为丢失
    3 hort readShort();
    4 writeShort(short);
    5 String readUTF();//按照utf-8修改版读取字符,注意,它只能读writeUTF()写入的字符数据。
    6 writeUTF(String);//按照utf-8修改版将字符数据进行存储,只能通过readUTF读取。
    

       
 注意:在使用数据流读/存数据的时候,需要有一定的顺序,即某个类型的数据先写入就必须先读出,服从先进先出的原则。

 

4、打印流(PrintStream/PrintWriter)

  • PrintStream是一个字节打印流,System.out对应的类型就是PrintStream,它的构造函数可以接受三种数据类型的值:1.字符串路径。2.File对象
    3.OutputStream

  •  PrintStream是一个字符打印流,它的构造函数可以接受四种类型的值:1.字符串路径。2.File对象
    3.OutputStream  4.Writer 
    对于1、2类型的数据,可以指定编码表,也就是字符集,对于3、4类型的数据,可以指定自动刷新,当该自动刷新为True时,只有3个方法可以用:println,printf,format。

 

5、对象流(ObjectInputStream/ObjectOutputStream)

        该类型的流可以把类作为一个整体进行存取,主要方法有:

  • Object readObject();该方法抛出异常:ClassNotFountException。

  • void
    writeObject(Object):被写入的对象必须实现一个接口:Serializable,否则就会抛出:NotSerializableException

扫描

标准流的重定向

上面说道System.in,System.out,System.err三个标准流的初始化使用了setIn0,setOut0,setErr0方法。System类还将这三个方法提供了调用接口给我们。

public static void setIn(InputStream in) {
    checkIO();
    setIn0(in);
}
public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}
public static void setErr(PrintStream err) {
    checkIO();
    setErr0(err);
}

使用这三个方法我们能够对标准流进行重定向。

说了这么多,可能还有人不了解流重定向到底是什么概念。

学过UNIX,Linux或者玩过Windows命令行的人可能很清楚,我们常用>>>将程序的打印内容输出到文件中,甚至用<将文件内容作为程序的输入。看到这,或许你会联想到C++的HelloWorld程序std::cout<<"Hello World!"<<std::endl;,C++就形象地使用了<<来表示流操作符。

额,扯远了→_→。总而言之:流的重定向就是将原来的应该输出的内容输出到其他地方。

举个例子:

// 标准输出流重定向
System.setOut(new PrintStream(new FileOutputStream("D://redirect.txt")));
// HelloWorld将会输出到D盘的redirect.txt文件中。
System.out.println("Hello World");

三、java IO的一般使用原则:

  1、按数据来源(去向)分类:

    1、是文件: FileInputStream, FileOutputStream, FileReader,
FileWriter

    2、是byte[]:ByteArrayInputStream, ByteArrayOutputStream

    3、是Char[]: CharArrayReader, CharArrayWriter

    4、是String: StringBufferInputStream, StringReader,
StringWriter

    5、网络数据流:InputStream, OutputStream, Reader, Writer

  2、按是否格式化输出分:

    1、要格式化输出:PRintStream,
PrintWriter

  3、按是否要缓冲分:

    1、要缓冲:BufferedInputStream, BufferedOutputStream,
BufferedReader, BufferedWriter

  4、按数据格式分:

    1、二进制格式(只要不能确定是纯文本的): InputStream,
OutputStream及其所有带Stream结束的子类

    2、纯文本格式(含纯英文与汉字或其他编码方式);Reader,
Writer及其所有带Reader, Writer的子类

  5、按输入输出分:

    1、输入:Reader, InputStream类型的子类

    2、输出:Writer, OutputStream类型的子类

  6、特殊需要:

    1、从Stream到Reader,Writer的转换类:InputStreamReader,
OutputStreamWriter

    2、对象输入输出:ObjectInputStream, ObjectOutputStream

    3、进程间通信:PipeInputStream,
PipeOutputStream, PipeReader, PipeWriter

    4、合并输入:SequenceInputStream

    5、更特殊的需要:PushbackInputStream, PushbackReader,
LineNumberInputStream, LineNumberReader

将其输入分解为标记

默认情况下,Scanner
使用空格字符分隔标记。(空格字符包括空格,制表符和行终止符。为完整列表,请参阅Character.isWhitespace)。示例
ScanXan 读取 xanadu.txt 的单个词语并打印他们:

public class ScanXan {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        Scanner s = null;

        try {
            s = new Scanner(new BufferedReader(new FileReader("resources/xanadu.txt")));

            while (s.hasNext()) {
                System.out.println(s.next());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

虽然 Scanner 不是流,但你仍然需要关闭它,以表明你与它的底层流执行完成。

调用 useDelimiter()
,指定一个正则表达式可以使用不同的标记分隔符。例如,假设您想要标记分隔符是一个逗号,后面可以跟空格。你会调用

s.useDelimiter(",\s*");

标准流的格式化输入输出

对于上面的System.out这个对象,我们肯定非常熟悉,System.err对象和System.out对象是相同类型的对象,用法也就不用多说了,但是System.in这个对象用的就相对少很多了。前面我们对标准输入输出流进行了简单的剖析,我们已经知道System.in是个BufferedInputStream对象,很明显我们要想从中读取基本数据类型(
booleancharbyteshortintlong
floatdouble)以及字符串(String)类型肯定相当麻烦。这个时候我们可能会想到使用DataInputStream这个类来对其进行包装。

DataInputStream in = new DataInputStream(System.in);

是个不错的想法,可惜并不可靠,比如下面这个程序:

DataInputStream in = new DataInputStream(System.in);
System.out.println(in.readInt());

这个时候你输入一个100再敲一下回车,你可能会得到825241613的输出。为什么呢?

我们将825241613转换成16进制:0x3130300D,31是字符1的ASCII码值,30是字符0的ASCII码值,0D是r也就是回车的ASCII。看到这我们明白了:不能用一个字节流去读一个字符流

那我们可以使用InputStreamReader将它转成字符流。很显然Reader的read方法不适合读取标准输入流。

下面我们来介绍一个工具类。

四、决定使用哪个类以及它的构造进程的一般准则如下(不考虑特殊需要):

  第一,考虑最原始的数据格式是什么:是否为文本?

  第二,是输入还是输出?

  第三,是否需要转换流:InputStreamReader, OutputStreamWriter?

  第四,数据来源(去向)是什么:文件?内存?网络?

  第五,是否要缓冲:bufferedReader
(特别注明:一定要注意的是readLine()是否有定义,有什么比read,
write更特殊的输入或输出方法)

  第六,是否要格式化输出:print?

转换成独立标记

该 ScanXan 示例是将所有的输入标记为简单的字符串值。Scanner 还支持所有的
Java 语言的基本类型(除 char),以及 BigInteger 和 BigDecimal
的。此外,数字值可以使用千位分隔符。因此,在一个美国的区域设置,Scanner
能正确地读出字符串“32,767”作为一个整数值。

这里要注意的是语言环境,因为千位分隔符和小数点符号是特定于语言环境。所以,下面的例子将无法正常在所有的语言环境中,如果我们没有指定
scanner
应该用在美国地区工作。可能你平时并不用关心,因为你输入的数据通常来自使用相同的语言环境。可以使用下面的语句来设置语言环境:

s.useLocale(Locale.US);

该 ScanSum 示例是将读取的 double 值列表进行相加:

public class ScanSum {
    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        Scanner s = null;
        double sum = 0;

        try {
            s = new Scanner(new BufferedReader(new FileReader("resources/usnumbers.txt")));
            s.useLocale(Locale.US);

            while (s.hasNext()) {
                if (s.hasNextDouble()) {
                    sum += s.nextDouble();
                } else {
                    s.next();
                }
            }
        } finally {
            s.close();
        }

        System.out.println(sum);
    }
}

输出为:1032778.74159

Scanner扫描工具处理格式化输入

需要注意Scanner类并不是I/O流。它是java.util包下的一个专门用来扫描文本数据的工具类。

Scanner使用正则表达式来解析任何可读取的对象中的数据。

它有以下的构造函数:

public Scanner(java.lang.Readable source);
public Scanner(java.io.InputStream source);
public Scanner(InputStream source, String charsetName);
public Scanner(java.io.File source);
public Scanner(File source, String charsetName);
public Scanner(java.nio.file.Path source);
public Scanner(Path source, String charsetName);
public Scanner(String source);
public Scanner(java.nio.channels.ReadableByteChannel source);
public Scanner(ReadableByteChannel source, String charsetName);

创建Scanner对象后,每次读取的数据都会将数据缓存在一个CharBuffer字符缓冲区中,我们可以对其读取方式进行查看或重新设置:

查看属性 设置属性
Pattern delimiter() Scanner useDelimiter(Pattern pattern) 或 Scanner useDelimiter(String pattern)
Locale locale() Scanner useLocale(Locale locale)
int radix() Scanner useRadix(int radix)

这些属性对应的意义以及默认值分别如下:

属性 属性的意义 默认值
locale 不同地区解析字符的方法不同 Locale.getDefault(Locale.Category.FORMAT)
delimiter 每次读取的分界符 任意空白字符
radix 遇到数组使用多少进制进行解析 10

我们还可以调用hasNextXxx()方法来查看有没有指定类型的数据,用NextXxx()方法来获取相应类型的数据。

澳门新葡亰平台游戏网站 7

Scanner

package cn.hff.io;

import java.io.IOException;
import java.util.Locale;
import java.util.Scanner;

public class ScannerTest {
    public static void main(String[] args) throws IOException {
        String source = "| FF | AAAA | 9C9C | Scanner | 9C9C | AAAA | FF |";
        Scanner scanner = new Scanner(source) // 创建Scanner对象
                .useRadix(16) // 设置数字以16进制解析
                .useDelimiter("\s*\|\s*") // 设置分隔符为“|”两边加任意空白符
                .useLocale(Locale.CHINA);
        while (scanner.hasNext()) {
            if (scanner.hasNextInt())
                System.out.println(scanner.nextInt());
            else
                System.out.println(scanner.next());
        }
        scanner.close();
    }
}

不得不说Scanner是个功能强大的类。

有了上面所说的这些功能,我们就可以使用Scanner随心所欲的操作System.in流了。

package cn.hff.io;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class SystemIO {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // 创建Scanner对象
        int count = 0;
        List<String> strList = new ArrayList<String>();
        List<Integer> intList = new ArrayList<Integer>();
        List<Double> dblList = new ArrayList<Double>();
        while (count++ < 10) {
            if (scanner.hasNextInt()) {
                intList.add(scanner.nextInt());
            } else if (scanner.hasNextDouble()) {
                dblList.add(scanner.nextDouble());
            } else {
                strList.add(scanner.next());
            }
        }
        scanner.close();
        System.out.println(strList);
        System.out.println(intList);
        System.out.println(dblList);
    }
}

控制台输入与输出的结果:

Hello
Google
World
10.10
10.24
1000
Code
07.23
04.17
OK?
[Hello, Google, World, Code, OK?]
[1000]
[10.1, 10.24, 7.23, 4.17]

格式化

实现格式化流对象要么是
字符流类的 PrintWriter 的实例,或为字节流类的 PrintStream 的实例。

注:对于 PrintStream 对象,你很可能只需要 System.out 和 System.err。
(请参阅命令行I/O)当你需要创建一个格式化的输出流,请实例化
PrintWriter,而不是 PrintStream。

像所有的字节和字符流对象一样,PrintStream 和 PrintWriter
的实例实现了一套标准的 write
方法用于简单的字节和字符输出。此外,PrintStream 和 PrintWriter
的执行同一套方法,将内部数据转换成格式化输出。提供了两个级别的格式:

  • print 和 println 在一个标准的方式里面格式化独立的值 。
  • format 用于格式化几乎任何数量的格式字符串值,且具有多种精确选择。

PrintStream和PrintWriter打印流处理格式化输出

刚说完格式化输入,现在讲讲格式化输出吧!

我们前面提到了System.out标准输出流,它是一个PrintStream对象,它本身就是一个格式化输出流对象。

所以我们不需要进行任何包装等操作就可以直接使用。另外还有一个和PrintStream功能很类似的类PrintWriter。这里对PrintStream和PrintWriter进行区别。事实上这两个类源码的80%以上都是一样的,功能上基本上是重复的,我也不知道为什么JDK中会有两个功能基本相同的类,因为这两个类之间的区别完全无关痛痒:

如果这两个类创建对象时都指定了autoFlush参数(默认为false),则PrintStream每次输入的内容中有n这个换行符就会调用flush方法对缓冲区进行强制输出,而PrintWriter只有调用println,formatprintf方法时才会对缓冲区进行强制输出。因为PrintStream继承自FilterOutputStream所以只能对字节流进行包装,而PrintStream继承自Writer所以它不仅能包装字节流还能包装字符流(这个体现在构造函数上)。

澳门新葡亰平台游戏网站 8

Print格式化输出

print 和 println 方法

调用 print 或 println 输出使用适当 toString
方法变换后的值的单一值。我们可以看到这 Root 例子:

public class Root {
    /**
     * @param args
     */
    public static void main(String[] args) {
            int i = 2;
        double r = Math.sqrt(i);

        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

输出为:

The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.

在 i 和 r 变量格式化了两次:第一次在重载的 print
使用代码,第二次是由Java编译器转换码自动生成,它也利用了
toString。您可以用这种方式格式化任意值,但对于结果没有太多的控制权。

标准输入输出流的封装—-Console

我们前面提到System.inSystem.outSystem.err这三个标准输入输出流。JDK1.6版本后提供给我们一个类—-Console(控制台),这个类封装了标准输入输出流。我们可以看一下它的构造方法:

private Console() {
    readLock = new Object();
    writeLock = new Object();
    String csname = encoding();
    if (csname != null) {
        try {
            cs = Charset.forName(csname);
        } catch (Exception x) {}
    }
    if (cs == null)
        cs = Charset.defaultCharset();
    out = StreamEncoder.forOutputStreamWriter(
              new FileOutputStream(FileDescriptor.out),
              writeLock,
              cs);
    pw = new PrintWriter(out, true) { public void close() {} };
    formatter = new Formatter(out);
    reader = new LineReader(StreamDecoder.forInputStreamReader(
                 new FileInputStream(FileDescriptor.in),
                 readLock,
                 cs));
    rcb = new char[1024];
}

其中reader是对标准输入流的封装,pw和out是对标准输出流的封装,而且这两个流都封装成了字符流,这个比System.inSystem.out好用多了。

从构造方法我们这个类我们没法new对象,在上面提到的System类中有一个静态方法System.console()可以获取Console对象。这个类中提供了一下几个方法给我们调用。

澳门新葡亰平台游戏网站 9

Console的相关方法

format 方法

该 format 方法用于格式化基于 format string(格式字符串)
多参。格式字符串包含嵌入了 format specifiers
(格式说明)的静态文本;除非使用了格式说明,否则格式字符串输出不变。

格式字符串支持许多功能。在本教程中,我们只介绍一些基础知识。有关完整说明,请参阅
API
规范关于格式字符串语法。

Root2 示例在一个 format 调用里面设置两个值:

public class Root2 {
    /**
     * @param args
     */
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);

        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

输出为:The square root of 2 is 1.414214.

像本例中所使用的格式为:

  • d 格式化整数值为小数
  • f 格式化浮点值作为小数
  • n 输出特定于平台的行终止符。

这里有一些其他的转换格式:

  • x 格式化整数为十六进制值
  • s 格式化任何值作为字符串
  • tB 格式化整数作为一个语言环境特定的月份名称。

还有许多其他的转换。

注意:除了 %% 和 %n,其他格式符都要匹配参数,否则抛出异常。在 Java
编程语言中, n转义总是产生换行符(u000A)。不要使用ñ除非你特别想要一个换行符。为了针对本地平台得到正确的行分隔符,请使用%n

除了用于转换,格式说明符可以包含若干附加的元素,进一步定制格式化输出。下面是一个
Format 例子,使用一切可能的一种元素。

public class Format {
    /**
     * @param args
     */
    public static void main(String[] args) {
         System.out.format("%f, %1$+020.10f %n", Math.PI);
    }
}

输出为:3.141593, +00000003.1415926536

附加元素都是可选的。下图显示了长格式符是如何分解成元素

澳门新葡亰平台游戏网站 10

元件必须出现在显示的顺序。从合适的工作,可选的元素是:

  • Precision(精确)。对于浮点值,这是格式化值的数学精度。对于 s
    和其他一般的转换,这是格式化值的最大宽度;该值右截断,如果有必要的。
  • Width(宽度)。格式化值的最小宽度;如有必要,该值被填充。默认值是左用空格填充。
  • Flags(标志)指定附加格式设置选项。在 Format 示例中,+
    标志指定的数量应始终标志格式,以及0标志指定0是填充字符。其他的标志包括

    (垫右侧)和(与区域特定的千位分隔符格式号)。请注意,某些标志不能与某些其他标志或与某些转换使用。
  • Argument
    Index(参数索引)允许您指定的参数明确匹配。您还可以指定<到相同的参数作为前面的说明一致。这样的例子可以说:System.out.format(“%F,%<+ 020.10f%N”,Math.PI);

数组I/O流

数组I/O流,可以将字节数组(或字符数组)以流的方式来操作。

JDK1.0开始就为我们提供了字节数组输入输出流:ByteArrayInputStream和ByteArrayOutputStream。

JDK1.1有了字符流后,同时提供给我们字符数组输入输出流:CharArrayReader和CharArrayWriter。

  1. ByteArrayInputStream

    这个类继承自InputStream。我们可以将字节数组作为参数来构造它。有了这个类,我们读取字节数组的时候,就可以不用自己去设计一个pos来标识我们读到了哪里,也不用担心pos标志位被程序的某个地方无意的修改了,因为ByteArrayInputStream已经完全为我们封装好了。

  2. ByteArrayOutputStream

    这个类继承自OutputStream。我们可以把它理解成Vector<byte>来使用,当然Vector不能直接放入byte,因为Java的Vector和ArrayList等集合类型底层保存数据都是使用Object数组,这就导致8种基本数据类型必须经过装箱操作才能保存到集合数组中,如果对大批量的数据进行拆装箱操作,这效率影响不言而喻。而ByteArrayOutputStream这个类直接使用byte[]来保存数据,并且能够自动扩容,扩容策略和Vector一样int newCapacity = oldCapacity << 1;。如果你想把这字节数组中的内容转成字符串可以直接调用这个类的toString方法。

    注意:ByteArrayInputStream和ByteArrayOutputStream的操作都是线程安全的,相应地,它们在单线程下的效率不是很高。

  3. CharArrayReader

    这个类继承自Reader。与ByteArrayInputStream功能一致,区别在于CharArrayReader读取的是字符数组的内容。

  4. CharArrayWriter

    这个类继承自Writer。与ByteArrayOutputStream功能一致,区别在于CharArrayWriter写入到字符数组中。

    注意:CharArrayReader和CharArrayWriter的所有方法不是线程安全的,在多线程的程序中可能会出现问题。

命令行 I/O

命令行 I/O 描述了标准流(Standard Streams)和控制台(Console)对象。

Java 支持两种交互方式:标准流(Standard
Streams)和通过控制台(Console)。

字符串I/O流

与CharArrayReader类似的还有一个StringReader,可以以流的方式来读取字符串的内容。JDK1.1设计了设个类之后,原本的StringBufferInputStream就被替代了。

相应地,和CharArrayWriter对应的也有一个StringWriter,可以以流的方式来将字符或字符串写入到一个StringBuffer中。

标准流

标准流是许多操作系统的一项功能。默认情况下,他们从键盘读取输入和写出到显示器。它们还支持对文件和程序之间的
I/O,但该功能是通过命令行解释器,而不是由程序控制。

Java平台支持三种标准流:标准输入(Standard Input, 通过 System.in
访问)、标准输出(Standard Output, 通过System.out 的访问)和标准错误(
Standard Error,
通过System.err的访问)。这些对象被自动定义,并不需要被打开。标准输出和标准错误都用于输出;错误输出允许用户转移经常性的输出到一个文件中,仍然能够读取错误消息。

您可能希望标准流是字符流,但是,由于历史的原因,他们是字节流。
System.out 和System.err
定义为 PrintStream 的对象。虽然这在技术上是一个字节流,PrintStream
利用内部字符流对象来模拟多种字符流的功能。

相比之下,System.in
是一个没有字符流功能的字节流。若要想将标准的输入作为字符流,可以包装
System.in 在 InputStreamReader

InputStreamReader cin = new InputStreamReader(System.in);

管道I/O流

管道流的主要作用是可以进行两个线程间的通讯。

JDK同样提供给我们两种管道I/O:

  1. 字节管道I/O流:PipedInputStream和PipedOutputStream。
  2. 字符管道I/O流:PipedReader和PipedWriter。

澳门新葡亰平台游戏网站 11

管道IO进行线程间通信

示例:

package cn.hff.io;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class PipeTest {
    /**
     * 生产者
     */
    static class Sender implements Runnable {
        private PipedWriter writer = new PipedWriter();

        public void run() {
            try {
                int count = 0;
                while (count++ < 100) {
                    writer.write((int) ('a' + Math.random() * 26));
                }
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void connect(Receiver receiver) throws IOException {
            this.writer.connect(receiver.getReader());
        }
    }

    /**
     * 消费者
     */
    static class Receiver implements Runnable {
        private PipedReader reader = new PipedReader();

        public void run() {
            try {
                int count = 0;
                while (count++ < 100) {
                    System.out.print((char) reader.read());
                }
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public PipedReader getReader() {
            return reader;
        }
    }

    public static void main(String[] args) throws IOException {
        Sender s = new Sender();
        Receiver r = new Receiver();
        s.connect(r);
        new Thread(s).start();
        new Thread(r).start();
    }
}

Console (控制台)

更先进的替代标准流的是 Console
。这个单一,预定义的 Console 类型的对象,有大部分的标准流提供的功能,另外还有其他功能。Console
对于安全的密码输入特别有用。Console
对象还提供了真正的输入输出字符流,是通过 reader 和 writer 方法实现的。

若程序想使用 Console ,它必须尝试通过调用 System.console() 检索 Console
对象。如果 Console 对象存在,通过此方法将其返回。如果返回 NULL,则
Console
操作是不允许的,要么是因为操作系统不支持他们或者是因为程序本身是在非交互环境中启动的。

Console
对象支持通过读取密码的方法安全输入密码。该方法有助于在两个方面的安全。第一,它抑制回应,因此密码在用户的屏幕是不可见的。第二,readPassword
返回一个字符数组,而不是字符串,所以,密码可以被覆盖,只要它是不再需要就可以从存储器中删除。

Password 例子是一个展示了更改用户的密码原型程序。它演示了几种 Console
方法

public class Password {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Console c = System.console();
        if (c == null) {
            System.err.println("No console.");
            System.exit(1);
        }

        String login = c.readLine("Enter your login: ");
        char [] oldPassword = c.readPassword("Enter your old password: ");

        if (verify(login, oldPassword)) {
            boolean noMatch;
            do {
                char [] newPassword1 = c.readPassword("Enter your new password: ");
                char [] newPassword2 = c.readPassword("Enter new password again: ");
                noMatch = ! Arrays.equals(newPassword1, newPassword2);
                if (noMatch) {
                    c.format("Passwords don't match. Try again.%n");
                } else {
                    change(login, newPassword1);
                    c.format("Password for %s changed.%n", login);
                }
                Arrays.fill(newPassword1, ' ');
                Arrays.fill(newPassword2, ' ');
            } while (noMatch);
        }

        Arrays.fill(oldPassword, ' ');
    }

    // Dummy change method.
    static boolean verify(String login, char[] password) {
        // This method always returns
        // true in this example.
        // Modify this method to verify
        // password according to your rules.
        return true;
    }

    // Dummy change method.
    static void change(String login, char[] password) {
        // Modify this method to change
        // password according to your rules.
    }
}

上面的流程是:

  • 尝试检索 Console 对象。如果对象是不可用,中止。
  • 调用 Console.readLine 提示并读取用户的登录名。
  • 调用 Console.readPassword 提示并读取用户的现有密码。
  • 调用 verify 确认该用户被授权可以改变密码。(在本例中,假设 verify
    是总是返回true )
  • 重复下列步骤,直到用户输入的密码相同两次:
    • 调用 Console.readPassword 两次提示和读一个新的密码。
    • 如果用户输入的密码两次,调用 change 去改变它。 (同样,change
      是一个虚拟的方法)
    • 用空格覆盖这两个密码。
  • 用空格覆盖旧的密码。

回退输入流

使用回退流对其他输入流包装后,我们不仅可以从流中读取数据,还可以往流中回写数据。当然这里说的回写仅仅是将数据写入到回写流的内存缓冲区,而不是写入输入源。

JDK中提供了两个回写输入流:

  1. 回写字节流:PushbackInputStream。
  2. 回写字符流:PushbackReader。

回写输入流在普通I/O流的基础上提供了一下三个方法,使用这三个方法我们就可以往输入流中回写数据了:

回写字节流 回写字符流
void unread(int b) unread(int c)
void unread(byte[] b, int off, int len) void unread(char cbuf[], int off, int len)
void unread(byte[] b) void unread(char cbuf[])

数据流(Data Streams)

Data Streams 处理原始数据类型和字符串值的二进制 I/O。

支持基本数据类型的值((boolean, char, byte, short, int, long, float, 和
double)以及字符串值的二进制
I/O。所有数据流实现 DataInput 或DataOutput 接口。本节重点介绍这些接口的广泛使用的实现,DataInputStream 和 DataOutputStream 类。

DataStreams
例子展示了数据流通过写出的一组数据记录到文件,然后再次从文件中读取这些记录。每个记录包括涉及在发票上的项目,如下表中三个值:

记录中顺序 数据类型 数据描述 输出方法 输入方法 示例值
1 double Item price DataOutputStream.writeDouble DataInputStream.readDouble 19.99
2 int Unit count DataOutputStream.writeInt DataInputStream.readInt 12
3 String Item description DataOutputStream.writeUTF DataInputStream.readUTF “Java T-Shirt”

首先,定义了几个常量,数据文件的名称,以及数据。

static final String dataFile = "invoicedata";

static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
static final int[] units = { 12, 8, 13, 29, 50 };
static final String[] descs = {
    "Java T-shirt",
    "Java Mug",
    "Duke Juggling Dolls",
    "Java Pin",
    "Java Key Chain"
};

DataStreams 打开一个输出流,提供一个缓冲的文件输出字节流:

out = new DataOutputStream(new BufferedOutputStream(
              new FileOutputStream(dataFile)))

DataStreams 写出记录并关闭输出流:

for (int i = 0; i < prices.length; i ++) {
    out.writeDouble(prices[i]);
    out.writeInt(units[i]);
    out.writeUTF(descs[i]);
}

该 writeUTF 方法写出以 UTF-8 改进形式的字符串值。

现在,DataStreams
读回数据。首先,它必须提供一个输入流,和变量来保存的输入数据。像
DataOutputStream 、DataInputStream 类,必须构造成一个字节流的包装器。

in = new DataInputStream(new
            BufferedInputStream(new FileInputStream(dataFile)));

double price;
int unit;
String desc;
double total = 0.0;

现在,DataStreams 可以读取流里面的每个记录,并在遇到它时将数据报告出来:

try {
    while (true) {
        price = in.readDouble();
        unit = in.readInt();
        desc = in.readUTF();
        System.out.format("You ordered %d" + " units of %s at $%.2f%n",
            unit, desc, price);
        total += unit * price;
    }
} catch (EOFException e) {
}

请注意,DataStreams 通过捕获 EOFException
检测文件结束的条件而不是测试无效的返回值。所有实现了 DataInput
的方法都使用 EOFException 类来代替返回值。

还要注意的是 DataStreams 中的各个 write 需要匹配对应相应的
read。它需要由程序员来保证。

DataStreams 使用了一个非常糟糕的编程技术:它使用浮点数来表示的货币价值。在一般情况下,浮点数是不好的精确数值。这对小数尤其糟糕,因为共同值(如
0.1),没有一个二进制的表示。

正确的类型用于货币值是 java.math.BigDecimal 的。不幸的是,BigDecimal
是一个对象的类型,因此它不能与数据流工作。然而,BigDecimal
将与对象流工作,而这部分内容将在下一节讲解。

对象流(Object Streams)

对象流处理对象的二进制 I/O。

正如数据流支持的是基本数据类型的 I/O,对象流支持的对象
I/O。大多数,但不是全部,标准类支持他们的对象的序列化,都需要实现 Serializable 接口。

对象流类包括 ObjectInputStream 和 ObjectOutputStream 的。这些类实现的 ObjectInput 与 ObjectOutput 的,这些都是
DataInput 和DataOutput
的子接口。这意味着,所有包含在数据流中的基本数据类型 I/O
方法也在对象流中实现了。这样一个对象流可以包含基本数据类型值和对象值的混合。该ObjectStreams
例子说明了这一点。ObjectStreams 创建与 DataStreams
相同的应用程序。首先,价格现在是 BigDecimal 对象,以更好地代表分数值。其次,Calendar 对象被写入到数据文件中,指示发票日期。

public class ObjectStreams {
    static final String dataFile = "invoicedata";

    static final BigDecimal[] prices = { 
        new BigDecimal("19.99"), 
        new BigDecimal("9.99"),
        new BigDecimal("15.99"),
        new BigDecimal("3.99"),
        new BigDecimal("4.99") };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = { "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain" };

    public static void main(String[] args) 
        throws IOException, ClassNotFoundException {

        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new
                    BufferedOutputStream(new FileOutputStream(dataFile)));

            out.writeObject(Calendar.getInstance());
            for (int i = 0; i < prices.length; i ++) {
                out.writeObject(prices[i]);
                out.writeInt(units[i]);
                out.writeUTF(descs[i]);
            }
        } finally {
            out.close();
        }

        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(new
                    BufferedInputStream(new FileInputStream(dataFile)));

            Calendar date = null;
            BigDecimal price;
            int unit;
            String desc;
            BigDecimal total = new BigDecimal(0);

            date = (Calendar) in.readObject();

            System.out.format ("On %tA, %<tB %<te, %<tY:%n", date);

            try {
                while (true) {
                    price = (BigDecimal) in.readObject();
                    unit = in.readInt();
                    desc = in.readUTF();
                    System.out.format("You ordered %d units of %s at $%.2f%n",
                            unit, desc, price);
                    total = total.add(price.multiply(new BigDecimal(unit)));
                }
            } catch (EOFException e) {}
            System.out.format("For a TOTAL of: $%.2f%n", total);
        } finally {
            in.close();
        }
    }
}

如果的 readObject()
不返回预期的对象类型,试图将它转换为正确的类型可能会抛出一个 ClassNotFoundException。在这个简单的例子,这是不可能发生的,所以我们不要试图捕获异常。相反,我们通知编译器,我们已经意识到这个问题,添加
ClassNotFoundException 到主方法的 throws 子句中的。

复杂对象的 I/O

writeObject 和 readObject
方法简单易用,但它们包含了一些非常复杂的对象管理逻辑。这不像 Calendar
类,它只是封装了原始值。但许多对象包含其他对象的引用。如果 readObject
从流重构一个对象,它必须能够重建所有的原始对象所引用的对象。这些额外的对象可能有他们自己的引用,依此类推。在这种情况下,writeObject
遍历对象引用的整个网络,并将该网络中的所有对象写入流。因此,writeObject
单个调用可以导致大量的对象被写入流。

如下图所示,其中 writeObject 调用名为 a
的单个对象。这个对象包含对象的引用 b和 c,而 b 包含引用 d 和 e。调用
writeObject(a) 写入的不只是一个
a,还包括所有需要重新构成的这个网络中的其他4个对象。当通过 readObject
读回 a 时,其他四个对象也被读回,同时,所有的原始对象的引用被保留。

澳门新葡亰平台游戏网站 12

如果在同一个流的两个对象引用了同一个对象会发生什么?流只包含一个对象的一个拷贝,尽管它可以包含任何数量的对它的引用。因此,如果你明确地写一个对象到流两次,实际上只是写入了2此引用。例如,如果下面的代码写入一个对象
ob 两次到流:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

每个 writeObject 都对应一个 readObject, 所以从流里面读回的代码如下:

Object ob1 = in.readObject();
Object ob2 = in.readObject();

ob1 和 ob2 都是相同对象的引用。

然而,如果一个单独的对象被写入到两个不同的数据流,它被有效地复用 –
一个程序从两个流读回的将是两个不同的对象。

源码

本章例子的源码,可以在  中 com.waylau.essentialjava.io 包下找到。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图