Java I/O

1. File

        $File$ 类用于储存文件路径,可以代表一个特定文件或者一组文件的名称,如果是后者,可以通过 $list(\ )$ 获得一个字符数组。在不传入参数时,$list(\ )$ 方法返回全部列表,也可以接受一个 $FilenameFilter$ 类型的参数,实现目录过滤:

class DirFilter implements FilenameFilter {
    private Pattern pattern;
    public DirFilter(String regex) {
        pattern = Pattern.compile(regex);
    }

    public boolean accept(File dir, String name) {
        return pattern.matcher(name).matches();
    }
}

public class DirList {
    public static void main(String[] args) {
        System.out.println(Arrays.toString(new File(".").list(new DirFilter(args[0]))));
    }
}

        除了代表文件和目录之外,$File$ 对象还可以用于创建和删除目录,查看文件信息等:

方法 作用
$getAbsolutePath$ 返回绝对路径
$canRead$ 可读性
$canWrite$ 可写性
$getName$ 返回名称
$getParent$ 返回父目录
$getPath$ 返回路径
$lastModified$ 最后修改时间
$isFile$ 是否为文件
$isDirectory$ 是否为目录
$exists$ 是否存在
$mkdirs$ 创建
$delete$ 删除
$renameTo$ 重命名或移动文件

2. I/O

        编程语言的I/O类库常使用流这个概念,代表任何有能力产出数据的数据源或者有接受能力的接收端对象。“流”屏蔽了设备处理I/O操作的细节。在Java中,任何由 $InputStream$ 和 $Reader$ 的类都含有 $read(\ )$ 方法,任何由 $OutputStream$ 和 $Writer$ 派生的类都含有 $write(\ )$ 方法。$InputStream$ 和 $OutputStream$ 类型用于处理单个字节,$Reader$ 和 $Writer$ 类型用于处理字节数组。一般情况下都是通过叠加多个对象提供功能,也因此,当需要一个流的时候,往往需要创建多个对象。

2.1 InputStream/OutputStream

功能
$ByteArrayInputStream$ 将内存缓冲区当 $InputStream$ 使用
$StringBufferStream$ 将 $String$ 当 $InputStream$ 使用
$FileInputStream$ 读取文件信息
$PipedInputStream$ 写入相关的 $PipedOutputStream$ 数据,实现管道
$SequenceInputStream$ 将多个 $InputStream$ 转换为一个
$FilterInputStream$ 抽象类,作为装饰器接口,为其他 $InputStream$ 提供功能
功能
$ByteArrayOutputStream$ 使用内存缓冲区存储数据
$FileOutputStream$ 写入文件信息
$PipedOutputStream$ 写入其中的信息都会成为相关 $PipedInputStream$ 的输出,实现管道
$FilterOutputStream$ 抽象类,作为装饰器接口,为其他 $OutputStream$ 提供功能

2.2 FilterInputStream/FilterOutputStream

        JavaI/O类内存在 $Filter$ 类作为所有装饰器类的基类。通过装饰器模式,我们可以根据需求定制I/O类。当然,这也带来了麻烦,因为我们需要创建许多类才能得到所需的对象。

功能
$DataInputStream$ 搭配 $DataOutputStream$ 使用,按照可移植方式从流读取基本数据类型
$BufferedInputStream$ 使用缓冲区暂时存储信息,防止每次都进行实际读操作
$LineNumberInputStream$ 跟踪输入流中的行号
$PushbackInputStream$ 可以回退读到的最后一个字符
功能
$DataOutputStream$ 搭配 $DataInputStream$ 使用,按照可移植方式向流中写入基本数据类型
$PrintStream$ 产生格式化输出
$BufferedOutputStream$ 使用缓冲区暂时存储信息,防止每次都进行实际写操作

2.3 Reader/Writer

        $Reader$ 和 $Writer$ 提供了兼容Unicode与面向字符的I/O功能。如果要把字节类和字符类结合起来,需要使用适配器,$InputStreamReader$ 可以将 $InputStream$ 转为 $Reader$ ,$OutputStreamWriter$ 可以将 $OutputStream$ 转为 $Writer$ 。几乎所有原始的I/O流类都有相应的 $Reader$ 和 $Writer$ ,在大部分场合,我们通常会倾向于用他们。当然,有时候,比如 $java.util.zip$ 类库,是面向字节的,因此需要使用 $InputStream$ 和 $OutputStream$ 。

来源 对应的类
$InputStream$ $Reader$
$OutputStream$ $Writer$
$FileInputStream$ $FileReader$
$FileOutputStream$ $FileWriter$
$StringBufferInputStream$ $StringReader$
$N/A$ $StringWriter$
$ByteArrayInputStream$ $CharArrayReader$
$ByteArrayOutputStream$ $CharArrayWriter$
$PipedInputStream$ $PipedReader$
$PipedOutputStream$ $PipedWriter$

        对应 $FilterInputStream$ 和 $FilterOutputStream$ ,$Reader$ 和 $Writer$ 也有对应的结构。

来源 对应的类
$FilterInputStream$ $FilterReader$
$FilterOutputStream$ $FilterWriter$
$BufferedInputStream$ $BufferedReader$
$BufferedOutputStream$ $BufferedWriter$
$DataInputStream$ $DataInputStream$
$PrintStream$ $PrintWriter$
$LineNumberInputStream$ $LineNumberReader$
$StreamTokenizer$ $StreamTokenizer$
$PushbackInputStream$ $PushbackReader$

        可以发现 $DataInputStream$ 是相同的,但要注意的是,使用 $DataInputStream.readLine(\ )$ 方法会触发编译器的警告,应该改为使用 $BufferedReader.readLine(\ )$ 。尽管 $BufferedOutputStream$ 是 $FilterOutputStream$ 的子类,但是 $BufferedWriter$ 并不是 $FilterWriter$ 的子类,$FilterWriter$ 是一个抽象类。

2.4 RadomAccessFile

        $RadomAccessFile$ 适用于由大小已知的记录组成的文件,可以通过 $seek(\ )$ 方法移动指针,实现读取或修改特定位置的记录,它并非 $InputStream$ 或 $OutputStream$ 继承层次的一部分,甚至不使用其中已有的功能,和 $DataInputStream$ 与 $DataOutputStream$ 一样实现了 $DataInput$ 和 $DataOutput$ 接口。$RadomAccessFile$ 支持读写功能,可以通过 $getFilePointer(\ )$ 方法获取文件指针位置,$length(\ )$ 获取文件长度。在JDK 1.4之后,$RadomAccessFile$ 的大部分功能被 $nio$ 存储映射文件替代。

3. I/O流的使用

3.1 文件输入

        $FileInputReader$ 用于文件字符输入,如果要提高速度,可以使用 $BufferedReader$ 作为参数构造,而且 $BufferedReader$ 也提供了 $readLine(\ )$ 方法。

public class BufferedInputFile {
    public static String read(String filename) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while ((s = in.readLine()) != null) {
            sb.append(s + "\n");
        }
        in.close();
        return sb.toString();
    }
}

3.2 格式化内存输入

        $DataInputStream$ 是一个面向字节的I/O类,可以读取格式化数据。

public class FormattedMemoryInput {
    public static void main(String[] args) {
        DataInputStream in = new DataInputStream(new ByteArrayInputStream("read test".getBytes()));
        while (in.available() != 0) {
            System.out.println((char) in.readByte());
        }
    }
}

        $ByteArrayInputStream$ 接受一个字节数组,传递给 $DataInputStream$ 。

3.3 文件输出

        $FileWriter$ 可以向文件写入数据,可以通过 $BufferedWriter$ 进行缓冲输出。

public class BasicFileOutput {
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new StringReader("write test"));
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(args[0])));
        String s;
        while ((s = in.readLine()) != null) {
            out.println(s + "\n");
        }
        out.close();
    }
}

        $PrintWriter$ 中存在一个辅助构造器,可以在创建文本文件并向其写入时自动进行缓冲,从而避免了装饰操作,只需以如下方式调用:

PrintWriter out = new PrintWriter(args[0]);

        这样的写法等价于上面的写法。

3.4 数据存储和恢复

        我们可以通过 $DataInputStream$ 写入数据,并通过 $DataOutputStream$ 恢复数据。

public class StoringAndRecoveringData {
    public static void main(String[] args) throws IOException {
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("Data.txt")));
        out.writerUTF("Write Test");
        out.close();
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("Data.txt")));
        System.out.println(in.readUTF());
    }
}

3.5 随机访问

        $RandomAccessFile$ 类类似于组合了 $DataInputStream$ 和 $DataOutputStream$ ,因为他们使用了相同的接口。在使用 $RandomAccessFile$ 之前必须先知道文件排版才能进行正确操作。$RandomAccessFile$ 构造函数的第二个参数接受一个字符串,$r$ 、 $w$ 、 $rw$ 分别对应只读、只写和读写。

public class UsingRandomAccessFile {
    public static void display(String filename) throws IOException {
        RandomAccessFile rf = new RandomAccessFile(filename, "r");
        for (int i = 0; i < 7; i++)
            System.out.println("Value " + i + ": " + rf.readDouble());
        System.out.println(rf.readUTF());
        rf.close();
    }

    public static void main(String[] args) throws IOException {
        RandomAccessFile rf = new RandomAccessFile(args[0], "rw");
        for (int i = 0; i < 7; i++)
            rf.writeDouble(i * 1.414);
        rf.writeUTF("End of the file");
        rf.close();
        display(args[0]);
        rf = new RandomAccessFile(args[0], "rw");
        rf.seek(5 * 8);
        rf.writeDouble(47.001);
        rf.close();
        display();
    }
}

4. 标准I/O

        通过标准I/O,我们可以很容易的把程序串联起来。标准输入可以是程序输入的来源,标准输出也可以接受程序的输出,同样的,程序的所有错误信息也可以发送到标准错误。参照标准I/O模型,Java提供了 $System.in$ , $System.out$ 和 $System.err$ 。其中 $System.out$ 和 $System.err$ 被包装为 $PrintStream$ ;$System.in$ 则只是简单的 $InputStream$ ,因此在使用前要进行包装。

4.1 读取

        通常我们会用 $readLine(\ )$ 读取一行的输入,因此可以使用 $BufferedReader$ 进行包装。

public class Echo {
    public static void main(String[] args) throws IOException {
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = stdin.readLine()) != null && s.length() != 0) {
            System.out.println(s);
        }
    }
}

4.2 使用PrintWriter

        默认情况下 $System.out$ 和 $System.err$ 被包装为 $PrintStream$ ,但是 $PrintWriter$ 有一个接受 $OutputStream$ 的构造器,通过该构造器可以将 $System.in$ 和 $System.err$ 包装为 $PrintWriter$ 。要注意接受两个参数的构造器,并将第二个参数设置为 $true$ 表示在每次调用 $println(\ )$ 方法后自动清空,否则可能会看不到输出。

public class ChangeSystemOut {
    public static void main(String[] args) {
        PrintWriter out = new PrintWriter(System.out, true);
    }
}

4.3 重定向

        Java的 $System$ 类提供了一些静态方法,允许我们对标准I/O进行重定向。重定向在产生大量输出的程序中十分有用,可以通过 $System.setIn$ , $System.setOut$ 和 $System.setErr$ 方法进行重定向。

public class Redirecting {
    public static void main(String[] args) throws IOException {
        PrintStream console = System.out;
        BufferedInputStream in = new BufferedInputStream(new FileInputStream(args[0]));
        PrintStream out = new PrintStream(new BufferedOutputStream("test.out"));
        System.setIn(in);
        System.setOut(out);
        System.setErr(out);
        out.close();
        System.setOut(console);
        System.setErr(console);
    }
}

5. 新I/O

        JDK 1.4之后引入的 $java.nio.*$ 包引入了新的I/O库,目的是为了提高速度。当然,旧的I/O包已经使用其重新实现过。速度的提高来源于使用了更接近于系统I/O的方式:通道和缓冲器。数据存储在通道上,缓冲器从通道中获取信息,我们再从缓冲器中获取信息,而写过程就是反过来。唯一一个直接与通道交互的缓冲器是 $ByteBuffer$ 。旧I/O中的 $FileInputStream$ 、$FileOutputStream$ 和 $RandomAccessFile$ 。$Reader$ 和 $Writer$ 不能用于产生通道,但是通过 $java.nio.channels.Channels$ 类,可以在通道中产生 $Reader$ 和 $Writer$ 。

public class GetChannel {
    public static void main(String[] args) throws IOException {
        FileChannel fc = new FileOutputStream("test.txt").getChannel();
        fc.write(ByteBuffer.wrap("Write test 1".getBytes()));
        fc.close();
        fc = new RandomAccessFile("test.txt", "rw").getChannel();
        fc.position(fc.size());
        fc.write(ByteBuffer.wrap("Write test 2".getBytes()));
        fc.close();
        fc = new FileInputStream("test.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        fc.read(buffer);
        buffer.flip();
        while (buffer.hasRemaining()) System.out.println((char) buffer.get());
    }
}

        $getChannel(\ )$ 产生一个 $FileChannel$ 对象,可以向其传送 $ByteBuffer$ 对象用于读写。在调用了 $read(\ )$ 向 $ByteBuffer$ 存储字节时,需要再调用 $flip(\ )$ 方法使得 $ByteBuffer$ 进入可以被读取的状态。如果需要进一步调用 $read(\ )$ 进行写入时,需要先调用 $clear(\ )$ 方法使 $ByteBuffer$ 进入可以被写入的状态。

public class TransferTo {
    public static void main(String[] args) throws Exception {
        FileChannel in = new FileInputStream(args[0]).getChannel(),
                    out = new FileOutputStream(args[1]).getChannel();
        in.transferTo(0, in.size(), out);
    }
}

        $transferTo(\ )$ 和 $transferFrom(\ )$ 允许我们直接连接两个通道,从而在通道间进行数据传输。

5.1 数据转换

        从 $ByteBuffer$ 中读取字节时读取到的是 $byte$ 类型的数据。如果要读取字符型,可以通过类型转换。$ByteBuffer.asCharBuffer(\ )$ 返回一个 $CharBuffer$ 对象,可以利用该方法进行更方便的数据转换。

public class BufferToText {
    public static void main(String[] args) throws IOException {
        FileChannel fc = new FileOutputStream("test.txt").getChannel();
        fc.write(ByteBuffer.wrap("Write test".getBytes()));
        fc.close();
        fc = new FileInputStream("test.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        fc.read(buffer);
        buffer.flip();
        System.out.println(buffer.asCharBuffer()); // ????
        buffer.rewind();
        String encoding = System.getProperty("file.encoding");
        System.out.println(
            "Decoded using " + encoding + ": " + Charset.forName(encoding).decode(buffer)); // Decoded using UTF-8: Write test
        fc = new FileOutputStream("test.txt").getChannel();
        fc.write(ByteBuffer.wrap("write test".getBytes(StandardCharsets.UTF_16BE)));
        fc.close();
        fc = new FileInputStream("test.txt").getChannel();
        buffer.clear();
        fc.read(buffer);
        buffer.flip();
        System.out.println(buffer.asCharBuffer()); // Write test
        fc = new FileOutputStream("test.txt").getChannel();
        buffer = ByteBuffer.allocate(24);
        buffer.asCharBuffer().put("write test");
        fc.write(buffer);
        fc.close();
        fc = new FileInputStream("test.txt").getChannel();
        buffer.clear();
        fc.read(buffer);
        buffer.flip();
        System.out.println(buffer.asCharBuffer());  // Write test
    }
}

        可以发现,如果要使用 $asCharBuffer(\ )$ 方法,必须要使用UTF-8编码后写入或者在读出时使用UTF-8编码格式读出。$java.nip.charset.Charset$ 类提供了数据编码的功能。

5.2 获取基本类型

public class GetData {
    public static void main(String[] args) throws IOException {
        ByteBuffer bb = ByteBuffer.allocate(1024);
        int i = 0;
        while (i++ < bb.limit()) if (bb.get() != 0) System.out.print("nonzero");
        System.out.println("i = " + i);
        bb.rewind();
        bb.asCharBuffer().put("Howdy!");
        char c;
        while ((c = bb.getChar()) != 0) System.out.println(c + " ");
        bb.rewind();
        bb.asShortBuffer().put((short) 471142);
        System.out.println(bb.getShort());
        bb.rewind();
        bb.asIntBuffer().put(99471142);
        System.out.println(bb.getInt());
        bb.rewind();
        bb.asLongBuffer().put(99471142);
        System.out.println(bb.getLong());
        bb.rewind();
        bb.asFloatBuffer().put(99471142);
        System.out.println(bb.getFloat());
        bb.rewind();
        bb.asDoubleBuffer().put(99471142);
        System.out.println(bb.getDouble());
        bb.rewind();
    }
}

        通过 $asCharBuffer(\ )$ 、 $asShortBuffer(\ )$ 、$asIntBuffer(\ )$ 等方法向 $ByteBuffer$ 中写入数据,再使用 $getChar(\ )$ 、$getShort(\ )$ 、$getInt(\ )$ 等方法读出。

5.3 缓冲器

        视图缓冲器可以让我们通过某个特定的基本数据类型的视图查看其底层 $ByteBuffer$ ,对视图的任意修改都会映射到 $ByteBuffer$ 。视图还允许我们从 $ByteBuffer$ 一次一次地或者通过数组成批地获取基本类型。

public class Endians {
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[12]);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
        bb.rewind();
        bb.order(ByteOrder.BIG_ENDIAN);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
        bb.rewind();
        bb.order(ByteOrder.LITTLE_ENDIAN);
        bb.asCharBuffer.put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
    }
}

        通过 $CharBuffer$ 视图,我们可以将 $charArray$ 插入 $ByteBuffer$ 中。利用 $ByteBuffer.order(\ )$ 方法,我们可以设置字节排序方式为大端序或者小端序。
        $ByteBuffer$ 是数据进出通道的唯一方式。虽然我们不能将基本类型转换为 $ByteBuffer$ ,但是可以通过视图缓冲器将基本类型从 $ByteBuffer$ 中移入和移出。
        $Buffer$ 由数据和可以高效地访问及操纵这些数据的四个索引:$mark$ , $position$ , $limit$ 和 $capacity$ 组成,分别对应的操作方法有:

方法 作用
$capacity(\ )$ 缓冲区容量
$clear(\ )$ 清空缓冲区,$position$ 清零,$limit$ 设为容量
$flip(\ )$ $limit$ 设为 $position$ ,$position$ 清零
$limit(\ )$ 返回 $limit$ ,或者设置 $limit$
$mark(\ )$ $mark$ 设为 $position$
$reset(\ )$ 将 $position$ 设为 $mark$
$position(\ )$ 返回 $position$
$remaining(\ )$ 返回 $limit - position$
$hasRemaining(\ )$ 若有介于 $limit$ 和 $position$ 之间的元素,返回 $true$

        缓冲器会先将数据存储在内存中,如果遇到那些因为过大而不能存储在内存中的文件,可以使用内存映射文件。通过内存映射文件,我们可以假定整个文件都处于内存中,可以使用类似于数组的方式进行访问。

public class LargeMappedFiles {
    private static final int length = 0x8FFFFFF;

    public static void main(String[] args) throws Exception {
        MappedByteBuffer out =
            new RandomAccessFile("test.txt", "rw")
                .getChannel()
                .map(FileChannel.MapMode.READ_WRITE, 0, length);
        for (int i = 0; i < length; i++)
            out.put((byte) 'x');
        System.out.println("Finished!");
        for (int i = length / 2; i < length / 2 + 6; i++)
            System.out.println((char) out.get(i));
    }
}

        通过 $FileChannel.map(\ )$ 方法,我们可以获取一个 $MappedByByteBuffer$ 对象。这种特殊的缓冲器会将某个大文件的较小部分映射到内存中,也因此我们在创建的时候要指定初始位置和映射区长度。$MappedByByteBuffer$ 是 $ByteBuffer$ 的子类,因此可以使用它的所有方法。

5.4 文件锁

public class FileLocking {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("test.txt");
        FileLock fl = fos.getChannel().tryLock();
        if (fl != null) {
            System.out.println("Locked File");
            TimeUnit.MILLISECONDS.sleep(100);
            fl.release();
            System.out.println("Released Lock");
        }
        fos.close();
    }
}

        通过 $FileChannel.tryLock(\ )$ 、$FileChannel.lock(\ )$ 和 $FileChannel.release(\ )$ 方法可以实现对文件的上锁和解锁。 $tryLock(\ )$ 是非阻塞的,这意味着如果没有获得锁的话会直接返回。相反的,$lock(\ )$ 是阻塞式的,会一直等待到获得锁。$tryLock(\ )$ 和 $lock(\ )$ 都可以接受参数,实现对文件的部分上锁,将第三个参数设置为 $true$ 还可以设置共享锁。共享锁的实现需要依赖于操作系统,通过 $FileLock.isShared(\ )$ 可以判断一个锁是共享锁还是独占锁。

6. 压缩

        I/O类库支持读写压缩格式的数据流,这些类是由 $InputStream$ 和 $OutputStream$ 派生而来的。

压缩类 功能
$CheckedInputStream$ $GetCheckSum(\ )$ 为任何 $InputStream$ 产生校验和
$CheckedOutputStream$ $GetCheckSum(\ )$ 为任何 $OutputStream$ 产生校验和
$DeflaterOutputStream$ 压缩类基类
$ZipOutputStream$ 将数据压缩为Zip文件格式
$GZIPOutputStream$ 将数据压缩为GZIP文件格式
$InflaterInputStream$ 解压缩类基类
$ZipInputStream$ 解压缩Zip文件格式
$GZIPInputStream$ 解压缩GZIP文件格式

        Zip格式的Java类库更加全面,可以用于保存多个文件。

public class ZipCompress {
    public static void main(String[] args) throws IOException {
        FileOutputStream f = new FileOutputStream("test.zip");
        CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
        ZipOutputStream zos = new ZipOutputStream(csum);
        BufferedOutputStream out = new BufferedOutputStream(zos);
        zos.setComment("A test of Java Zipping");
        for (String arg : args) {
            System.out.println("Wrting file " + arg);
            BufferedReader in = new BufferedReader(new FileReader(arg));
            zos.putNextEntry(new ZipEntry(arg));
            int c;
            while ((c = in.read()) != -1) out.write(c);
            in.close();
            out.flush();
        }
        out.close();
        System.out.println("Checksum: " + csum.getChecksum().getValue());
        System.out.println("Reading file");
        FileInputStream fi = new FileInputStream("test.zip");
        CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());
        ZipInputStream in2 = new ZipInputStream(csumi);
        BufferedInputStream bis = new BufferedInputStream(in2);
        ZipEntry ze;
        while ((ze = in2.getNextEntry()) != null) {
            System.out.println("Reading file " + ze);
            int x;
            while ((x = bis.read()) != -1) System.out.print(x + " ");
        }
        if (args.length == 1) System.out.println("Checksum: " + csumi.getChecksum().getValue());
        bis.close();
        ZipFile zf = new ZipFile("test.zip");
        Enumeration e = zf.entries();
        while (e.hasMoreElements()) {
            ZipEntry ze2 = (ZipEntry) e.nextElement();
            System.out.println("File: " + ze2);
        }
    }
}

        $CheckSum$ 类提供了用于计算和校验文件的检验和方法,可以选择更快的 $Adler32$ 或者更准确的 $CRC32$ 类型。对于每一个要加入压缩档案的文件,都要调用 $putNextEntry(\ )$ 方法,传递一个 $ZipEntry$ 对象。通过 $ZipEntry$ 对象可以获得压缩文件的信息,比如名字、压缩后的大小、解压后的大小、日期、校验和等。虽然 $CheckSum$ 支持 $Adler32$ 和 $CRC32$ ,但是 $ZipEntry$ 类只支持 $CRC32$ 。如果要解压缩文件,$getNextEntry(\ )$ 方法可以获得 $ZipEntry$ ;也可以通过 $ZipFile$ 对象,该对象包含的 $entries(\ )$ 方法可以返回一个包含 $ZipEntry$ 的枚举。读取校验和要求拥有至少一个与之关联的 $CheckSum$ 对象,当然 $CheckedInputStream$ 和 $CheckedOutputStream$ 对象也是可以的。
        Zip格式也可以应用于Jar文件中。Jar文件由一组压缩文件组成,还包括一张描述了这些压缩文件的清单。可以通过命令行的方式调用Jar文件:

jar [options] destination [manifest] inputfile(s)
$options$ 功能
$c$ 创建新的或者空的压缩文档
$t$ 列出目录表
$x$ 解压所有文件
$x\ \ file$ 解压该文件
$f$ 指定一个文件名,从该文件中读取输入和输出(未指定则为标准I/O
$m$ 将文件清单名字设为第一个参数
$v$ 产生详细输出
$O$ 储存文件但不压缩
$M$ 不自动创建文件清单

        Jar工具的功能没有Zip那样强大,比如不能对已有的Jar文件进行修改,只能重新创建。但是Jar文件具有跨平台的便利性,只需要在一个平台上创建,就能被其他所有平台访问。

7. 对象序列化

        对象序列化会将实现了 $Serializable$ 接口的对象转换为字节序列,并且能够在之后由这个序列重新恢复为对象,而且通过序列化还可以跨平台传输对象。利用序列化可以实现轻量级持久性,即对象的声明周期并不取决于程序,因为对象可以通过序列化存储在磁盘中,然后在重新调用程序时恢复。对象序列化可以追踪对象内包含的引用,并保存那些对象。将一个序列恢复为对象时,需要注意的是必须存在对应的 $Class$ 对象,否则就会产生 $ClassNotFoundException$ 异常。要想序列化一个对象,首先要创建一些 $OutputStream$ ,并将它们封装在 $ObjectOutputStream$ 中,通过 $writeObject(\ )$ 方法将其序列化。而将一个序列还原为一个对象,则需要使用 $ObjectInputStream$ 封装 $InputStream$ 并调用 $readObject(\ )$ 方法,最终会得到一个 $Object$ 对象,需要进行向上转型。在一些特殊情况下,我们希望实现对序列化的控制,可以通过实现 $Externalizable$ 接口,$Externalizable$ 接口继承了 $Serializable$ 接口,提供了 $writeExternal(\ )$ 和 $readExternal(\ )$ 方法,会在序列化的过程中被自动调用。
        通过 $transient$ 关键字,可以将某个特定的子对象不进行序列化,这对于一些敏感信息比如密码等十分有用。由于 $Externalizable$ 默认不保存字段,因此 $transient$ 只能与 $Serializable$ 一起使用。
        如果不想使用 $Externalizable$ 接口,那么可以实现 $Serializable$ 接口并添加以下方法:

private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjecetInputStream stream) throws IOException, ClassNotFoundException;

        当对象被序列化和反序列化的时候就会自动调用这两个方法。在方法内部,还可以调用 $ObjectOutputStream.defaultWriteObject(\ )$ 和 $ObjectInputStream.defaultReadObject(\ )$ 方法执行默认序列化和反序列方法。
        $Class$ 对象是序列化的,因此也可以使用序列化。如果想要序列化 $static$ 字段,那么需要声明并实现:

serializeStaticState(ObjectOutputStream stream) throws IOException;
deserializaStaticState(ObjectInputStream stream) throws IOException;

        这两个方法会在序列化和反序列化过程中被调用,从而序列化 $static$ 字段。

Java I/O