java 序列化

 

java中一切都是对象,在分布式环境中经常需要将Object从一端网络或设备传到另一端。

java序列化机制就是为了解决这个问题而产生的

 

java对象序列化如何实现?

  • 一个对象能够序列化的前提是实现Serializable接口,这个接口没有方法,更像是一个标记,告诉jvm这个类可以被序列化机制处理。

  • ObjectOutputStream和ObjectInputStream可以用于处理对象的序列化

 

java对象序列化算法步骤:

1.将对象实例相关的类元数据输出

2.递归地输出类的超类描述知道不再有超类

3.类元数据完了后,开始从最顶层的超类开始输出对象实例的实际数据值

4.从上至下递归输出实例的数据

 

serialVersionUID 序列化版本id

java序列化机制是通过类的serialVersionUID来验证版本一致性的。

在反序列化时,jvm会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同就认为一致,可以进行反序列化,否则出现InvalidCastException异常

 

serialVersionUID有两种显式生成方式:

1.默认的1L,比如:private static final long serialVersionUID = 1L;

2.根据类名、接口名、成员方法、属性等来生成一个64位的哈希字段 = xxxL;

当实现Serializable接口对象没有显式定义一个serialVersionUID时,java序列化会根据编译的Class自动生成一个serialVersionUID,这种情况下只要class文件发生变化,serialVersionUID就会改变,否则一直不变

 

列出几种情况直观看到serialVersionUID作用:

情况一:Test类序列化后,从A端传到B端,然后在B端反序列化(或者先序列化到本地文件,再反序列化),如果类都一样,serialVersionUID不一样

结果:报错

 

情况二:两处serialVersionUID一致,A端增加一个字段

结果:正常序列化、反序列化。A端多的字段B端忽略

 

情况三:两处serialVersionUID一致,B端增加一个字段

结果:序列化、反序列化正常,B端新增字段赋予默认值

 

java 代码示例

import java.io.Serializable;

public class Person implements Serializable{
    private static final long serialVersionUID = 2l;

    private String name;
//    private String age;
    private String id;


    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
//
//    public String getAge() {
//        return age;
//    }
//
//    public void setAge(String age) {
//        this.age = age;
//    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "serialVersionUID=" + serialVersionUID +
                ", name='" + name + '\'' +
//                ", age='" + age + '\'' +
                ", id='" + id + '\'' +
                '}';
    }
}
public static void main(String[] args) throws Exception{

        Person person=new Person();
        //person.setAge("12");
        person.setName("hello");


        FileOutputStream fos = new FileOutputStream("tempdata.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(person);
        oos.close();


    }
public static void main(String[] args) throws Exception{

        FileInputStream fis = new FileInputStream("tempdata.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Person newone = (Person) ois.readObject();
        ois.close();

        System.out.println(newone);


    }

 

java 序列化的坑

  • 1 :由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。所以更改之后序列化获取到的仍然是更改之前的对象。
public class Set {

    public static void main(String[] args) throws Exception{

        Person person=new Person();
        person.setAge("12");
        person.setName("hello");


        FileOutputStream fos = new FileOutputStream("tempdata.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(person);


        person.setAge("13");
        person.setName("sdad");
        oos.writeObject(person);
        ;
        oos.close();
    }
}
public class Get {

    public static void main(String[] args) throws Exception{

        FileInputStream fis = new FileInputStream("tempdata.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Person newone = (Person) ois.readObject();
        Person newone2 = (Person) ois.readObject();
        ois.close();

        System.out.println(newone);
        System.out.println(newone2);
    }
}

输出结果:

Person{serialVersionUID=2, name='hello', age='12'}
Person{serialVersionUID=2, name='hello', age='12'}

  • 2 :静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。
  • 3 : final变量是对象的一部分,将直接通过值参与序列化,将final变量声明为transient变量不会产生任何影响,依然会被序列化。

 

java自定义序列化

1:transient关键字

使用transient 关键字可以 使得某些字段不进行序列化和反序列化。

import java.io.Serializable;

public class Person implements Serializable{
    private static final long serialVersionUID = 2l;

    private transient String name;

    private String age;
    
    public Person() {
    }
    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "serialVersionUID=" + serialVersionUID +
                ", name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

 

2: 使用transient虽然简单,但将此属性完全隔离在了序列化之外。java提供了可选的控制序列化的方式 

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

通过重写类的 writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。这里展示对名字进行反转加密。

public class Person implements Serializable {
   private String name;
   private int age;
   //省略构造方法,get及set方法

   private void writeObject(ObjectOutputStream out) throws IOException {
       //将名字反转写入二进制流
       out.writeObject(new StringBuffer(this.name).reverse());
       out.writeInt(age);
   }

   private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
       //将读出的字符串反转恢复回来
       this.name = ((StringBuffer)ins.readObject()).reverse().toString();
       this.age = ins.readInt();
   }
}


write的时候也可以先修改 对象属性,然后使用默认写方法写入
read的时候可以先默认读,然后再修改属性 


private void writeObject(java.io.ObjectOutputStream stream)
        throws java.io.IOException
{
    age =age +10;
    stream.defaultWriteObject();
}

private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException
{
    stream.defaultReadObject();
    age =age -10;
}


当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

 

更彻底的自定义序列化

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

  • writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象

    public class Person implements Serializable {
      private String name;
      private int age;
      //省略构造方法,get及set方法
    
      private Object writeReplace() throws ObjectStreamException {
          ArrayList<Object> list = new ArrayList<>(2);
          list.add(this.name);
          list.add(this.age);
          return list;
      }
    
       public static void main(String[] args) throws Exception {
          try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
               ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
              Person person = new Person("9龙", 23);
              oos.writeObject(person);
              ArrayList list = (ArrayList)ios.readObject();
              System.out.println(list);
          }
      }
    }
    //输出结果
    //[9龙, 23]
    
  • readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。

    public class Person implements Serializable {
        private String name;
        private int age;
        //省略构造方法,get及set方法
         private Object readResolve() throws ObjectStreamException{
            return new ("brady", 23);
        }
        public static void main(String[] args) throws Exception {
            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                 ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
                Person person = new Person("9龙", 23);
                oos.writeObject(person);
                HashMap map = (HashMap)ios.readObject();
                System.out.println(map);
            }
        }
    }
    //输出结果
    //{brady=23}
    

    readResolve常用来反序列单例类,保证单例类的唯一性。

    注意:readResolve与writeReplace的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于final修饰的类重写readResolve方法没有问题;否则,重写readResolve使用private修饰。

 

 

4  Externalizable:强制自定义序列化

通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
     void writeExternal(ObjectOutput out) throws IOException;
     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
public class ExPerson implements Externalizable {

    private String name;
    private int age;
    //注意,必须加上pulic 无参构造器
    public ExPerson() {
    }

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //将name反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //将读取的字符串反转后赋值给name实例变量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {
            oos.writeObject(new ExPerson("brady", 23));
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        }
    }
}
//输出结果
//ydarb
//brady
//ExPerson{name='brady', age=23}

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

 

 

两种序列化对比

实现Serializable接口实现Externalizable接口
系统自动存储必要的信息程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持必须实现接口内的两个方法
性能略差性能略好

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。

 

对数据加密或者签名如何实现?

如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。

结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节

 

如何验证序列化流的完整性?

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

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

 

 

java 序列化的优缺点:

优点

  • 1  不用借助三方IDL文件
  • 2  反序列化很快
  • 3  可以兼容旧版本的对象,只要序列化UID一样就可以了。

 

缺点:

  • 1 字节流比较大 空间占用多
  • 2 只支持java语言

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值