JavaIO总结与巩固

澳门新葡亰网站注册 5

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

[TOC]

字节流(Byte Streams)

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

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

字节流

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

澳门新葡亰网站注册 1

字节流

用法

下面一例子 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默认抛出异常。

记得始终关闭流

不再需要一个流记得要关闭它,这点很重要。所以,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。

澳门新葡亰网站注册 3

字符流

字符流(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"))

用法

字符流类描述在 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位字节的值。

缓冲流

字符流使用字节流

字符流往往是对字节流的“包装”。字符流使用字节流来执行物理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"));

缓冲流(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这两个实现类。

澳门新葡亰网站注册 4

数据流

和缓冲流一样,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接口,用于自定义可继承类的序列化。

澳门新葡亰网站注册 5

序列化与反序列化相关类

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

扫描(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对象。

扫描

标准流的重定向

上面说道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");