Java 序列化

简介: 读完这篇文章你将会收获到• Serializable 和 Externalizable 的使用• 序列化 ID 问题• 静态变量序列化• 父类的序列化• ArrayList 序列化:为啥 size 被序列化两次?• 序列化对单例的破坏序列化就是将对象的状态信息转为可以存储或者传输的形式的过程比如说将对象序列化之后存储在硬盘上比如说将对象序列化之后返回给调用方反序列化则是序列化的反过程

读完这篇文章你将会收获到

  • SerializableExternalizable 的使用
  • 序列化 ID 问题
  • 静态变量序列化
  • 父类的序列化
  • ArrayList 序列化:为啥 size 被序列化两次?
  • 序列化对单例的破坏

序列化就是将对象的状态信息转为可以存储或者传输的形式的过程

比如说将对象序列化之后存储在硬盘上

比如说将对象序列化之后返回给调用方

反序列化则是序列化的反过程


Serializable

我们在 Java 中经常借助 SerializableObjectOutputStreamObjectInputStream 进行序列化和反序列化操作

public class Person implements Serializable {
    private String name;
    private String wish;
 .............
 .............
}
复制代码
private void serialize() throws Exception {
    Person person = new Person();
    person.setName("coderLi");
    person.setWish("被关注");
    FileOutputStream fileOutputStream = new FileOutputStream("coderLi.per");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(person);
    objectOutputStream.close();
    fileOutputStream.close();
}
private void deserialize() throws Exception {
    FileInputStream fileInputStream = new FileInputStream("coderLi.per");
    ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
    Person person = (Person) objectInputStream.readObject();
    System.out.println(person);
}
复制代码


Java 中一个对象想要序列化成功、必须满足两个条件

  • 该类必须实现 Serializable 接口
  • 该对象的所有属性都是可序列化的,如果不想参与序列化或者不能序列化、则可以使用 transient 修饰


Serializable 接口中其实并无任何的方法、只是单纯的一个空接口。它的作用仅仅是作为一个标记。我们可以在 java.io.ObjectOutputStream#writeObject0 中找到其判断

// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
  .......
  .......
   throw new NotSerializableException(cl.getName());
}
复制代码

可以看到如果你既不是 String , Array , Enum , 也不是 Serializable , 那么你就等着吃异常吧 !


Externalizable

Externalizable 继承 Serializable 接口并添加了两个方法,通过实现这两个类来序列化或反序列化对象

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
复制代码
public class Animal implements Externalizable {
   private String name;
   private int age;
  @Override
 public void writeExternal(ObjectOutput out) throws IOException {
    System.out.println(out.getClass().getSimpleName());
  out.writeInt(age);
  out.writeObject(name);
 }
 @Override
 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    System.out.println(in.getClass().getSimpleName());
  age = in.readInt();
  name = (String) in.readObject();
 }
}
复制代码


但是使用 Externalizable 要注意

  • 必须提供一个 public 的无参构造方法(因为反序列化的时候是先创建一个对象然后再调用 readExternal 方法)
  • 写入的顺序与读取的顺序要一致

调试发现、序列化的时候 ObjectOutput 的参数对象的类型是 ObjectOutputStream 、反序列化的时候 ObjectInput 的参数对象的类型是 ObjectInputStream


序列化 ID

在上面的例子中、我们都没有加上 serialVersionUID , 我们现在在 Animal 中加上并随意赋值

private static final long serialVersionUID = 1L;
复制代码


然后我将其序列化到 animal.ani 文件中、然后修改 serialVersionUID 的值变为 2L、然后看看会发生什么

java.io.InvalidClassException: com.demo.Animal; 
local class incompatible: stream classdesc serialVersionUID = 1, 
local class serialVersionUID = 2
复制代码


我们可以从异常信息中知道、序列化的时候是有保存其 serialVersionUID 的,如果反序列化的时候两个值不一致、则会反序列化失败

那假如我们不指定这个静态常量的值,它是根据什么生成的?

其实这个值的生成是根据这个类的信息去生成的,我尝试了一下、不指定 serialVersionUID 序列化之后、然后为这个类增加一个非 final 的属性,反序列化就报上面的异常了。当然,你也可以为这个类增加方法、同样也会导致 serialVersionUID 不同继而反序列化失败

所以没有什么特殊要求的时候、我们可以将 serialVersionUID 设置为 1 (当然也可以是其他值,只要指定值就行) , 那么就不会说因为服务端升级改东西了,客户端暂时没有升级而导致反序列化失败


静态变量序列化

默认情况下、静态变量不参与序列化。对象的序列化、当然只是序列化对象的属性啦

例子就不贴了


父类的序列化

要将父类的属性也序列化、那就让父类实现 Serializable 接口吧。如果父类不实现呢?那爸爸你给我一个无参的构造方法吧,那么这个时候我就不序列化你了,但是因为又这个无参的构造函数、那么我在反序列化的时候就可以调用这个构造函数、因为 Java 里面、是先有父对象才有子对象嘛,当然反序列完之后的父类属性、如果没有在在无参构造方法中赋值的话、那么就是其类型的默认的值了


ArrayList 序列化

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
      transient Object[] elementData; 
  private int size;
............
}
复制代码


我们在上面的已经分析了

// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
  .......
  .......
   throw new NotSerializableException(cl.getName());
} 
复制代码


当一个对象是 Serializable 的实例、那么就会进入到 writeOrdinaryObject 中,那么我们看看它这个方法做了什么

private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared)
    throws IOException
{
    try {
        desc.checkSerialize();
        bout.writeByte(TC_OBJECT);
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc);
        }
    } finally {
    }
}
复制代码

ArrayList 并没有实现 Externalizable 接口,所以直接进入到 writeSerialData 方法中

private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
              ...........
                // 这里这里是重点
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
            }
            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}
复制代码


我们 能看到这里有一个分支,如果你有一个叫做 writeObject 的方法,那么我就调用你这个方法进行序列化,如果你没有则调用 defaultWriteFields 方法。我们看看 slotDesc.invokeWriteObject 方法吧

void invokeWriteObject(Object obj, ObjectOutputStream out)
    throws IOException, UnsupportedOperationException
{
    requireInitialized();
    if (writeObjectMethod != null) {
        try {
            writeObjectMethod.invoke(obj, new Object[]{ out });
        } catch (InvocationTargetException ex) {
            ...... 
            throw (IOException) th;
        } catch (IllegalAccessException ex) {
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}
/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
复制代码


我们看到这两个属性的定义、一个是 writeObject、一个是 readObject ,这里便去 invoke

我们再回到 ArrayList 这个方法,巧了、在 ArrayList 中还真有这么两个方法

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    // 将当前类的非静态(non-static)和非瞬态(non-transient)字段写入流
    // 在这里也会将size字段写入。
    s.defaultWriteObject();
  // 序列化数组包含元素数量,为了向后兼容
   // 两次将size写入流
    s.writeInt(size);
    // 按照顺序写入,只写入到数组包含元素的结尾,并不会把数组的所有容量区域全部写入
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
/**
 * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
 * deserialize it).
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
  // 将流中的的非静态(non-static)和非瞬态(non-transient)字段读取到当前类
     // 包含 size
    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();
        }
    }
}
复制代码


其实为啥 ArrayList 需要自定义一个序列化和反序列化?其实大概看看其序列化的代

码、就能大概估摸出其意图,ArrayList 中的数组大小本身是比实际存储的元素个数要多的,我们序列化的时候没必要将没有用到的数组空间也序列化下来、显然是浪费性能的


其实代码中可以看到 size 被序列化了两次,而在反序列化的时候却直接丢弃第二次序

列化的 size ?  why


其实这么做是为了兼容问题、在旧版本的 JDK 中、ArrayList 的实现有所不同、会对 length 字段进行序列化


而在现在的版本中、不再序列化 length 了。为了能是新版本的序列化的对象能在旧版中能顺利的反序列化、所以就将 size 序列化两次了


序列化对单例的破坏

我们常见的单例模式 比如说 DCL

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
复制代码

我们将其序列化、然后反序列化、那么得到的是一个新的对象,而不是原来的对象、也就是说 JVM 中现在同时存在两个 Singleton 对象了


其实挺扯淡的、你想单例还实现 Serializable 接口? 哈哈哈、好吧,那我们把 Serializable 接口去掉,那是不是还可以通过反射创建一个新的实例,那行我们在构造方法中判断一下静态变量 singleton 是否为空、不为 null 就直接抛异常。貌似这样子还行,但是这个判断放在构造函数里面是否会太迟了。


我们回到上面的代码中、假设就是有这么扯淡的代码、要单例还实现了 Serializable 接口、那么我们可以保障其在 JVM 中的唯一性呢

我们查看 ObjectInputStreamreadObject 方法开始追踪

readObject->readObject0->readOrdinaryObject
复制代码


在这个方法里面看到了一个比较跟上面 invokeWriteObject 类似的方法: invokeReadResolve


点进去看了下、哦吼、我们可以在 Singleton 中实现一个 readResolve 的方法、它会在反序列化的时候被调用到、然后就最终返回给反序列化调用方

public Object readResolve(){
    return singleton;
}
复制代码


但是这里还是存在这么一个问题、在某个时刻,JVM 确实存在过两个这个单例类的对象、即使它没有被返回给反序列化的调用方,但却是真实存在


那么有没有一个好的单例模式可以用呢? 有、那就是枚举。因为枚举的反序列化最终调用的是 Enum.valueOf 的方法


其实这就是为啥推荐使用枚举作为单例的原因。对于反射调用构造方法、枚举也是做了限制直接抛异常的



目录
相关文章
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
1天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
10 3
|
4天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
17天前
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
23天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第3天】在Java编程的世界里,对象序列化与反序列化是实现数据持久化和网络传输的关键技术。本文将深入探讨Java序列化的原理、应用场景以及如何通过代码示例实现对象的序列化与反序列化过程。从基础概念到实践操作,我们将一步步揭示这一技术的魅力所在。
|
23天前
|
消息中间件 存储 Java
大数据-58 Kafka 高级特性 消息发送02-自定义序列化器、自定义分区器 Java代码实现
大数据-58 Kafka 高级特性 消息发送02-自定义序列化器、自定义分区器 Java代码实现
30 3
|
23天前
|
分布式计算 资源调度 Hadoop
Hadoop-10-HDFS集群 Java实现MapReduce WordCount计算 Hadoop序列化 编写Mapper和Reducer和Driver 附带POM 详细代码 图文等内容
Hadoop-10-HDFS集群 Java实现MapReduce WordCount计算 Hadoop序列化 编写Mapper和Reducer和Driver 附带POM 详细代码 图文等内容
76 3
|
28天前
|
Java 数据库 对象存储
Java 序列化详解
本文详细解析了Java序列化的概念与应用。通过具体实例,深入探讨了其在对象存储和传输中的作用及实现方法,帮助读者理解如何有效利用这一特性来简化数据交换,并对其实现机制有了更深入的认识。
|
4天前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
6 0
|
2月前
|
JSON NoSQL Java
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)
这篇文章介绍了在Java中使用Redis客户端的几种方法,包括Jedis、SpringDataRedis和SpringBoot整合Redis的操作。文章详细解释了Jedis的基本使用步骤,Jedis连接池的创建和使用,以及在SpringBoot项目中如何配置和使用RedisTemplate和StringRedisTemplate。此外,还探讨了RedisTemplate序列化的两种实践方案,包括默认的JDK序列化和自定义的JSON序列化,以及StringRedisTemplate的使用,它要求键和值都必须是String类型。
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)