为什么要实现Serializable,明明就是个空接口嘛?

简介: 对于 Java 的序列化,以前我一直停留在最浅显的认知上,因为每次新建一个类自然而然就把序列化接口 Serializbale 给实现了。我不愿意做更深入的研究,因为会用就行了嘛。

对于 Java 的序列化,以前我一直停留在最浅显的认知上,因为每次新建一个类自然而然就把序列化接口 Serializbale 给实现了。我不愿意做更深入的研究,因为会用就行了嘛。

但随着时间的推移,见到 Serializbale 的次数越来越多,我便对它产生了浓厚的兴趣。是时候花点时间研究研究了。

几个待思考的问题

  1. 为什么序列化一个对象时,仅需要实现 Serializable 接口就可以了。
  2. 通常我们序列化一个类时,为什么推荐的做法是要实现一个静态 final 成员变量 serialVersionUID。
  3. 序列化机制是怎么忽略 transient 关键字的, static变量也不会被序列化。

接下来我们就带着问题,在源码中找寻答案吧。

什么是序列化?

序列化:Java中的序列化机制能够将一个实例对象信息写入到一个字节流中(只序列化对象的属性值,而不会去序列化方法),序列化后的对象可用于网络传输,或者持久化到数据库、磁盘中。

反序列化:需要对象的时候,再通过字节流中的信息来重构一个相同的对象。

Java 中要使一个类可以序列化,实现 `java.io.Serializable 接口是最简单的。

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
}
复制代码

那么我们来看看 Serializable 接口的源码实现,可以看到 Serializable 接口中并没有方法或字段,这个接口仅仅用于标识可序列化的语义,也就是说它只是用来标识一个对象是否可被序列化。

package java.io;
/**
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable {}
复制代码

为什么要序列化?

  • 把数据进行本地存储
  • 我们知道数据只能以二进制的形式在网络中传输,JavaBean对象在网络中传输时所以也需要用到序列化技术
  • IPC通信(因为进程内存的隔离,所以需要通过序列化传输数据)

接下来写一个例子测试一下:

创建一个 User 对象

@Datapublic class User {
    private String name;
    private String age;
}
复制代码

再来创建一个测试类,通过 ObjectOutputStream 将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过 ObjectInputStream 将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。

public class Test {
    public static void main(String[] args) {
        // 初始化
        User user = new User();
        user.setName("王二");
        user.setAge(18);
        System.out.println(user);
        // 把对象写到文件中
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 从文件中读出对象
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){
            User user1 = (User) ois.readObject();
            System.out.println(user1);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
复制代码

不过,由于 User 没有实现 Serializbale 接口,所以在运行测试类的时候会抛出异常,堆栈信息如下:

java.io.NotSerializableException: com.cmower.java_demo.xuliehua.Wanger
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.cmower.java_demo.xuliehua.Test.main(Test.java:21)
复制代码

接下来将 User 实现 Serializable 接口

@Data
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String age;
}
复制代码
@Slf4j
public class serializeTest {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("fufu");
        user.setAge("18");
        serialize(user);
        log.info("Java序列化前的结果:{} ", user);
        User duser = deserialize();
        log.info("Java反序列化的结果:{} ", duser);
    }
    /**
     * @author xzf
     * @description 序列化
     * @date 2020/2/22 19:34
     */
    private static void serialize(User user) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\111.txt")));
        oos.writeObject(user);
        oos.close();
    }
    /**
     * @author xzf
     * @description 反序列化
     * @date 2020/2/22 19:34
     */
    private static User deserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\111.txt")));
        return (User) ois.readObject();
    }
}
复制代码

因为 User 实现了 Serializable ,此时就可以序列化和反序列化了,输出结果如下:

序列化前的结果: User(name=fufu, age=18)
反序列化后的结果: User(name=fufu, age=18)
复制代码

打开 writeObject 方法的源码看一下,发现方法中有这么一个逻辑,当要写入的对象是 StringArrayEnumSerializable 类型的对象则可以正常序列化,否则会抛出 NotSerializableException 异常。

这就能解释为什么Java序列化一定要实现 Serializable 接口了。

/**
 * Underlying writeObject/writeUnshared implementation.
 */
private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // 省略号。。。。。。。。。。
        // 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);
    }
}
复制代码

那么可能会有人疑问,String 为啥就不用实现 Serializable 接口呢?其实 String 已经内部实现了 Serializable,不用我们再显示实现。看看源码就懂了

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    ......
}
复制代码

也就是说,ObjectOutputStream 在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable,如果全都不是的话,抛出 NotSerializableException

既然已经实现了 Serializable接口,为什么还要显示指定serialVersionUID的值呢?

因为序列化对象时,如果不设置 serialVersionUID,Java在序列化时会根据对象属性自动的生成一个 serialVersionUID,再进行存储或用作网络传输。

在反序列化时,会根据对象属性自动再生成一个新的 serialVersionUID,和序列化时生成的 serialVersionUID 进行比对,两个 serialVersionUID 相同则反序列化成功,否则就会抛异常。

而当手动设置 serialVersionUID 后,Java在序列化和反序列化对象时,生成的serialVersionUID 和我们设定的 serialVersionUID 相同,这样就保证了反序列化的成功。

transient

序列化对象时如果希望哪个属性不被序列化,则用 transient 关键字修饰即可

@Datapublic class User implements Serializable {
    private transient String name;
    private String age;
}
复制代码

可以看到字段 name 的值没有被保存到磁盘中,一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

Java序列化前的结果: User(name=fufu, age=18)
Java反序列化的结果:User(name=null, age=18)
复制代码

一个静态变量不管是否被 transient 修饰,均不能被序列化。因为 static 修饰的属性是属于类,而非对象。

为什么 transient 不被序列化呢?transient 的中文字义为“临时的”(论英语的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null

如果想要深究源码的话,你可以在 ObjectStreamClass 中发现下面这样的代码:

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    int mask = Modifier.STATIC | Modifier.TRANSIENT;
    int size = list.size();
    return (size == 0) ? NO_FIELDS :
        list.toArray(new ObjectStreamField[size]);
}
复制代码

复制代码看到 Modifier.STATIC | Modifier.TRANSIENT,是不是感觉更好了呢?

序列化过程

以 ObjectOutputStream 为例:

进入writeObject()方法。

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {//初始化时设置了false,不会走这里
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);//直接调用了这个方法
    } catch (IOException ex) {
        if (depth == 0) {
            // BEGIN Android-changed: Ignore secondary exceptions during writeObject().
            // writeFatalException(ex);
            try {
                writeFatalException(ex);
            } catch (IOException ex2) {
                // If writing the exception to the output stream causes another exception there
                // is no need to propagate the second exception or generate a third exception,
                // both of which might obscure details of the root cause.
            }
            // END Android-changed: Ignore secondary exceptions during writeObject().
        }
        throw ex;
    }
}
复制代码

进入writeObject0()方法。

/**
 * Underlying writeObject/writeUnshared implementation.
 */
private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        //... 代码省略
        // check for replacement object
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;//重点关注这个类
        //...代码省略
        Class repCl;
        desc = ObjectStreamClass.lookup(cl, true);//对desc对象进行了初始化
        if (desc.hasWriteReplaceMethod() &&
            (obj = desc.invokeWriteReplace(obj)) != null &&
            (repCl = obj.getClass()) != cl)
        {
            cl = repCl;
            desc = ObjectStreamClass.lookup(cl, true);
        }
        // END Android-changed: Make only one call to writeReplace.
        if (enableReplace) {
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            obj = rep;
        }
        //...代码省略
        //可以看出下面这些类型都是可以进行写入操作的
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        } else 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) {
        //实现Serializable会执行下面这个方法
        //第一个问题序列化为什么要实现Serializable?这就有了答案,Serializable只是一个标记接口,本身没有任何意义。
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else { //所以没有实现Serializable接口是会报错的
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}
复制代码

在writeObject0()方法中做了两件重要的事

  1. 通过ObjectStreamClass.lookup(cl, true)方法,初始化了ObjectStreamClass对象
  2. 根据不同写入类型,进行不同的操作

首先,我们先进入ObjectStreamClass.lookup(cl, true)方法。

static ObjectStreamClass  lookup(Class<?> cl, boolean all) {
    if (!(all || Serializable.class.isAssignableFrom(cl))) {
        return null;
    }
    processQueue(Caches.localDescsQueue, Caches.localDescs);
    WeakClassKey key = new WeakClassKey(cl, Caches.localDescsQueue);
    //下面先是从缓存中获取对象,首次进入都是没有
    Reference<?> ref = Caches.localDescs.get(key);
    Object entry = null;
    if (ref != null) {
        entry = ref.get();
    }
    EntryFuture future = null;
    if (entry == null) {
        EntryFuture newEntry = new EntryFuture();
        Reference<?> newRef = new SoftReference<>(newEntry);
        do {
            if (ref != null) {
                Caches.localDescs.remove(key, ref);
            }
            ref = Caches.localDescs.putIfAbsent(key, newRef);
            if (ref != null) {
                entry = ref.get();
            }
        } while (ref != null && entry == null);
        if (entry == null) {
            future = newEntry;
        }
    }
    //如果缓存中存在,就直接返回
    if (entry instanceof ObjectStreamClass) {  // check common case first
        return (ObjectStreamClass) entry;
    }
    if (entry instanceof EntryFuture) {
        future = (EntryFuture) entry;
        if (future.getOwner() == Thread.currentThread()) {
            /*
             * Handle nested call situation described by 4803747: waiting
             * for future value to be set by a lookup() call further up the
             * stack will result in deadlock, so calculate and set the
             * future value here instead.
             */
            entry = null;
        } else {
            entry = future.get();
        }
    }
    if (entry == null) {
        try {
            //缓存获取失败,直接创建对象
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        if (future.set(entry)) {
            Caches.localDescs.put(key, new SoftReference<Object>(entry));//存入缓存
        } else {
            // nested lookup call already set future
            entry = future.get();
        }
    }
    if (entry instanceof ObjectStreamClass) {
        return (ObjectStreamClass) entry;
    } else if (entry instanceof RuntimeException) {
        throw (RuntimeException) entry;
    } else if (entry instanceof Error) {
        throw (Error) entry;
    } else {
        throw new InternalError("unexpected entry: " + entry);
    }
}
复制代码

再进入ObjectStreamClass构造方法。

private ObjectStreamClass(final Class<?> cl) {
    this.cl = cl;
    //获取了类的一些基本信息
    name = cl.getName();
    isProxy = Proxy.isProxyClass(cl);
    isEnum = Enum.class.isAssignableFrom(cl);
    serializable = Serializable.class.isAssignableFrom(cl);
    externalizable = Externalizable.class.isAssignableFrom(cl);
    Class<?> superCl = cl.getSuperclass();//获取父类类型
    superDesc = (superCl != null) ? lookup(superCl, false) : null;//父类存在,接着调用lookup方法,一直向上递归,直到父类为null。
    localDesc = this;
    if (serializable) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                if (isEnum) {
                    suid = Long.valueOf(0);
                    fields = NO_FIELDS;
                    return null;
                }
                if (cl.isArray()) {
                    fields = NO_FIELDS;
                    return null;
                }
                suid = getDeclaredSUID(cl);//这里获取了serialVersionUID,如果序列化类没有声明将为null。
                try {
                    fields = getSerialFields(cl);//最重要的一点,获取序列化的属性
                    computeFieldOffsets();
                } catch (InvalidClassException e) {
                    serializeEx = deserializeEx =
                        new ExceptionInfo(e.classname, e.getMessage());
                    fields = NO_FIELDS;
                }
                if (externalizable) {
                    cons = getExternalizableConstructor(cl);
                } else {
                    cons = getSerializableConstructor(cl);
                    //如果序列化类重写了writeObject()和readObject()方法。
                    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                        new Class<?>[] { ObjectOutputStream.class },
                        Void.TYPE);
                    readObjectMethod = getPrivateMethod(cl, "readObject",
                        new Class<?>[] { ObjectInputStream.class },
                        Void.TYPE);
                    readObjectNoDataMethod = getPrivateMethod(
                        cl, "readObjectNoData", null, Void.TYPE);
                    hasWriteObjectData = (writeObjectMethod != null);
                }
                writeReplaceMethod = getInheritableMethod(
                    cl, "writeReplace", null, Object.class);
                readResolveMethod = getInheritableMethod(
                    cl, "readResolve", null, Object.class);
                return null;
            }
        });
    } else {
        suid = Long.valueOf(0);
        fields = NO_FIELDS;
    }
    try {
        fieldRefl = getReflector(fields, this);
    } catch (InvalidClassException ex) {
        // field mismatches impossible when matching local fields vs. self
        throw new InternalError(ex);
    }
    if (deserializeEx == null) {
        if (isEnum) {
            deserializeEx = new ExceptionInfo(name, "enum type");
        } else if (cons == null) {
            deserializeEx = new ExceptionInfo(name, "no valid constructor");
        }
    }
    for (int i = 0; i < fields.length; i++) {
        if (fields[i].getField() == null) {
            defaultSerializeEx = new ExceptionInfo(
                name, "unmatched serializable field(s) declared");
        }
    }
    initialized = true;
}
复制代码

接着进入getSerialFields()方法。

private static ObjectStreamField[] getSerialFields(Class<?> cl) throws InvalidClassException {
    ObjectStreamField[] fields;
    if (Serializable.class.isAssignableFrom(cl) &&
        !Externalizable.class.isAssignableFrom(cl) &&
        !Proxy.isProxyClass(cl) &&
        !cl.isInterface())
    {
        if ((fields = getDeclaredSerialFields(cl)) == null) {
            fields = getDefaultSerialFields(cl);//又调用了这里
        }
        Arrays.sort(fields);//进行升序排列
    } else {
        fields = NO_FIELDS;
    }
    return fields;
}
复制代码

接着进入getDefaultSerialFields()方法。

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    int mask = Modifier.STATIC | Modifier.TRANSIENT;
    //看到了STATIC和TRANSIENT关键词
    for (int i = 0; i < clFields.length; i++) {
            if ((clFields[i].getModifiers() & mask) == 0) {
            //通过这一行代码,过滤掉了被STATIC和TRANSIENT修饰的序列化属性,这是第三个问题的答案!
            list.add(new ObjectStreamField(clFields[i], false, true));
        }
    }
    int size = list.size();
    return (size == 0) ? NO_FIELDS :
        list.toArray(new ObjectStreamField[size]);
}
复制代码

ObjectStreamClass相关的主要工作基本上也就结束了,在初始化中主要完成了对序列化对象的,基本信息、serialVersionUID、属性的收集;当然也包括往上所有的父类,是一个递归的过程。

我们回到writeObject0()方法中对Serializable数据的写入操作,writeOrdinaryObject(obj, desc, unshared)方法。

private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException {
    if (extendedDebugInfo) {
        debugInfoStack.push(
            (depth == 1 ? "root " : "") + "object (class "" +
            obj.getClass().getName() + "", " + obj.toString() + ")");
    }
    try {
        desc.checkSerialize();
        bout.writeByte(TC_OBJECT);//写入Object标识
        writeClassDesc(desc, false);//写了一些类信息
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc);//继续调用这个方法
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}
复制代码

writeOrdinaryObject()方法重点调用了两个方法,writeClassDesc()和writeSerialData();我们先看writeClassDesc()。

private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException {
    int handle;
    if (desc == null) {
        //对象为null,写入null标识
        writeNull();
    } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
        writeHandle(handle);
    } else if (desc.isProxy()) {
        writeProxyDesc(desc, unshared);
    } else {
        writeNonProxyDesc(desc, unshared);//一般都会调用这个方法
    }
}
复制代码

进入writeNonProxyDesc()方法。

private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException {
    bout.writeByte(TC_CLASSDESC);//写入类元信息标记位
    handles.assign(unshared ? null : desc);
    if (protocol == PROTOCOL_VERSION_1) {
        // do not invoke class descriptor write hook with old protocol
        desc.writeNonProxy(this);
    } else {
        writeClassDescriptor(desc);//重点在这里,写入了类的描述符信息
    }
    Class<?> cl = desc.forClass();
    bout.setBlockDataMode(true);
    if (cl != null && isCustomSubclass()) {
        ReflectUtil.checkPackageAccess(cl);
    }
    annotateClass(cl);
    bout.setBlockDataMode(false);
    bout.writeByte(TC_ENDBLOCKDATA);
    writeClassDesc(desc.getSuperDesc(), false);//又调用writeClassDesc(),传入父类初始化了ObjectStreamClass对象,进入递归模式,写入类的描述符信息。
}
复制代码

进入writeClassDescriptor()方法。

protected void writeClassDescriptor(ObjectStreamClass desc) {
    desc.writeNonProxy(this);
}
复制代码

又再调用了ObjectStreamClass对象本身的writeNonProxy()方法。

void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name);//写入类名
    out.writeLong(getSerialVersionUID());//再次出现serialVersionUID,这里被写入
    //判断类类型的标识
    byte flags = 0;
    if (externalizable) {
        flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
        int protocol = out.getProtocolVersion();
        if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
            flags |= ObjectStreamConstants.SC_BLOCK_DATA;
        }
    } else if (serializable) {
        flags |= ObjectStreamConstants.SC_SERIALIZABLE;
    }
    if (hasWriteObjectData) {
        flags |= ObjectStreamConstants.SC_WRITE_METHOD;
    }
    if (isEnum) {
        flags |= ObjectStreamConstants.SC_ENUM;
    }
    //写入类类型标识
    out.writeByte(flags);
    //写入序列化属性的信息
    out.writeShort(fields.length);
    for (int i = 0; i < fields.length; i++) {
        ObjectStreamField f = fields[i];
        out.writeByte(f.getTypeCode());
        out.writeUTF(f.getName());
        if (!f.isPrimitive()) {
            out.writeTypeString(f.getTypeString());
        }
    }
}
复制代码

writeNonProxy()方法中,写入了很多信息,我们还需要看一下getSerialVersionUID()方法。

public long getSerialVersionUID() {
    // REMIND: synchronize instead of relying on volatile?
    if (suid == null) {//如果ObjectStreamClass初始化时,序列化类没有定义SerialVersionUID,那么suid就是null值。
        suid = AccessController.doPrivileged(
            new PrivilegedAction<Long>() {
                public Long run() {
                    //获取默认的值,所以这就是第2个问题的一部分答案,为什么要在序列类中定义
                    //SerialVersionUID,因为如果不定义就会默认生产,而这个默认的值很可能就因为类内容的改变而改变。会产
                    //生什么具体的影响,后面继续分析!
                    return computeDefaultSUID(cl);
                }
            }
        );
    }
    return suid.longValue();
}
复制代码

分析完writeClassDesc()的流程,我们接着分析writeSerialData()方法,写入真正的数据。

private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();//获取数据布局的ClassDataSlot实例数组
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        //如果序列化对象实现了自己的writeObject()方法,进入if。
        if (slotDesc.hasWriteObjectMethod()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class "" +
                    slotDesc.getName() + "")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);//一般都是调用这里
        }
    }
}
复制代码

我们先看下如何获取数据布局的ClassDataSlot实例数组的。

ClassDataSlot[] getClassDataLayout() throws InvalidClassException {
    // REMIND: synchronize instead of relying on volatile?
    if (dataLayout == null) {
        dataLayout = getClassDataLayout0();
    }
    return dataLayout;
}
复制代码

又调用了getClassDataLayout0()方法。

private ClassDataSlot[] getClassDataLayout0() throws InvalidClassException {
    ArrayList<ClassDataSlot> slots = new ArrayList<>();
    Class<?> start = cl, end = cl;
    // locate closest non-serializable superclass
    while (end != null && Serializable.class.isAssignableFrom(end)) {
        end = end.getSuperclass();
    }
    HashSet<String> oscNames = new HashSet<>(3);
    for (ObjectStreamClass d = this; d != null; d = d.superDesc) {
        if (oscNames.contains(d.name)) {
            throw new InvalidClassException("Circular reference.");
        } else {
            oscNames.add(d.name);
        }
        // search up inheritance hierarchy for class with matching name
        String searchName = (d.cl != null) ? d.cl.getName() : d.name;
        Class<?> match = null;
        for (Class<?> c = start; c != end; c = c.getSuperclass()) {
            if (searchName.equals(c.getName())) {
                match = c;
                break;
            }
        }
        //通过遍历,把所有的ObjectStreamClass对象包装成了ClassDataSlot对象
        // add "no data" slot for each unmatched class below match
        if (match != null) {
            for (Class<?> c = start; c != match; c = c.getSuperclass()) {
                slots.add(new ClassDataSlot(
                    ObjectStreamClass.lookup(c, true), false));
            }
            start = match.getSuperclass();
        }
        // record descriptor/class pairing
        slots.add(new ClassDataSlot(d.getVariantFor(match), true));
    }
    // add "no data" slot for any leftover unmatched classes
    for (Class<?> c = start; c != end; c = c.getSuperclass()) {
        slots.add(new ClassDataSlot(
            ObjectStreamClass.lookup(c, true), false));
    }
    // order slots from superclass -> subclass
    Collections.reverse(slots);//调用了reverse反转,所以最顶端的父类ClassDataSlot对象在前。
    return slots.toArray(new ClassDataSlot[slots.size()]);
}
复制代码

把所有的ObjectStremClass对象再封装成了ClassDataSlot对象,并且进行了倒序排列。回到writeSerialData()方法中,继续进入defaultWriteFields()。

private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }
    desc.checkDefaultSerialize();
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    //获取基本数据类型的实例数据,并存入primVals数组
    desc.getPrimFieldValues(obj, primVals);
    //写入primVals数组中的数据,所以基本数据类型在这里就直接写入了。
    bout.write(primVals, 0, primDataSize, false);
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    //获取未写入的对象数据,存入objVals数组
    desc.getObjFieldValues(obj, objVals);
    for (int i = 0; i < objVals.length; i++) {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                "field (class "" + desc.getName() + "", name: "" +
                fields[numPrimFields + i].getName() + "", type: "" +
                fields[numPrimFields + i].getType() + "")");
        }
        try {
            //再调用writeObject0方法,重新进行类型判断,进行不同的写入处理;也是一种递归处理,直到所有数据写入完成
            writeObject0(objVals[i],
                         fields[numPrimFields + i].isUnshared());
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
}
复制代码

到这里,序列化的过程就结束了。

反序列化过程

以 ObjectInputStream 为例:

下面我们进入关键的readObject()方法。

public final Object readObject() throws IOException, ClassNotFoundException {
    if (enableOverride) {//初始化设置成了false,不会走。
        return readObjectOverride();
    }
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
        Object obj = readObject0(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();
        }
    }
}
复制代码

进入readObject0()方法。

private Object readObject0(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++;
    // Android-removed: ObjectInputFilter logic, to be reconsidered. http://b/110252929
    // totalObjectRefs++;
    try {
        switch (tc) {
            case TC_NULL:
                return readNull();
            case TC_REFERENCE:
                return readHandle(unshared);
            case TC_CLASS:
                return readClass(unshared);
            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                return readClassDesc(unshared);
            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));
            case TC_ARRAY:
                return checkResolve(readArray(unshared));
            case TC_ENUM:
                return checkResolve(readEnum(unshared));
            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));//进入这里
            case TC_EXCEPTION:
                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);
    }
}
复制代码

我们进入readOrdinaryObject()方法。

private Object readOrdinaryObject(boolean unshared) throws IOException {
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }
    ObjectStreamClass desc = readClassDesc(false);//读取类的描述信息
    desc.checkDeserialize();
    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
            || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }
    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }
    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        readSerialData(obj, desc);//读取序列化数据
    }
    handles.finish(passHandle);
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Android-removed: ObjectInputFilter logic, to be reconsidered. http://b/110252929
            /*
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            */
            handles.setObject(passHandle, obj = rep);
        }
    }
    return obj;
}
复制代码

我们先看下readClassDesc()方法。

private ObjectStreamClass readClassDesc(boolean unshared) throws IOException {
    byte tc = bin.peekByte();//获取类元信息标记位
    ObjectStreamClass descriptor;
    switch (tc) {
        case TC_NULL:
            descriptor = (ObjectStreamClass) readNull();
            break;
        case TC_REFERENCE:
            descriptor = (ObjectStreamClass) readHandle(unshared);
            break;
        case TC_PROXYCLASSDESC:
            descriptor = readProxyDesc(unshared);
            break;
        case TC_CLASSDESC:
            descriptor = readNonProxyDesc(unshared);//会调用这里
            break;
        default:
            throw new StreamCorruptedException(
                String.format("invalid type code: %02X", tc));
    }
    // Android-removed: ObjectInputFilter logic, to be reconsidered. http://b/110252929
    // if (descriptor != null) {
    //     validateDescriptor(descriptor);
    // }
    return descriptor;
}
复制代码

进入readNonProxyDesc()方法。

private ObjectStreamClass readNonProxyDesc(boolean unshared) throws IOException {
    if (bin.readByte() != TC_CLASSDESC) {
        throw new InternalError();
    }
    ObjectStreamClass desc = new ObjectStreamClass();
    int descHandle = handles.assign(unshared ? unsharedMarker : desc);
    passHandle = NULL_HANDLE;
    ObjectStreamClass readDesc = null;
    try {
        readDesc = readClassDescriptor();//读取存储的序列化类的描述信息
    } catch (ClassNotFoundException ex) {
        throw (IOException) new InvalidClassException(
            "failed to read class descriptor").initCause(ex);
    }
    Class<?> cl = null;
    ClassNotFoundException resolveEx = null;
    bin.setBlockDataMode(true);
    final boolean checksRequired = isCustomSubclass();
    try {
        if ((cl = resolveClass(readDesc)) == null) {
            resolveEx = new ClassNotFoundException("null class");
        } else if (checksRequired) {
            ReflectUtil.checkPackageAccess(cl);
        }
    } catch (ClassNotFoundException ex) {
        resolveEx = ex;
    }
    skipCustomData();
    desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));//初始化类描述符
    // Android-removed: ObjectInputFilter unsupported - removed filterCheck() call.
    // // Call filterCheck on the definition
    // filterCheck(desc.forClass(), -1);
    handles.finish(descHandle);
    passHandle = descHandle;
    return desc;
}
复制代码

readNonProxyDesc()方法,进行了两个非常关键的处理

  1. 通过调用readClassDescriptor()方法读取了存储的序列化类的描述信息,并初始化了ObjectStreamClass对象,赋值给了readDesc属性;
  2. 通过调用新创建的ObjectStreamClass对象desc的initNonProxy()方法,进行了初始化;

这里需要区分一下,readDesc局部属性对象是通过readClassDescriptor()方法创建的,而desc属性对象则是新创建的对象再通过initNonProxy()方法进行初始化,我们先看一下readClassDescriptor()方法。

protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
    ObjectStreamClass desc = new ObjectStreamClass();
    desc.readNonProxy(this);
    return desc;
}
复制代码

创建了一个ObjectStreamClass对象,直接再调用readNonProxy()方法。

void readNonProxy(ObjectInputStream in) throws IOException, ClassNotFoundException {
    name = in.readUTF();//类名
    suid = Long.valueOf(in.readLong());//熟悉吧,序列化写入的serialVersionUID回来了
    isProxy = false;
    byte flags = in.readByte();
    hasWriteObjectData =
        ((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0);
    hasBlockExternalData =
        ((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0);
    externalizable =
        ((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0);
    boolean sflag =
        ((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0);
    if (externalizable && sflag) {
        throw new InvalidClassException(
            name, "serializable and externalizable flags conflict");
    }
    serializable = externalizable || sflag;
    isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0);
    if (isEnum && suid.longValue() != 0L) {
        throw new InvalidClassException(name,
            "enum descriptor has non-zero serialVersionUID: " + suid);
    }
    int numFields = in.readShort();
    if (isEnum && numFields != 0) {
        throw new InvalidClassException(name,
            "enum descriptor has non-zero field count: " + numFields);
    }
    fields = (numFields > 0) ?
        new ObjectStreamField[numFields] : NO_FIELDS;
    for (int i = 0; i < numFields; i++) {
        //循环读取序列化属性信息
        char tcode = (char) in.readByte();
        String fname = in.readUTF();
        String signature = ((tcode == 'L') || (tcode == '[')) ?
            in.readTypeString() : new String(new char[] { tcode });
        try {
            fields[i] = new ObjectStreamField(fname, signature, false);
        } catch (RuntimeException e) {
            throw (IOException) new InvalidClassException(name,
                "invalid descriptor for field " + fname).initCause(e);
        }
    }
    computeFieldOffsets();
}
复制代码

分析完,我们再回过头去看新创建的ObjectStreamClass对象对initNonProxy()方法的调用。

void initNonProxy(ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException {
    //需要重点标注一下,参数model就是readDesc,cl是传入的序列化类
    long suid = Long.valueOf(model.getSerialVersionUID());//获取了序列化类的serialVersionUID()
    ObjectStreamClass osc = null;
    if (cl != null) {
        osc = lookup(cl, true);
        if (osc.isProxy) {
            throw new InvalidClassException(
                    "cannot bind non-proxy descriptor to a proxy class");
        }
        if (model.isEnum != osc.isEnum) {
            throw new InvalidClassException(model.isEnum ?
                    "cannot bind enum descriptor to a non-enum class" :
                    "cannot bind non-enum descriptor to an enum class");
        }
        if (model.serializable == osc.serializable &&
                !cl.isArray() &&
                suid != osc.getSerialVersionUID()) {//重点、重点、重点 ......
                //这就是第二个问题答案,serialVersionUID的作用,通过序列化缓存对象的
                //serialVersionUID和传入的序列化类的serialVersionUID对比来判断版本是否一致,这
                //也是为什么建议自己定义serialVersionUID的原因,避免类的改动导致serialVersionUID的变化而版本冲突报错。
            throw new InvalidClassException(osc.name,
                    "local class incompatible: " +
                            "stream classdesc serialVersionUID = " + suid +
                            ", local class serialVersionUID = " +
                            osc.getSerialVersionUID());
        }
        if (!classNamesEqual(model.name, osc.name)) {
            throw new InvalidClassException(osc.name,
                    "local class name incompatible with stream class " +
                            "name "" + model.name + """);
        }
        if (!model.isEnum) {
            if ((model.serializable == osc.serializable) &&
                    (model.externalizable != osc.externalizable)) {
                throw new InvalidClassException(osc.name,
                        "Serializable incompatible with Externalizable");
            }
            if ((model.serializable != osc.serializable) ||
                    (model.externalizable != osc.externalizable) ||
                    !(model.serializable || model.externalizable)) {
                deserializeEx = new ExceptionInfo(
                        osc.name, "class invalid for deserialization");
            }
        }
    }
    //初始化参数赋值
    this.cl = cl;
    this.resolveEx = resolveEx;
    this.superDesc = superDesc;
    name = model.name;
    this.suid = suid;
    isProxy = false;
    isEnum = model.isEnum;
    serializable = model.serializable;
    externalizable = model.externalizable;
    hasBlockExternalData = model.hasBlockExternalData;
    hasWriteObjectData = model.hasWriteObjectData;
    fields = model.fields;
    primDataSize = model.primDataSize;
    numObjFields = model.numObjFields;
    if (osc != null) {
        localDesc = osc;
        writeObjectMethod = localDesc.writeObjectMethod;
        readObjectMethod = localDesc.readObjectMethod;
        readObjectNoDataMethod = localDesc.readObjectNoDataMethod;
        writeReplaceMethod = localDesc.writeReplaceMethod;
        readResolveMethod = localDesc.readResolveMethod;
        if (deserializeEx == null) {
            deserializeEx = localDesc.deserializeEx;
        }
        cons = localDesc.cons;
    }
    fieldRefl = getReflector(fields, localDesc);
    // reassign to matched fields so as to reflect local unshared settings
    fields = fieldRefl.getFields();
    initialized = true;
}
复制代码

我们再回到readOrdinaryObject()方法去看readSerialData()方法,来获取属性数据。

private void readSerialData(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 (slots[i].hasData) {
            if (obj == null || handles.lookupException(passHandle) != null) {
                defaultReadFields(null, slotDesc); // skip field values
            } else if (slotDesc.hasReadObjectMethod()) {//如果重写了readObject()方法
                SerialCallbackContext oldContext = curContext;
                if (oldContext != null)
                    oldContext.check();
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bin.setBlockDataMode(true);
                    slotDesc.invokeReadObject(obj, this);
                } catch (ClassNotFoundException ex) {
                    /*
                     * In most cases, the handle table has already
                     * propagated a CNFException to passHandle at this
                     * point; this mark call is included to address cases
                     * where the custom readObject method has cons'ed and
                     * thrown a new CNFException of its own.
                     */
                    handles.markException(passHandle, ex);
                } finally {
                    curContext.setUsed();
                    if (oldContext!= null)
                        oldContext.check();
                    curContext = oldContext;
                    // END Android-changed: ThreadDeath cannot cause corruption on Android.
                }
                /*
                 * defaultDataEnd may have been set indirectly by custom
                 * readObject() method when calling defaultReadObject() or
                 * readFields(); clear it to restore normal read behavior.
                 */
                defaultDataEnd = false;
            } else {
                defaultReadFields(obj, slotDesc);//会执行到这里
                }
            if (slotDesc.hasWriteObjectData()) {
                skipCustomData();
            } else {
                bin.setBlockDataMode(false);
            }
        } else {
            if (obj != null &&
                slotDesc.hasReadObjectNoDataMethod() &&
                handles.lookupException(passHandle) == null)
            {
                slotDesc.invokeReadObjectNoData(obj);
            }
        }
    }
}
复制代码

进入到defaultReadFields()方法中。

private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException {
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
        bin.readFully(primVals, 0, primDataSize, false);//从流中读取基本数据类型的值
    if (obj != null) {
        desc.setPrimFieldValues(obj, primVals);//把基本数据类型的值设置到序列化类中,obj就是传入的序列化类
    }
    int objHandle = passHandle;
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    //遍历读取对象类型的序列化值
    for (int i = 0; i < objVals.length; i++) {
        ObjectStreamField f = fields[numPrimFields + i];
        objVals[i] = readObject0(f.isUnshared());//递归调用,回到readObject0()再根据不同的数据类型进行处理。
        if (f.getField() != null) {
            handles.markDependency(objHandle, passHandle);
        }
    }
    if (obj != null) {
        desc.setObjFieldValues(obj, objVals);
    }
    passHandle = objHandle;
}
复制代码

到这里,反序列化也完成了。

序列化和反序列化流程图

网络异常,图片无法展示
|

总结

实现Serializable序列化接口就是起一个标识作用,无任何实质意义。

serialVersionUID是用来判断序列化类的版本,重写serialVersionUID是为了防止因为序列化类的变动,导致默认生成的serialVersionUID不一致而冲突报错。

transientstatic修饰的属性会在ObjectStreamClass类初始化时,getDefaultSerialFields() 方法中直接过滤掉了。

相关文章
|
3月前
|
Java 开发者
Java抽象类与接口的正确使用姿势!别再写错代码了!
Java抽象类与接口的正确使用姿势!别再写错代码了!
40 3
|
5月前
|
Java 开发者
别再写错代码了!Java抽象类与接口的正确使用姿势!
【6月更文挑战第17天】在Java中,抽象类与接口助力构建灵活代码结构,提升效率。抽象类用于定义公共行为和属性,适合有层次的对象集合;接口包含抽象方法,实现多态,适合不相关对象集合。通过示例展示了如何创建抽象类和实现接口,强调理解其核心价值和使用场景的重要性,以提升代码质量和设计。正确使用抽象类与接口,让代码从平凡走向专业。
78 0
|
设计模式 Java 安全
Java中包的作用以及使用、访问权限、单例/多例设计模式、枚举和异常(附带相关面试题)
本章节讲述以下方面知识点 1.包的作用以及使用 2.单例/多例化设计模式 3.枚举 4.访问权限2.异常
79 0
Java中包的作用以及使用、访问权限、单例/多例设计模式、枚举和异常(附带相关面试题)
|
SQL 安全 Java
Java高频面试题:在DCL单例写法中,为什么主要做两次检查?
有位工作5年的小伙伴,面试的时候被问到这样一道题,说在DCL单例写法中,为什么要做两次检查。要回答好这个问题,需要知道DCL单例的写法以及为什么要这样写? 今天,我给大家详细分析一下。
138 0
|
存储 Java
Serializable接口的意义和用法
Serializable接口的意义和用法
|
存储 缓存 安全
Java之实现Serializable接口的类,transient关键字
Java之实现Serializable接口的类,transient关键字
134 0
Java之实现Serializable接口的类,transient关键字
|
JSON Java 数据库连接
87. Java序列化和反序列化为什么要实现Serializable接口
87. Java序列化和反序列化为什么要实现Serializable接口
193 0
|
JSON Java 数据库连接
面试官问:Java 序列化和反序列化为什么要实现 Serializable 接口?什么鬼??
面试官问:Java 序列化和反序列化为什么要实现 Serializable 接口?什么鬼??
242 0
|
存储 Java
RandomAccess 明明是个空接口,能有什么用呢?
Hello,大家好,我是阿粉,Java 语言中有很多有意思的设计,之前二哥的一篇文章中介绍了Serializable 空接口,今天阿粉给大家介绍一个另一个 Java 中的空接口 RandomAccess。
|
IDE Java 编译器
还不懂Java的泛型?只用这一篇文章,保证你面试对答如流
还不懂Java的泛型?只用这一篇文章,保证你面试对答如流
96 0