java-序列化jdk实现

java.io.Serializable

类的序列化性由实现该类的类启用。 java.io.Serializable。不实现此的类接口将不具有它们的任何状态序列化或
反序列化。可序列化类的所有子类型都是自己的可序列化。序列化接口没有方法或字段。
*只用于标识可序列化的语义。

允许序列化非序列化类的子类型,
*子类型可以承担保存和恢复的责任。超级类型的公共、受保护和(如果可访问的)状态包字段。子类型只能承担此责任。
只有当它扩展的类有一个可访问的无参构造函数去初始化类的状态。如果不是这种情况,声明类是错误的,运行时将检测到错误。

在反序列化过程中,不可序列化类的变量将使用该类的公共或受保护的无参数构造函数来初始化。无参数构造函数必须可以被可序列化的子类访问。可序列化子类的字段将从流中恢复

进行遍列时,可能会遇到不支持可序列化接口的类。在这种情况下,将抛出NotSerializableException用以标识该类的不可序列化的对象

如需要对类的序列化与反序列化进行特殊处理,则需要实现以下几个特殊函数。

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

writeObject方法负责为其特定类编写对象的状态,以便相应的readObject方法可以恢复它。保存对象的字段的默认机制可以通过调用out.defaultWriteObject来调用。该方法不需要关心属于其超类或子类的状态。通过使用writeObject方法或通过使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存状态。

readObject方法负责从流中读取和恢复类字段。 它可以调用in.defaultReadObject来调用恢复对象的非静态和非瞬态字段的默认机制。 defaultReadObject方法使用流中的信息来将流中保存的对象的字段分配给当前对象中相应命名的字段。这处理当类已经进化以添加新字段时的情况。该方法不需要关心属于其超类或子类的状态。 通过使用writeObject方法或通过使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存状态

readObjectNoData方法负责在序列化流不将给定类列为被反序列化的对象的超类的情况下,为其特定类初始化对象的状态。这可能发生在接收方使用与发送方不同的反序列化实例的类的版本,并且接收方的版本扩展了未被发送方的版本扩展的类的情况下。如果串行化流已经被篡改,这也可能发生;因此,readObjectNoData对于正确初始化反序列化对象非常有用,尽管有“敌意”或不完整的源流。

在将对象写入流时,需要指定一个替代对象的可序列化类应该使用具有特定签名的特殊方法:

 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

如果该方法存在,并且它可以从正在序列化的对象的类中定义的方法访问,则通过序列化调用此writeReplace方法。因此,该方法可以具有私有,受保护和包私有访问。子类对此方法的访问遵循Java可见性规则。

此readResolve方法遵循与writeReplace相同的调用规则和可见性规则。

serialVersionUID

序列化运行时与每个可序列化类相关联的版本号,称为serialVersionUID,其在反序列化期间用于验证序列化对象的发送方和接收方已经加载了针对该序列化兼容的对象的类。 如果接收者加载了一个对象的类,该类的serialVersionUID不同于对应的发送者类的serialVersionUID,那么反序列化将导致InvalidClassException。 可序列化类可以通过声明一个名为“serialVersionUID”的字段来显式声明它自己的serialVersionUID,该字段必须是static,final,类型为long:

serialVersionUID 用来表明类的不同版本间的兼容性

 ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果可序列化类没有显式地声明serialVersionUID,则串行化运行时将基于类的各个方面计算该类的默认serialVersionUID值,如Java(TM)对象序列化规范中所述。但是,强烈建议所有可序列化类显式声明serialVersionUID值,因为缺省的serialVersionUID计算对类详细信息非常敏感,可能会因编译器实现而异,因此可能会导致反序列化期间出现意外的InvalidClassExceptions。因此,为了确保不同Java编译器实现中的serialVersionUID值一致,可序列化类必须声明一个显式的serialVersionUID值。还强烈建议,显式serialVersionUID声明在可能的情况下使用private修饰符,因为这样的声明仅适用于立即声明的类 - serialVersionUID字段不作为继承成员使用。数组类不能声明显式serialVersionUID,因此它们总是具有默认计算值,但对于数组类,可以不需要匹配serialVersionUID值。

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来 的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序 列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变 量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的 class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化

实验一

public class Person implements Serializable {

    private static final long serialVersionUID = -5234479639633075574L;

    String name;

    int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

测试类

import java.io.*;

public class SerTest {

    public static void main(String[] args) {
        Person person1 = new Person();

        person1.setName("zhangsan");
        person1.setAge(21);

        final String FILE_PATH = "data/Person.sera";

        //序列化
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
            oos.writeObject(person1);
            oos.flush();
            System.out.println("序列化成功!");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //反序列化
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
            Person person2 = (Person) ois.readObject();
            System.out.println("反序列化成功");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

序列化结果

�� sr &com.aiso.jdksrc.io.serializable.Person�[d�O�� I ageL namet Ljava/lang/String;xp t zhangsan

改serialVersionUID

public class Person implements Serializable {

    private static final long serialVersionUID = -2L;

    String name;

    int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

再反序列化

java.io.InvalidClassException: com.aiso.jdksrc.io.serializable.Person; local class incompatible: stream classdesc serialVersionUID = -5234479639633075574, local class serialVersionUID = -2
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    at com.aiso.jdksrc.io.serializable.SerTest.main(SerTest.java:41)

给Person类添加字段

序列化允许重构

序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。

Java Object Serialization 规范可以自动管理的关键任务是:

将新字段添加到类中
将字段从 static 改为非 static
将字段从 transient 改为非 transient
取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。

将新字段sex添加到person类中

public class Person implements Serializable {

    private static final long serialVersionUID = -5234479639633075574L;

    String name;

    int age;

    int sex;

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

这里写图片描述

一旦有了 Person 的 serialVersionUID,不仅可以从原始对象 Person 的序列化数据创建 PersonV2 对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2 的数据通过反序列化得到 Person,这毫不奇怪。

安全性

让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。

这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。

幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。

模糊化序列化数据

假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。

public class Person implements Serializable {

    private static final long serialVersionUID = -5234479639633075574L;

    String name;

    int age;

    int sex;

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException
    {
        // "Encrypt"/obscure the sensitive data
        age = age << 2;
        stream.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException
    {
        stream.defaultReadObject();

        // "Decrypt"/de-obscure the sensitive data
        age = age >> 2;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}

测试类

import java.io.*;

public class SerTest {

    public static void main(String[] args) {
        Person person1 = new Person();

        person1.setName("zhangsan");
        person1.setAge(21);

        final String FILE_PATH = "data/Person.sera";

        System.out.println(person1);

        //序列化
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
            oos.writeObject(person1);
            oos.flush();
            System.out.println("序列化成功!");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        //反序列化
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
            Person person2 = (Person) ois.readObject();
            System.out.println("反序列化成功!");
            System.out.println(person2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

结果

Person{name=’zhangsan’, age=21, sex=0}
序列化成功!
反序列化成功
Person{name=’zhangsan’, age=21, sex=0}

签名和密封

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。

如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。

序列化允许将代理放在流中

打包和解包代理

writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 sex。

这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。

信任,但要验证认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。

对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现 ObjectInputValidation接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException

class PersonProxy
        implements java.io.Serializable {

    public String data;


    public PersonProxy(Person orig) {
        data = orig.getName() + "," + orig.getAge();
        if (orig.getSex() != null) {
            Integer sex = orig.getSex();
            data = data + "," + sex;

        }
    }


    private Object readResolve()
            throws java.io.ObjectStreamException {
        String[] pieces = data.split(",");
        Person result = new Person(pieces[0], Integer.parseInt(pieces[1]));
        if (pieces.length > 2) {
//            result.setSex(Integer.parseInt(pieces[2]));
            result.setSex(2);
        }
        return result;
    }
}
public class Person implements Serializable {

    private static final long serialVersionUID = -5234479639633075574L;

    String name;

    int age;

    Integer sex;

    public Person(){

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Integer getSex() {
        return sex;
    }

    public void setSex(Integer sex) {
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {
        return new PersonProxy(this);
    }



    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}
public class SerTest {

    public static void main(String[] args) {
        Person person1 = new Person();

        person1.setName("zhangsan");
        person1.setAge(21);
        person1.setSex(1);

        final String FILE_PATH = "data/Person.sera";

        System.out.println(person1);

        //序列化
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
            oos.writeObject(person1);
            oos.flush();
            System.out.println("序列化成功!");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        //反序列化
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
            Person person2 = (Person) ois.readObject();
            System.out.println("反序列化成功!");
            System.out.println(person2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

结果

Person{name='zhangsan', age=21, sex=1}
序列化成功!
反序列化成功!
Person{name='zhangsan', age=21, sex=2}

transient

java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程

一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

EnumMap

与Enum类型键一起使用的专用{@链接Map}实现。所有枚举映射中的键必须来自单个枚举类型,即
*在创建映射时指定、显式或隐式指定。枚举映射内部表示为数组。这种表示极其紧凑高效。
*< p>枚举映射保持在它们的键的自然顺序中。
*(枚举常量被声明的顺序)。这反映出来了。

*在集合视图返回的迭代器中
({@link #keySet()},
* {@link #entrySet()}, and {@link #values()}).

*>集合视图返回的迭代器是弱一致的
*它们将不会抛出 {@link ConcurrentModificationException} 它们可能会
*或可能不显示对发生的map的任何修改的影响。迭代正在进行中。

不允许空键。尝试插入空键将抛出 {@link NullPointerException}. 。尝试测试存在一个空键或删除一个,但是,功能正常。允许空值。

*

与大多数集合实现TT>枚举图不一样
非同步。如果多个线程同时访问枚举映射,则至少有一个线程修改了映射,应该同步在外部。这通常是通过在一些自然封装Enum 映射的对象同步上实现的。如果不存在这样的对象,*该映射应该使用 {@link Collections#synchronizedMap}方法。最好在创建时完成,以防止非同步访问意外发生。

 Map&lt;EnumKey, V&gt; m
         = Collections.synchronizedMap(new EnumMap&lt;EnumKey, V&gt;(...));

P>执行x说明:所有基本操作都是在固定时间内执行的。他们很可能(虽然没有保证)比{@link HashMap}快。
EnumMap的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键(枚举值),一个表示对应的值,不允许keynull,但允许value为null,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作都是数组,因此效率很高。

EnumSet

与*Enum类型一起使用的专用{@链接集}实现。所有的
*枚举集合中的元素必须来自单个枚举类型,即在创建集合时显式或隐式指定。枚举集
内部表示为位向量。这种表示是非常紧凑和高效。
空间和时间性能类应该足够好,允许使用它作为一个高质量、类型的工具。
替代传统的 -基于int“位标志”。
桶操作(如containsAllretainAll< /TT>)
*如果它们的参数也是枚举集合,则运行得非常快。

由TT>迭代器方法返回的迭代器遍历它们的自然顺序中的元素(枚举的顺序)声明常量。返回迭代器为弱。一致性:它将不会抛出ConcurrentModificationException
在迭代正在进行时发生,它可能或可能不显示对该集合进行任何修改的效果。

空元素是不允许的。尝试插入空元素将抛出{@链接nulLoPoExtExt}。尝试测试
存在一个空元素或删除一个,但是,函数正确。
*

与大多数集合实现一样,枚举集不是
*同步。如果多个线程同时访问枚举集,则
*至少有一个线程修改了集合,它应该是同步的。外部实现。这通常是通过在一些同步上实现的。
*自然封装Enum集合的对象。如果不存在这样的对象,
*该集合应该使用 {@link Collections#synchronizedSet}包装”方法。最好在创建时完成,以防止
非同步访问意外发生。

 Set&lt;MyEnum&gt; s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));

所有基本操作都是在固定时间内执行的。他们很可能(虽然没有保证)比他们快得多。
{@link HashSet} 对应。甚至批量操作在*如果它们的参数也是枚举集合,则为一定时间

EnumSet内部使用的位向量实现的,前面我们说过EnumSet是一个抽象类,事实上它存在两个子类,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量作为位向量,long类型的位长度是64,因此可以存储64个枚举实例的标志位,一般情况下是够用的了,而JumboEnumSet使用一个long类型的数组,当枚举个数超过64时,就会采用long数组的方式存储。先看看EnumSet内部的数据结构:

EnumSet中有两个变量,一个elementType用于表示枚举的类型信息,universe是数组类型,存储该类型信息所表示的所有可能的枚举实例,EnumSet是抽象类,因此具体的实现是由子类完成的,下面看看noneOf(Class elementType)静态构建方法

从源码可以看出如果枚举值个数小于等于64,则静态工厂方法中创建的就是RegularEnumSet,否则大于64的话就创建JumboEnumSet。无论是RegularEnumSet还是JumboEnumSet,其构造函数内部都间接调用了EnumSet的构造函数,因此最终的elementType和universe都传递给了父类EnumSet的内部变量

在RegularEnumSet类和JumboEnumSet类中都存在一个elements变量,用于记录位向量的操作,

在RegularEnumSet中elements是一个long类型的变量,共有64个bit位,因此可以记录64个枚举常量,当枚举常量的数量超过64个时,将使用JumboEnumSet,elements在该类中是一个long型的数组,每个数组元素都可以存储64个枚举常量,这个过程其实与前面位向量的分析是同样的道理,只不过前面使用的是32位的int类型,这里使用的是64位的long类型罢了。接着我们看看EnumSet是如何添加数据的,RegularEnumSet中的add实现如下

其内部使用位向量,存储结构很简洁,节省空间,大部分操作都是按位运算,直接操作二进制数据,因此效率极高

猜你喜欢

转载自blog.csdn.net/qq_16038125/article/details/80727975