K:枚举的线程安全性及其序列化问题

  枚举是如何保证线程安全的且其在序列化和反序列化的操作中是单例的?

  要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后代码内容如下:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        return (T[])$VALUES.clone();
    }
 
    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(T, s);
    }
 
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T $VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        $VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,我们看到这个类中有几个属性和方法。
我们可以看到:

public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T $VALUES[];
static
{
    SPRING = new T("SPRING", 0);
    SUMMER = new T("SUMMER", 1);
    AUTUMN = new T("AUTUMN", 2);
    WINTER = new T("WINTER", 3);
    $VALUES = (new T[] {
        SPRING, SUMMER, AUTUMN, WINTER
    });
}

都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

为什么用枚举实现的单例是最好的方式

在单例模式的七种写法中,我们看到一共有七种实现单例的方式,其中,Effective Java作者Josh Bloch提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?

1. 枚举写法简单

  写法简单这个大家看看单例模式的七种写法里面的实现就知道区别了。

public enum EasySingleton{
    INSTANCE;
}

你可以通过EasySingleton.INSTANCE来访问。

2. 枚举自己处理序列化

  我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

  Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

翻译如下(限于本人的英文水平,翻译不好的地方请见谅):
枚举常数的序列化方式不同于普通Serializable或者Externalizable对象的序列化(相关博文:K:java中序列化的两种方式—Serializable或Externalizable)。枚举常数的序列化表单仅由其name属性构成;常数的字段值不存在于表单中。在序列化一个枚举常量时,ObjectOutputStream对象会写入由枚举常量的name方法所返回的值(一般也就是name属性的值,也就是枚举常量的名称)。在反序列化枚举常量时,通过ObjectInputStream对象从相关的流中读取枚举常量的name属性值(也就是枚举常量的名称),然后通过调用java.lang.Enum对象的valueOf方法,将从流中获取的枚举常量的枚举类型(指的是继承了Enum类的相关的子类)及其常量名称(也就是枚举常量的名称)一起作为该方法的参数传递给该valueOf方法。像其它Serializable或者Externalizable对象一样,枚举常量可以作为随后出现的序列化流的反向引用。枚举常量被序列化的过程无法自定义:在序列化和反序列化期间将忽略由枚举类型定义的任何特定于该类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法。类似地,任何serialPersistentFields或serialVersionUID字段声明也被将被忽略。所有的枚举类型的fixedserialVersionUID值都是0L。记录枚举类型的可序列化字段其相关的数据是不必要的,因为发送的数据类型并没有发生变化。
以下代码演示说明枚举常量的序列化只保存了其枚举常量的name属性值

示例代码:

package other.serial;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 该类用于测试枚举类的序列化是不是只是序列化了name属性的值,也就是枚举常量的常量名
 * 
 * @author 学徒
 * 
 */
public enum AS implements Serializable
{
	H("sss");
	private String names;

	private AS(String names)
	{
		this.names = names;
	}

	public void setNames(String names)
	{
		this.names = names;
	}

	public String getNames()
	{
		return this.names;
	}
}

class AB
{
	public static void main(String args[]) throws Exception
	{
		// 设置枚举对象的names属性值为as
		AS.H.setNames("as");
		System.out.println("原始枚举对象的属性值为" + AS.H.getNames());
		// 对枚举对象进行序列化,如果其还序列化了相关的域值,则其应该保留了names属性域的值为as
		new ObjectOutputStream(new FileOutputStream("H:\\as.txt"))
				.writeObject(AS.H);
		System.out.println("序列化成功");
		// 修改枚举对象中的names属性域的属性值
		AS.H.setNames("SS");
		// 对已序列化的对象进行反序列化处理,若其还序列化了枚举对象的相关的域值,则其反序列化出来的names的属性值应当为as
		AS a = (AS) new ObjectInputStream(new FileInputStream("H:\\as.txt"))
				.readObject();
		System.out.println("反序列化成功");
		// 通过输出结果判断
		System.out.println("反序列化的对象的值" + a.getNames());
		System.out.println(a == AS.H);
	}
}


输出结果:
原始枚举对象的属性值为as
序列化成功
反序列化成功
反序列化的对象的值SS
true

  通过分析其相应的输出结果可以看出枚举类型对象在序列化的时候,枚举常数的序列化仅序列化了其name属性值,也就是枚举常量名称

  概括起来就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

我们看一下这个valueOf方法:

public static <T extends Enum<T>>T valueOf(Class<T> enumType,String name) 
{  
    T result = enumType.enumConstantDirectory().get(name);  
    if (result != null)  
        return result;  
    if (name == null)  
        throw new NullPointerException("Name is null");  
    throw new IllegalArgumentException(  "No enum const " + enumType +"." + name);  
}

  从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以,JVM对序列化有保证。

回到目录|·(工)·)

posted @ 2017-12-10 21:28  林学徒  阅读(...)  评论(...编辑  收藏