序言

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

Java 领域的对象如何传输

socket

public class User implements Serializable {
	private String id;
	private String name;
	
	public String getId() {
		return id;
	}
	
	public void setId(String id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString() {
		return "User{" +
				"id='" + id + '\'' +
				", name='" + name + '\'' +
				'}';
	}
}
public class ServerProvider {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ServerSocket serverSocket = new ServerSocket(8080);
		Socket socket = serverSocket.accept();
		ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
		User user = (User) objectInputStream.readObject();
		System.out.println(user);
		serverSocket.close();
	}
}
public class SocketClientConsumer {
	public static void main(String[] args) throws IOException {
		Socket socket = new Socket("127.0.0.1", 8080);
		User user = new User();
		user.setId("111");
		user.setName("小明");
		ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
		oos.writeObject(user);
		oos.close();
	}
}

我们发现对 User 这个类增加一个 Serializable,就可以解决 Java 对象的网络传输问题。

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在 JVM 停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能简单来说序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化。

序列化的高阶认识

简单认识一下 Java 原生序列化

前面的代码中演示了,如何通过 JDK 提供了 Java 对象的序列化方式实现对象序列化传输,主要通过输出流java.io.ObjectOutputStream和对象输入流java.io.ObjectInputStream来实现。java.io.ObjectOutputStream:表示对象输出流 , 它的 writeObject(Object obj)方法可以对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。java.io.ObjectInputStream:表示对象输入流 ,它的 readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回需要注意的是,被序列化的对象需要实现 java.io.Serializable 接口。

serialVersionUID 的作用

字面意思上是序列化的版本号,凡是实现 Serializable 接口的类都有一个表示序列化版本标识符的静态变量。

如下代码:先序列化没有uid的到文件,再加上uid去反序列化,结果会如何?

public class SerialUtil implements Closeable {
	public static ObjectOutputStream oos = null;
	public static ObjectInputStream ois = null;
	
	public static void ser(User user) throws IOException {
		 oos = new ObjectOutputStream(new FileOutputStream("src/main/resources/bytes.txt"));
		oos.writeObject(user); //将对象写入ObjectOutputStream
		
	}
	
	
	public static void deSer() throws IOException, ClassNotFoundException {
		 ois = new ObjectInputStream(new FileInputStream("src/main/resources/bytes.txt"));
		User user1 = (User) ois.readObject(); //将对象写入ObjectOutputStream
		System.out.println(user1);
	}
	
	@Override
	public void close() throws IOException {
		if (oos != null)oos.close();
		if (ois != null)ois.close();
	}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
//		User user = new User();
//		user.setId("111");
//		user.setName("小明");
//		SerialUtil.ser(user);
		SerialUtil.deSer();
	}
}

结果是:

Exception in thread "main" java.io.InvalidClassException: com.anxin.serializable.User; local class incompatible: stream classdesc serialVersionUID = -637521579295226403, local class serialVersionUID = -895973149236497235
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at com.anxin.serializable.SerialUtil.deSer(SerialUtil.java:19)
	at com.anxin.serializable.Test1.main(Test1.java:11)

Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException。

从结果可以看出,文件流中的 class 和 classpath 中的 class,也就是修改过后的 class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从错误结果来看,如果没有为指定的 class 配置 serialVersionUID,那么 java 编译器会自动给这个 class 进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的 UID 就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以,由于没有显指定 serialVersionUID,编译器又为我们生成了一个 UID,当然和前面保存在文件中的那个不会一样了,于是就出现了 2 个序列化版本号不一致的错误。因此,只要我们自己指定了 serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。

tips: serialVersionUID 有两种显示的生成方式:
一是默认的 1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。

Transient 关键字

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是0,对象型的是 null。

绕开 transient 机制的办法

虽然 name 被 transient 修饰,但是通过我们写的这两个方法依然能够使得 name 字段正确被序列化和反序列化

public class User implements Serializable {
	private String id;
	private transient  String name;
	private final static Long serialVersionUID = -121321332423342L;
	public String getId() {
		return id;
	}
	
	public void setId(String id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString() {
		return "User{" +
				"id='" + id + '\'' +
				", name='" + name + '\'' +
				'}';
	}
	
	private void writeObject(java.io.ObjectOutputStream s) throws IOException {
		s.defaultWriteObject();
		
		s.writeObject(name);
	}
	
	private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
		s.defaultReadObject();
		name=(String)s.readObject();
	}
}

重写writeObject 和 readObject 可以照样实例化被修饰的字段,Why?

writeObject 和 readObject 原理

writeObject 和 readObject 是两个私有的方法,他们是什么时候被调用的呢?从运行结果来看,它确实被调用。而且他们并不存在于 Java.lang.Object,也没有在 Serializable 中去声明。我们唯一的猜想应该还是和 ObjectInputStream 和 ObjectOutputStream 有关系,所以基于这个入口去看看在哪个地方有调用。

void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (writeObjectMethod != null) {
            try {
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }
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()) {
                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);
            }
        }
    }

从源码层面来分析可以看到,readObject 是通过反射来调用的。

总结

  1. Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
  3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进
    行序列化(实现深度克隆)
  4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和readObject