Java I/O

简介: Java I/O标签 : Java基础 Java的I/O功能通过java.io包下的类和接口来支持,在java.

Java I/O

标签 : Java基础


Java的I/O功能通过java.io包下的类和接口来支持,在java.io包下主要包括输入/输出两种IO流,每种输入/输出流又可分为字节流字符流两大类.字节流支持以字节(8位)为单位的IO操作,而字符流则以字符(16位-Java中)为单位进行IO操作.
除此之外,Java的IO流还使用装饰者模式,将IO流分成底层节点流和上层处理流,节点流直接和底层的物理存储节点关联,虽然不同的物理节点获取的节点流可能存在差异,但程序可以把不同的物理节点包装成统一处理流,从而允许程序使用统一的IO代码来操作不同的物理节点.


File

Java使用java.io.File来提供对底层文件的抽象, File能够新建/删除/重命名文件和目录,但不能访问文件内容本身(需要使用IO流).

File类提供了很多实用的方法,我们可以将其分为以下几类:

  • 文件名相关方法
    getAbsoluteFile() getAbsolutePath() getName() getParent() getParentFile() getPath() renameTo(File dest)
  • 文件状态相关方法
    exists() canExecute() canRead() canWrite() isFile() isDirectory() isAbsolute()(UNIX/Linux中是否以/开头) isHidden() lastModified() length()

  • 文件操作
    createNewFile() createTempFile(String prefix, String suffix) delete() deleteOnExit() setExecutable(boolean executable) setReadOnly()

  • 目录操作
    如: mkdir() mkdirs() list() list(FilenameFilter filter) listFiles() listFiles(FileFilter filter) listRoots()
    这儿只是大致介绍File类提供的功能,细节请参考JDK文档.

/**
 * 模拟tree命令
 *
 * @author jifang
 * @since 16/1/6下午5:20.
 */
public class Tree {

    public static void main(String[] args) {
        // 打印当前目录
        if (args == null || args.length == 0) {
            displayFiles(new File("."), 0);
        } else {
            displayFiles(new File(args[0]), 0);
        }
    }

    private static void displayFiles(File directory, int depth) {
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                for (int i = 0; i < depth - 1; ++i) {
                    System.out.print("|   ");
                }
                if (depth != 0) {
                    System.out.print("|-- ");
                }
                System.out.println(file.getName());

                if (file.isDirectory()) {
                    displayFiles(file, depth + 1);
                }
            }
        }
    }
}

I/O流体系


InputStream / Reader

InputStreamReader是所有输入流的抽象基类, 虽然本身不能创建实例来执行输入, 但作为所有输入流的模板, 他们的方法是所有输入流都通用的.
InputStream包含如下三个方法:

  • int read()
  • int read(byte[] b)
  • int read(byte[] b, int off, int len)

Reader包含如下三个方法:

  • int read()
  • int read(char[] cbuf)
  • int read(char[] cbuf, int off, int len)

对比InputStreamReader所提供的方法, 这两个类的功能基本相同, 只是一个提供了字节byte读, 一个提供了字符char读.
除此之外, InputStreamReader还支持几个方法来移动记录指针以实现跳读, 重复读等操作.

方法 释义
void mark(int readlimit) Marks the current position in this input stream.
boolean markSupported() Tests if this input stream supports the mark and reset methods.
void reset() Repositions this stream to the position at the time the mark method was last called on this input stream.
long skip(long n) Skips over and discards n bytes of data from this input stream.

OutputStream / Writer

两个流都提供了如下三个方法:

  • void write(byte[]/char[] b)
  • void write(byte[]/char[] b, int off, int len)
  • void write(int b)

因为字符流直接以字符作为操作单位, 所以Writer可以用字符串来代替字符数组(以String对象作为参数). Writer还包含如下方法:

  • void write(String str)
  • void write(String str, int off, int len)
  • Writer append(char c)
  • Writer append(CharSequence csq)
  • Writer append(CharSequence csq, int start, int end)
/**
 * 模拟copy功能
 *
 * @author jifang
 * @since 16/1/6下午7:52.
 */
public class Copy {

    private static final int BUFFER_LENGTH = 1024;

    public static void main(String[] args) throws IOException {
        if (args == null || args.length != 2) {
            throw new RuntimeException("Use like: copy <src-file> <dest-file>");
        }

        Reader reader = new FileReader(args[0]);
        Writer writer = new FileWriter(args[1]);
        char[] buffer = new char[BUFFER_LENGTH];
        int count;
        while ((count = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, count);
        }
        reader.close();
        writer.close();
    }
}

小结:

  • 使用Java IO流执行完IO后,不能忘记关闭流,关闭流才可以保证物理资源会被回收(关闭输出流还可以保证将缓冲区中的数据flush到物理节点中,因为在执行close()方法之前,会自动执行输出流的flush()方法).
  • Java 1.7改写了所有的IO资源类,他们都实现了AutoCloseable接口,因此都可通过自动关闭的try语句来自动关闭这些流.
  • 通常来说,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,因此字节流可以处理所有所有的二进制文件.
  • 如果使用字节流来处理文本文件,则需要将这些字节转换成字符,增加了编程的复杂度.所以有一个规则:如果进行IO是文本内容,则应该考虑使用字符流;如果进行IO的是二进制内容, 则应该考虑使用字节流.

节点流/处理流

节点流的的构造参数是物理IO节点,而处理流的构造参数是已经存在的流.

常用节点流:

\ InputStream OutputStream Reader Writer
文件 FileInputStream FileOutputStrean FileReader FileWriter
数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
字符串 * * StringReader StringWriter
管道 PipedInputStream PipedOutputStream PipedReader PipedWriter

常用处理流:

\ InputStream OutputStream Reader Writer
缓冲流 BufferedInputStrean BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter * *
数据流 DataInputStream DataOutputStream * *
对象流 ObjectInputStream ObjectOutStream * *
合并流 SequenceInputStream * * *
回退流 PushbackInputStream * PushbackReader *
打印流 PrintStream * PrintWriter *
  • 处理流与节点流相比可以隐藏底层节点流的差异,对外提供更加统一的IO方法,让开发人员只需关心高级流的操作(使用更加简单, 执行效率更高).
  • 使用处理流的思路:使用处理流来包装节点流,程序通过处理流执行IO,让节点流与底层IO设备/文件交互.

在使用处理流包装了节点流之后, 关闭输入/输出流资源时, 只要关闭最上层的处理流即可.关闭最上层的处理流时, 系统会自动关闭该处理流包装的节点流.


数组作为IO节点

上表中列出了一种以数组为物理节点的节点流,字节流以字节数组为节点ByteArrayInputStream/ByteArrayOutputStream,字符流以字符数组为节点CharArrayReader/CharArrayWriter, 这种数组流除了在创建节点流需要传入数组外,用法上与其他节点流完全类似.数组流类似还可以使用字符串作为物理节点,用于实现从字符串中读取字符串(String)java.io.StringReader, 或将内容写入字符串(StringBuffer)java.io.StringWriter.

/**
 * @author jifang
 * @since 16/1/7上午9:48.
 */
public class StringReaderWriter {

    @Test
    public void testStringReaderWriter() throws IOException {
        String string = "锦瑟无端五十弦,一弦一柱思华年。" +
                "庄生晓梦迷蝴蝶,望帝春心托杜鹃。" +
                "沧海月明珠有泪,蓝田日暖玉生烟。" +
                "此情可待成追忆?只是当时已惘然。";
        try (StringReader reader = new StringReader(string)) {
            char[] buffer = new char[32];
            int count;
            while ((count = reader.read(buffer)) != -1) {
                System.out.println(new String(buffer, 0, count));
            }
        }

        System.out.println("************");
        try (StringWriter writer = new StringWriter()) {
            writer.write("昨夜星辰昨夜风,");
            writer.append("画楼西畔桂堂东。");
            writer.append("身无彩凤双飞翼,");
            writer.append("心有灵犀一点通。");
            System.out.println(writer.toString());
        }
    }
}

缓冲流

BufferedInputStream BufferedOutputStream BufferedReader
BufferedWriter 四个缓冲流增加了缓冲功能, 可以提高IO效率, 但是需要使用flush()才可以将缓冲区的内容写入实际的物理节点.

/**
 * @author jifang
 * @since 16/1/7上午9:48.
 */
public class BufferWriterTest {

    @Test
    public void testBufferedWriter() throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("save.txt"))) {
            writer.write("昨夜星辰昨夜风,");
            writer.write("画楼西畔桂堂东。");
            writer.write("身无彩凤双飞翼,");
            writer.write("心有灵犀一点通。");
            writer.write("********");
            writer.flush();
        }
    }
}

转换流

Java I/O流体系提供了两个转换流, 用于实现将字节流转换成字符流:

  • java.io.InputStreamReader将字节输入流转换成字符输入流
  • java.io.OutputStreamWriter将字节输出流转换成字符输出流

回退流

在IO流体系中, 有两个特殊的流与众不同: PushbackInputStream PushbackReader 他们都提供了如下三个方法:

方法 释义
void unread(byte[]/char[] buf) Pushes back an array of bytes/characters by copying it to the front of the pushback buffer.
void unread(byte[]/char[] buf, int off, int len) Pushes back a portion of an array of bytes/characters by copying it to the front of the pushback buffer.
void unread(int b/c) Pushes back a single byte/character by copying it to the front of the pushback buffer.

这两个推回输入流都带有一个推回缓冲区, 当程序调用者两个的unread()方法时,系统将会把指定的数据推回到该缓冲区中, 而这两个流在每次调用read()方法时,都会先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后, 才会到原输入流中获取.
这样, 当程序创建该流时, 需要指定推回缓冲区大小(默认为1),如果在程序中推回的数据超出了推回缓冲区的大小, 则会抛出java.io.IOException: Pushback buffer overflow.


RandomAccessFile

java.io.RandomAccessFile与普通的Java I/O流不同的是, 他可以支持随机访问文件, 程序可以直接跳转到文件的任意地方读写数据(因此如果只是访问文件的部分内容,或向已存在的文件追加数据, RandomAccessFile是更好的选择).但RandomAccessFile也有一个局限就是只能读写文件, 不能读写其他IO节点.

RandomAccessFile对象包含了一个记录指针, 用以标识当前读写位置, 当程序新建一个RandomAccessFile对象时, 记录指针位于文件头, 当读/写n个字节后, 指针将会向后移动n个字节.除此之外RandomAccessFile还可以(前/后)自由移动该记录指针,RandomAccessFile提供了如下方法来操作文件记录指针:

方法 释义
long getFilePointer() Returns the current offset in this file.
void seek(long pos) Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs.

RandomAccessFile既可以读文件, 也可以写文件, 所以他提供了完全类似于InputStreamread()方法, 也提供了完全类似于OutputStreamwrite()方法, 此外他还包含了一系列的readXxx() writeXxx()来完成IO(其实这几个操作是DataInput DataOutput接口来提供的, DataInputStream/DataOutputStream ObjectInputStream/ObjectOutputStream也实现了这两个接口).

在构造一个RandomAccessFile时, 都需要提供一个String mode参数, 用于指定RandomAccessFile的访问模式, 该参数有如下取值:

mode 模式
"r" 只读
"rw" 读写, 如果文件不存在, 则创建
"rws" 读写,相对于”rw”,还要求对文件内容文件元数据的更新都同步写入底层存储数据
"rwd" 读写,相对于”rw”,要求对文件内容的更新同步写入底层存储数据

注意: RandomAccessFile不能直接向文件的指定位置插入数据,不然新插入的数据会覆盖文件的原内容.如果需要向指定的位置插入数据,程序需要先把指定插入点后的内容读入缓冲区, 等把需要插入的数据写入完成后, 再将缓冲区的内容追加到文件后面.

public void writePositionContent(RandomAccessFile file, long position, byte[] content) {
    try {
        // 首先将position到文件末尾的内容写入数组
        file.seek(position);
        ByteArrayOutputStream tmpStream = new ByteArrayOutputStream();
        byte[] tmpArray = new byte[1024];
        int readCount;
        while ((readCount = file.read(tmpArray)) != -1) {
            tmpStream.write(tmpArray, 0, readCount);
        }

        // 然后再回到position, 向其中写入内容
        file.seek(position);
        file.write(content);

        // 最后将暂存的内容写入文件
        file.write(tmpStream.toByteArray());
        tmpStream.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

对象序列化

序列化机制使得对象可以脱离程序的运行环境而独立存在: 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流, 从而可以把这种二进制流持久的保存在磁盘上, 或通过网络将这种二进制流传输到另一个网络节点. 其他程序一旦获取到了这个二进制流, 都可以将他恢复成原先的Java对象.

如果需要让某个对象支持序列化机制, 则必须让他的类是可序列化的(serializable). 该类必须实现如下接口之一:

  • java.io.Serializable
  • java.io.Externalizable

注意: 对象的类名, 实例变量(基本类型/数组/引用对象)都会被序列化; 而方法/类变量(static)/transient实例变量都不会序列化.


Serializable

使用Serialiable实现对象序列化只需实现这个接口即可(而这个接口只是一个Tag接口).

/**
 * @author jifang
 * @since 16/1/13下午7:50.
 */
public class Bean implements Serializable {
    private Boolean isUsed;

    private Double rate;

    private String name;

    public Bean(Boolean isUsed, Double rate, String name) {
        this.isUsed = isUsed;
        this.rate = rate;
        this.name = name;
    }

    public Boolean getIsUsed() {
        return isUsed;
    }

    public void setIsUsed(Boolean isUsed) {
        this.isUsed = isUsed;
    }

    public Double getRate() {
        return rate;
    }

    public void setRate(Double rate) {
        this.rate = rate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Bean{" +
                "isUsed=" + isUsed +
                ", rate=" + rate +
                ", name='" + name + '\'' +
                '}';
    }
}
/**
 * @author jifang
 * @since 16/1/13下午7:48.
 */
public class Serialization {

    @Test
    public void writeObject() throws IOException {
        try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("save.txt"))) {
            Bean bean = new Bean(true, 3.14, "fq");
            output.writeObject(bean);
        }
    }

    @Test
    public void readObject() throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("save.txt"))) {
            Bean bean = (Bean) input.readObject();
            System.out.println(bean);
        }
    }
}

注意:

  • 反序列化读取的仅仅是Java对象的数据, 而不是Java类, 因此采用反序列化恢复Java对象时,必须提供对象所属的类的class文件, 否则将会引发ClassNotFoundException.
  • 从上例看到: Bean类并没有提供无参构造器, 因此可以证明反序列化机制无须通过构造器来初始化对象.
  • 如果使用序列化机制向文件中写入了多个Java对象, 反序列化时必须按实际写入的顺序读取.

根据经验: 像Date BigInteger这样的值类应该实现Serializable接口,大多数的集合也应该如此. 但代表活动实体的类, 如线程池(Thread Pool), 一般不应该实现Serializable.


对象引用序列化

  • 如果某个类的成员变量不是基本类型或String类型,而是另一个引用类型, 那么这个引用类必须是可序列化的, 否则拥有该类型成员变量的类也是不可序列化的.
  • Java序列化算法
    • 所有保存到磁盘(或传输到网络中)的对象都有一个序列化编号.
    • 当程序试图序列化一个对象时, 程序将先检查该对象是否已经被序列化过, 只有该对象(在本次虚拟机的上下文Context中)从未被序列化过, 系统才会将该对象转换成字节序列并输出.
    • 如果某个对象已经序列化过(即使该对象的实例变量后来发生了改变), 程序将不再重新序列化该对象.
/**
 * @author jifang
 * @since 16/1/13下午7:48.
 */
public class Serialization {

    @Test
    public void testSerialization() throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("save.txt"));
             ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("save.txt"))) {
            Bean bean = new Bean(true, 3.14, "fq");
            output.writeObject(bean);
            bean.setName("feiqing");
            output.writeObject(bean);

            // 可以看到两个对象是完全一样的
            Bean readBean1 = (Bean) input.readObject();
            Bean readBean2 = (Bean) input.readObject();
            System.out.println(readBean1 == readBean2);
            System.out.println(readBean1);
            System.out.println(readBean2);
        }
    }

    @Test
    public void writeObject() throws IOException {
        try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("save.txt"))) {
            Bean bean = new Bean(true, 3.14, "bean");
            ComplexBean complexBean = new ComplexBean();
            complexBean.setName("complex_bean");
            complexBean.setRefBean(bean);
            output.writeObject(bean);

            // 在这里对complexBean中的refBean成员做了修改
            complexBean.getRefBean().setName("simple_bean");
            output.writeObject(complexBean);
        }
    }

    @Test
    public void readObject() throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("save.txt"))) {

            // 可以发现complex_bean内的refBean属性并未改变
            Bean bean = (Bean) input.readObject();
            ComplexBean complexBean = (ComplexBean) input.readObject();
            System.out.println("bean : " + bean + "\n" + "complexBean: " + complexBean);
            System.out.println(bean == complexBean.getRefBean());
        }
    }
}
/**
 * @author jifang
 * @since 15/12/31下午4:04.
 */
public class ComplexBean implements Serializable {

    private String name;

    private Bean refBean;

    public ComplexBean() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Bean getRefBean() {
        return refBean;
    }

    public void setRefBean(Bean refBean) {
        this.refBean = refBean;
    }

    @Override
    public String toString() {
        return "ComplexBean{" +
                "name='" + name + '\'' +
                ", refBean=" + refBean +
                '}';
    }
}

序列化版本

Java序列化机制允许为序列化的类提供一个private static final long serialVersionUID = xxxL;值, 该值用于标识该Java类的序列化版本; 一个类升级之后, 只要他的serialVersionUID值不变, 序列化机制也会把它们当成同一个序列化版本(由于提供了serialVersionUID之后JVM不需要再次计算该值,因此还有个小小的性能好处).

可以通过JDK提供的serialver工具来提取类的serialVersionUID值.

serialver com.fq.domain.Bean    #生成serialVersionUID值
serialver -show     #启动serialVersionUID的图形生成界面

如果不显式定义serialVersionUID的值,可能会造成以下问题:

  • 该值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类的版本不兼容而失败.
  • 不利于程序在不同JVM之间移植, 因为不同的编译器对该变量的计算策略可能不同, 而从造成类虽然没有改变, 但因为JVM的不同, 也会出现序列化版本不兼容而导致无法正确反序列化的现象.

自定义序列化

在一些的场景下, 如果一个类里面包含的某些变量不希望对其序列化(或某个实例变量是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException异常); 或者将来这个类的实现可能改动(最大程度的保持版本兼容), 或者我们需要自定义序列化规则, 在这种情景下我们可选择实用自定义序列化.


transient

通过在实例变量前面加transient关键字以指定Java序列化时无需理会该实例变量(注意: transient关键字只能用于修饰实例变量, 不可修饰Java其他成分).

transient修饰的实例变量被称为瞬时变量.

/**
 * 将ComplexBean的refBean属性设置不需要序列化
 *
 * @author jifang
 * @since 15/12/31下午4:04.
 */
public class ComplexBean implements Serializable {

    private static final long serialVersionUID = 7046068335702080988L;

    private String name;

    private transient Bean refBean;

    public ComplexBean() {
    }

    public ComplexBean(String name, Bean refBean) {
        this.name = name;
        this.refBean = refBean;
    }

    //... 
}
/**
 * @author jifang
 * @since 16/1/13下午7:48.
 */
public class Serialization {

    @Test
    public void writeObject() throws IOException {
        try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("save.txt"))) {
            ComplexBean complexBean = new ComplexBean("complex_bean", new Bean(true, 3.14, "bean"));
            output.writeObject(complexBean);
        }
    }

    @Test
    public void readObject() throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("save.txt"))) {
            // 可以发现complex_bean内的refBean属性为null
            ComplexBean complexBean = (ComplexBean) input.readObject();
            System.out.println("complexBean: " + complexBean);
        }
    }
}

readObject/writeObject

使用transient关键字修饰实例变量虽然简单, 但被transient修饰的实例变量将完全被隔离在序列化机制之外, 这样导致反序列化恢复Java对象时无法取得该实例变量的值. 因此, Java还提供了另一种自定义序列化机制,通过提供writeObject() readObject() readObjectNoData()等方法可以让程序控制如何序列化各实例变量, 甚至完全不序列化某些实例变量.

  • private void writeObject(java.io.ObjectOutputStream output) throws IOException;
    负责序列化类实例,以便readObject()可以恢复它;通过重写该方法, 可以实现完全控制该类的序列化机制,自主决定哪些实例变量需要实例化,需要怎样序列化. 在默认情况下, 该方法会调用output.defaultWriteObject()来保存Java对象的各实例变量, 从而可以实现序列化对象的目的.

  • private void readObject(java.io.ObjectInputStream input) throws IOException, ClassNotFoundException;
    负责反序列化类实例,通过重写该方法,可以完全控制该类的反序列化机制,自主决定需要反序列化哪些实例变量,以及如何进行反序列化.在默认情况下, 该方法会调用input.defaultReadObject()来恢复Java对象的非瞬时变量.
    一般readObject()应与writeObject()方法对应,如果writeObject()对Java对象的实例变量进行了一些处理, 则应该在readObject()方法中对其实例变量进行相应的反处理,以便正确恢复该对象.

/**
 * 自定义readObject, writeObject
 *
 * @author jifang
 * @since 16/1/13下午7:50.
 */
public class Bean implements Serializable {

    private static final long serialVersionUID = 2975296536292876992L;

    private boolean isUsed;

    private Double rate;

    private String name;

    public Bean() {
    }

    public Bean(boolean isUsed, Double rate, String name) {
        this.isUsed = isUsed;
        this.rate = rate;
        this.name = name;
    }

    private void writeObject(ObjectOutputStream output) throws IOException {
        // 将name实例变量值转为大写反转之后写入二进制流
        output.writeObject(new StringBuilder(this.name.toUpperCase()).reverse());
        // output.writeBoolean(isUsed);
        output.writeDouble(rate);
    }

    private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
        this.name = ((StringBuilder) input.readObject()).reverse().toString().toLowerCase();
        //this.isUsed = input.readBoolean();
        this.rate = input.readDouble();
    }

    // ...
}

由于我们可以自己定制序列化规则, 因此, 在网络传输中, 可以对对象实例进行加密, 在读出时自动解密, 这样即使在传输过程被截获, 获取到的也是加密后的值.但writeObject()方法的加密规则必须与readObject()的解密规则一致.

建议readObject() writeObject()的方法内首先调用defaultReadObject() defaultWriteObject();

  • private void readObjectNoData() throws ObjectStreamException;
    当序列化流不完整时, readObjectNoData()方法可以用来正确地反序列化对象.例如, 接收方使用的反序列化类的版本不同于发送方,或接收方版本扩展的类不是发送方版本扩展的类时,系统都会调用readObjectNoData()方法来初始化反序列化的对象.

  • private Object writeReplace() throws ObjectStreamException;
    Java序列化机制保证在序列化某个对象之前, 先调用对象的writeReplace()方法, 如果该方法返回另一个Java对象, 则系统转化为序列化另一个对象.

/**
 * 实际序列化的是ArrayList
 *
 * @author jifang
 * @since 16/1/13下午7:50.
 */
public class Bean implements Serializable {

    private static final long serialVersionUID = 2975296536292876992L;

    private boolean isUsed;

    private Double rate;

    private String name;


    private Object writeReplace() throws ObjectStreamException {
        List<String> list = new ArrayList<>();
        list.add("bean1");
        list.add("bean2");
        return list;
    }

    // ...
}
/**
 * @author jifang
 * @since 16/1/13下午7:48.
 */
public class Serialization {

    @Before
    public void writeObject() throws IOException {
        try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("save.txt"))) {
            output.writeObject(new Bean());
        }
    }

    @Test
    @SuppressWarnings(value = "unchecked")
    public void readObject() throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("save.txt"))) {
            List<String> list = (List<String>) input.readObject();
            for (String bean : list){
                System.out.println(bean);
            }
        }
    }
}

writeReplace()对应, 序列化还有一个readResolve()方法

  • private Object readResolve() throws ObjectStreamException;
    可以实现保护性复制整个对象, 这个方法会紧接着readObject()之后调用, 该方法的返回值将会替换原来反序列化的对象, 而原来readObject()反序列化的对象将会被立即丢弃.
    readResolve()方法在序列化单例类, 枚举类时尤其有用(细节请参考static, enum, 内部类与单例模式), 因此所有的单例类, 枚举类在实现序列化时都应该提供readResolve()方法.

readResolve()writeReplace()还可以使用其他的访问修饰符, 但建议使用private修饰.


Externalizable

实现Externalizable接口以实现对象序列化, 这种序列化方式完全由程序员决定存储和恢复对象数据的机制.该接口提供了如下两个方法:

  • public void writeExternal(ObjectOutput out) throws IOException;
    序列化
  • public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    反序列化
/**
 * @author jifang
 * @since 16/1/13下午7:50.
 */
public class Bean implements Externalizable {

    private static final long serialVersionUID = 2975296536292876992L;

    private boolean isUsed;

    private Double rate;

    private String name;

    public Bean() {
    }

    public Bean(boolean isUsed, Double rate, String name) {
        this.isUsed = isUsed;
        this.rate = rate;
        this.name = name;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeBoolean(isUsed);
        out.writeDouble(rate);
        out.writeObject(name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.isUsed = in.readBoolean();
        this.rate = in.readDouble();
        this.name = (String) in.readObject();
    }

    // ...
}

实现Externalizable接口序列化可以带来一定的性能提升,而且还可以完全自己的自定义序列化规则, 因此只有当默认的序列化形式(Serializable)能够合理地描述对象的逻辑状态时,才能实用默认的序列化形式(详见Effective Java第11/12章).


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

使用Runtime对象的exec()方法可以运行操作系统平台上的其他程序, 该方法返回一个Process对象, 代表由该Java程序启动的子进程.Process提供如下方法, 用于主进程和子进程进行通信:

方法 释义
InputStream getErrorStream() Returns the input stream connected to the error output of the subprocess.
InputStream getInputStream() Returns the input stream connected to the normal output of the subprocess.
OutputStream getOutputStream() Returns the output stream connected to the normal input of the subprocess.

注意: Input/Output是站在主进程角度来看的.

/**
 * @author jifang
 * @since 16/1/10下午8:45.
 */
public class ProcessCommunication {
    public static void main(String[] args) throws IOException {
        InputStream input = Runtime.getRuntime().exec("ls").getInputStream();
        System.out.println(CharStreams.toString(new InputStreamReader(input)));
    }
}

注意: 上面程序使用到了Guava的CharStreams, 其详细用法请参考我的下一篇博客Java I/O 扩展.暂时可在pom中添加如下依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

目录
相关文章
|
4月前
|
存储 监控 Java
Java输入输出:什么是NIO(New I/O)?
Java输入输出:什么是NIO(New I/O)?
33 1
|
7月前
|
Java
63.【Java I/O 操作】(一)
63.【Java I/O 操作】
39 0
|
1月前
|
Java
【Java】深入了解Java I/O 流
【Java】深入了解Java I/O 流
31 0
|
7月前
|
Java
63.【Java I/O 操作】(三)
63.【Java I/O 操作】
27 0
|
6月前
|
Java
Java I/O流知识点总结分享
Java I/O流知识点总结分享
|
6月前
|
Java 程序员 数据处理
【Java 多线程编程 | 从0到1】线程I/O模型
【Java 多线程编程 | 从0到1】线程I/O模型
85 0
|
7月前
|
Java
63.【Java I/O 操作】(四)
63.【Java I/O 操作】
31 0
|
7月前
|
Java
63.【Java I/O 操作】(二)
63.【Java I/O 操作】
34 0
|
8月前
|
存储 Java
[java]I/O
[java]I/O
44 0
|
8月前
|
存储 Java