Java 序列化机制详解

简介: Java 序列化机制是一种将对象转换为字节流的过程,以便在网络上传输或保存到文件中,并能在需要时将字节流还原为对象。这一机制通过实现 `java.io.Serializable` 接口来实现,同时涉及到一些关键概念和注意事项。

Java 序列化机制是一种将对象转换为字节流的过程,以便在网络上传输或保存到文件中,并能在需要时将字节流还原为对象。这一机制通过实现 java.io.Serializable 接口来实现,同时涉及到一些关键概念和注意事项。

java-serial.jpg

Serializable 接口

Serializable 接口是 Java 提供的标记接口,没有包含任何需要实现的方法。实现了这个接口的类表明其对象是可序列化的,可以被转换为字节流。

public interface Serializable {
   
}

通过实现 Serializable 接口,标识类的对象可以被序列化。这使得对象可以在网络上传输或保存到文件中,而不失去其状态和结构。

序列化过程

序列化是将对象的状态(字段值)转换为字节流的过程。这个过程由 ObjectOutputStream 类来完成。序列化使得对象可以以字节流的形式进行存储或传输,便于在不同系统之间进行数据交换。如下我们列举几个重要的方法的源码:

  • writeObject方法


public final void writeObject(Object obj) throws IOException {
   
    if (enableOverride) {
   
        writeObjectOverride(obj);
        return;
    }
    try {
   
        writeObject0(obj, false);
    } catch (IOException ex) {
   
        if (depth == 0) {
   
            writeFatalException(ex);
        }
        throw ex;
    }
}

enableOverride 表示是否启用了对象写入的覆盖机制。如果启用,会调用 writeObjectOverride 方法来执行对象的特定写入逻辑。

如果没有启用覆盖机制,则调用 writeObject0 方法执行实际的对象序列化。

writeObject0 方法负责处理对象的序列化,其中第二个参数 false 表示不使用不共享的方式进行序列化。

如果在序列化过程中抛出 IOException 异常,会捕获该异常。如果当前深度为0(表示不在嵌套序列化过程中),则调用 writeFatalException 方法来处理异常,否则将异常重新抛出。

  • writeObject0


private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
   
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
   
        // handle previously written and non-replaceable objects
        int h;
        if ((obj = subs.lookup(obj)) == null) {
   
            writeNull();
            return;
        } else if (!unshared && (h = handles.lookup(obj)) != -1) {
   
            writeHandle(h);
            return;
        } else if (obj instanceof Class) {
   
            writeClass((Class) obj, unshared);
            return;
        } else if (obj instanceof ObjectStreamClass) {
   
            writeClassDesc((ObjectStreamClass) obj, unshared);
            return;
        }

        // check for replacement object
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
        for (;;) {
   
            // REMIND: skip this check for strings/arrays?
            Class<?> repCl;
            desc = ObjectStreamClass.lookup(cl, true);
            if (!desc.hasWriteReplaceMethod() ||
                (obj = desc.invokeWriteReplace(obj)) == null ||
                (repCl = obj.getClass()) == cl)
            {
   
                break;
            }
            cl = repCl;
        }
        if (enableReplace) {
   
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
   
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            obj = rep;
        }

        // if object replaced, run through original checks a second time
        if (obj != orig) {
   
            subs.assign(orig, obj);
            if (obj == null) {
   
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
   
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
   
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
   
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }
        }

        // 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 {
   
            if (extendedDebugInfo) {
   
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
   
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
   
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

反序列化过程

当需要从字节流中恢复对象时,Java 序列化机制会将字节流还原为对象的状态。这个过程由 ObjectInputStream 类来完成。如下我们列举几个重要的方法的源码:

  • readObject()


private final Object readObject(Class<?> type)
    throws IOException, ClassNotFoundException
{
   
    if (enableOverride) {
   
        return readObjectOverride();
    }

    if (! (type == Object.class || type == String.class))
        throw new AssertionError("internal error");

    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
   
        Object obj = readObject0(type, false);
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
   
            throw ex;
        }
        if (depth == 0) {
   
            vlist.doCallbacks();
        }
        return obj;
    } finally {
   
        passHandle = outerHandle;
        if (closed && depth == 0) {
   
            clear();
        }
    }
}

这段代码的主要作用是根据给定的类型 (type) 进行对象的反序列化。在这个过程中,它使用了一些状态变量,如 enableOverride、passHandle、handles、depth、vlist 等,来管理反序列化的过程。在处理嵌套对象时,它通过 markDependency 方法标记了当前对象与封闭对象的依赖关系。在深度为 0 时,执行了clear方法。具体反序列化执行的核心方法是readObject0()

  • readObject0()


private Object readObject0(Class<?> type, boolean unshared) throws IOException {
   
    boolean oldMode = bin.getBlockDataMode();
    if (oldMode) {
   
        int remain = bin.currentBlockRemaining();
        if (remain > 0) {
   
            throw new OptionalDataException(remain);
        } else if (defaultDataEnd) {
   
            /*
             * Fix for 4360508: stream is currently at the end of a field
             * value block written via default serialization; since there
             * is no terminating TC_ENDBLOCKDATA tag, simulate
             * end-of-custom-data behavior explicitly.
             */
            throw new OptionalDataException(true);
        }
        bin.setBlockDataMode(false);
    }

    byte tc;
    while ((tc = bin.peekByte()) == TC_RESET) {
   
        bin.readByte();
        handleReset();
    }

    depth++;
    totalObjectRefs++;
    try {
   
        switch (tc) {
   
            case TC_NULL:
                return readNull();

            case TC_REFERENCE:
                // check the type of the existing object
                return type.cast(readHandle(unshared));

            case TC_CLASS:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast a class to java.lang.String");
                }
                return readClass(unshared);

            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast a class to java.lang.String");
                }
                return readClassDesc(unshared);

            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));

            case TC_ARRAY:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast an array to java.lang.String");
                }
                return checkResolve(readArray(unshared));

            case TC_ENUM:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast an enum to java.lang.String");
                }
                return checkResolve(readEnum(unshared));

            case TC_OBJECT:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast an object to java.lang.String");
                }
                return checkResolve(readOrdinaryObject(unshared));

            case TC_EXCEPTION:
                if (type == String.class) {
   
                    throw new ClassCastException("Cannot cast an exception to java.lang.String");
                }
                IOException ex = readFatalException();
                throw new WriteAbortedException("writing aborted", ex);

            case TC_BLOCKDATA:
            case TC_BLOCKDATALONG:
                if (oldMode) {
   
                    bin.setBlockDataMode(true);
                    bin.peek();             // force header read
                    throw new OptionalDataException(
                        bin.currentBlockRemaining());
                } else {
   
                    throw new StreamCorruptedException(
                        "unexpected block data");
                }

            case TC_ENDBLOCKDATA:
                if (oldMode) {
   
                    throw new OptionalDataException(true);
                } else {
   
                    throw new StreamCorruptedException(
                        "unexpected end of block data");
                }

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
   
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

serialVersionUID

serialVersionUID 是用于版本控制的序列化版本号。它是一个长整型数值,用于标识类的版本。通过显式声明 serialVersionUID,可以在类结构发生变化时依然能够正确地进行反序列化。

@Data
public class LoginUserInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    ...
}

如果在类中没有明确声明 serialVersionUID,Java 运行时系统会根据类的结构自动生成一个。这种自动生成的 serialVersionUID 是基于类的各个方面的,包括字段、方法、父类等。如果类的结构发生变化,可能导致自动生成的 serialVersionUID 发生变化。这可能会导致在反序列化时,类的版本不一致,从而导致 InvalidClassException 异常。

所以显式声明 serialVersionUID是确保反序列化过程正确的关键,避免因类结构变化而导致的问题。

transient 关键字

关键字 transient 用于标记字段,表示在对象序列化的过程中,这个字段应该被忽略。例如,如果一个类有一个不希望被序列化的缓存字段,可以使用 transient 关键字来避免将其写入序列化数据。例如ArrayList、LinkedList 等类中的一些属性就是使用transient修饰的:

_20231218215928.jpg

_20231218215951.jpg

自定义序列化和反序列化

有时候,可能需要自定义序列化和反序列化的过程以满足特定需求。可以通过实现 writeObject 和 readObject 方法来实现自定义逻辑。如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();
    }
}

/**
 * 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;

    // 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();
        }
    }
}

序列化不保存静态变量

  • 对象状态 vs. 类状态

序列化的主要目的是保存对象的状态,即对象的实例变量。静态变量是类级别的,它们对于每个对象实例都是相同的。序列化关注的是对象的实例状态,因为这是对象在不同环境中重建时所需的关键信息。

  • 节省空间

静态变量通常用于表示类级别的常量或共享数据,这些数据在所有对象实例之间是相同的。如果每个对象的静态变量都被序列化并存储,将导致冗余,浪费存储空间。序列化的目标之一是尽可能紧凑地保存对象的状态,因此不保存静态变量是一种优化。

  • 不需要还原

静态变量在类加载时初始化,并在整个应用程序的生命周期内保持不变。因此,在反序列化时不需要重新初始化静态变量。序列化和反序列化的目标是保存和还原对象的动态状态,而不是类级别的静态状态。

序列化的安全性和性能考虑

在实际应用中,需要注意序列化的安全性和性能。反序列化过程中可能存在安全风险,因此要谨慎处理来自不受信任源的序列化数据。此外,对于大量数据的序列化,可能会影响系统性能,可以考虑使用更高效的序列化工具或压缩算法。

总结

综合来看,Java 序列化的核心思想是将对象的状态转换为字节流,并通过 ObjectOutputStream 类完成这一过程。该类在内部处理了对象引用的记录、对象字段的写入、自定义写入方法的执行等。在实际应用中,我们需要注意序列化版本控制、对象字段的 transient 关键字的处理以及序列化性能等方面的问题。

请注意,Java 序列化机制在现代应用中可能会遇到一些挑战,包括性能问题、安全性问题以及与其他语言的兼容性等。因此,在一些场景下,开发者可能会考虑使用其他序列化框架,如 JSON 或 Protocol Buffers,以满足不同的需求。

目录
相关文章
|
11月前
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
526 32
AQS:Java 中悲观锁的底层实现机制
|
11月前
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
595 0
|
6月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
341 1
|
6月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
339 1
|
7月前
|
存储 NoSQL Java
配置RedisTemplate序列化机制
通过上述步骤,你可以灵活配置RedisTemplate的序列化机制,根据应用需求选择合适的序列化器,从而确保数据在Redis中的存储和读取效率最优化。配置合适的序列化机制对于性能和存储效率至关重要,而且这样可以确保数据在存储和传输过程中的结构清晰和一致性。
540 11
|
9月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
358 4
|
9月前
|
存储 安全 Java
深入理解Java序列化接口及其实现机制
记住,序列化不仅仅是把对象状态保存下来那么简单,它涉及到类的版本控制、安全性和性能等多个重要方面。正确理解和实现Java序列化机制对于构建高效、安全和可维护的Java应用至关重要。
293 0
|
10月前
|
JSON Java 数据库连接
|
11月前
|
Java 区块链 网络架构
酷阿鲸森林农场:Java 区块链系统中的 P2P 区块同步与节点自动加入机制
本文介绍了基于 Java 的去中心化区块链电商系统设计与实现,重点探讨了 P2P 网络在酷阿鲸森林农场项目中的应用。通过节点自动发现、区块广播同步及链校验功能,系统实现了无需中心服务器的点对点网络架构。文章详细解析了核心代码逻辑,包括 P2P 服务端监听、客户端广播新区块及节点列表自动获取等环节,并提出了消息签名验证、WebSocket 替代 Socket 等优化方向。该系统不仅适用于农业电商,还可扩展至教育、物流等领域,构建可信数据链条。
|
缓存 Dubbo Java
理解的Java中SPI机制
本文深入解析了JDK提供的Java SPI(Service Provider Interface)机制,这是一种基于接口编程、策略模式与配置文件组合实现的动态加载机制,核心在于解耦。文章通过具体示例介绍了SPI的使用方法,包括定义接口、创建配置文件及加载实现类的过程,并分析了其原理与优缺点。SPI适用于框架扩展或替换场景,如JDBC驱动加载、SLF4J日志实现等,但存在加载效率低和线程安全问题。
665 7
理解的Java中SPI机制