JAVA I/O 之对象序列化

前言

​ 最近在看 《JAVA 编程思想》看到 I/O 这一章中有关对象序列化相关的内容之前不熟悉,于是借本文做一个整理与记录。

Serializable 接口

​ 在 Java 中要序列化一个对象非常简单,只需要该类实现了 Serializable 接口我们便能通过 I/O 流的方式来序列化与反序列化对象。

// 一个简单的 Person 类
public class Person implements Serializable {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void setName(String name) {this.name = name;}
    public void setAge(int age) {this.age = age;}
    public String getName() {return name;}
    public int getAge() {return age;}
}

public class Test {
    private static String outputFile = "./IOFiles/Person.obj";
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(
                                    new FileOutputStream(
                                            new File(outputFile)));
        Person person = new Person("Li Si", 20);
        // 将对象序列化
        out.writeObject(person);

        ObjectInputStream in = new ObjectInputStream(
                                    new FileInputStream(
                                            new File(outputFile)));
        // 恢复序列化的对象
        Person recoveredPerson = (Person)in.readObject();
        System.out.println("name: " + recoveredPerson.getName() + ", age: " + 			                             recoveredPerson.getAge());
    }
}

在这里插入图片描述

​ 可以看到实现了 Serializable 接口就能非常方便的进行对象的序列化与反序列化,实际上 Serializable 的序列化非常强大不单单是能序列化上述这个例子中的简单属性,事实上其在序列化的过程中会将对象中的引用一起序列化而如果引用对象也具有引用就会一直追溯下去,最终会建立一个对象的全景图或者说对象网,因此实现通过这种方式的序列化的对象我们可以在反序列化后直接使用不会出现对象中的引用对象不存在而出错的问题。

Externalizable

Serializable 接口很强大能够自动的序列化对象中的所有信息,但是有些时候我们不希望这样,比如对于密码这类必要重要的属性就喜欢能够不对其进行序列化,Java 通过实现 Externalizable 接口然后在 writeExternalreadExternal 方法中就指定需要序列化的内容就可以避免那些我们不希望被序列化的内容被序列化,具体见下面代码:

public class Account implements Externalizable {
    private String username;
    private String password;
	
    // 使用这种方式需要一个 public 的空构造器
    public Account(){}
    
    public Account(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 希望密码保密因此我们只序列化 username
        out.writeUTF(username);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        username = in.readUTF();
    }
   // setter and getter
}


public class Test {
    private static String outputFile = "./IOFiles/Account.obj";
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(
                                    new FileOutputStream(
                                            new File(outputFile)));
        Account account = new Account("USERNAME", "PASSWORD");
        // 将对象序列化
        out.writeObject(account);

        ObjectInputStream in = new ObjectInputStream(
                                    new FileInputStream(
                                            new File(outputFile)));
        // 恢复序列化的对象
        Account recoveredAccount = (Account)in.readObject();
        System.out.println("username: " + recoveredAccount.getUsername() + 
                            ", password: " + recoveredAccount.getPassword());
    }
}

在这里插入图片描述

​ 可以看到我们只序列化了 username 属性而 password 并没有被序列化,需要注意的是这种方式在恢复对象时与 Serializable 接口不同 Serializable 接口 直接从存储的二进制文件中构造对象,而 Externalizable 接口在恢复的过程中会调用默认的构造器因此我们需要显示的定义一个 public 的空构造器,还有需要注意的是我们需要在 writeExternal 方法中显示的写出我们需要序列化的属性然后在 readExternal 方法中按照相同的顺序写入反序列化的属性才能使得我们的序列化能够得到正确的结果,这种方式很像 Hadoop 中序列化数据的方式或者反过来 Hadoop 序列化数据的操作很像这种模式。

transient 关键字

​ 上述方法虽然可以达到只序列化我们想序列化的内容的目的,但是如果类中的属性非常多那么 write 和 read 两个方法写起来还是很费力的,在只有少数我们不需要序列化的内容的情况下我们可以使用 transient 来达到相同的目的,在实现 Serializable 接口的类中如果属性被 transient 关键字修饰那么其就不会被序列化,见下面代码:

// 还是我们的 Account 类但是这次我们实现 Serializabl接口并用 transient 关键字修饰 password
public class Account implements Serializable {
    private String username;
    private transient String password;

    public Account(){}

    public Account(String username, String password) {
        this.username = username;
        this.password = password;
    }
    // setter and getter
}

public class Test {
    private static String outputFile = "./IOFiles/Account.obj";
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(
                                    new FileOutputStream(
                                            new File(outputFile)));
        Account account = new Account("USERNAME", "PASSWORD");
        // 将对象序列化
        out.writeObject(account);

        ObjectInputStream in = new ObjectInputStream(
                                    new FileInputStream(
                                            new File(outputFile)));
        // 恢复序列化的对象
        Account recoveredAccount = (Account)in.readObject();
        System.out.println("username: " + recoveredAccount.getUsername() +
                            ", password: " + recoveredAccount.getPassword());
    }
}

在这里插入图片描述

​ 可以看到效果与 Externalizable 接口一样 password 也安全地没有被序列化。不过有些时候我们希望被 transient 也能被序列化那该怎么办呢,这操作感觉有些多余或者难以理解,但我们的好朋友 ArrayList 在实现的时候就是这样的,见下面代码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	.....
    // ArrayList 中实际存储数据的 elementData 数组就是被 transient 修饰的
    transient Object[] elementData; 
    .....
        
}

那看到这个就会有疑问了难道我序列化 ArrayList 以后我存的数据就丢了吗,那我序列化的意义在哪呢?那数据当然不会丢这就要用到之前所说的序列化被 transient 关键字修饰的方法,具体的实现与 Externalizable 接口中的序列化方法很相似,那我们就继续用 ArrayList 中的代码吧:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

可以看到使用 write 和 read 的方式就可以将我们被 transient 修饰的关键字序列化出去了,为了到达这个目的我们在编写方法是必须严格的按照上述两个方法的方法签名即:

// 注意此处为 private
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{}
// 注意此处为 private
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {}

很奇怪的一点是这两个方法显然不属于 Serializable 接口因为它们是 private 的而接口中不能定义私有方法,而从形式上来说完成的功能又类似于实现接口,确实是很让人迷惑的设计,OK 不纠结这个问题了来解决一下既然要序列化那么为什么要用 transient 关键字修饰呢,从上面的代码就可以看到在 ArrayList 中存放数据的数组实际上会存在许多的空值,那么将那些空值都序列化是浪费资源的一种行为,而通过 transient 关键字加上这种序列化方式我们就可以只序列化那些有用的信息从而节省资源。因此当我们的属性中存在一些我们不需要的内容的时候就可以使用 transient 关键字修饰然后自己写序列化方法从而节省资源。

完。

参考

《Thinking in Java》 Bruce Eckel

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值