枚举深入,包括序列化安全/反射安全

开始前关于枚举的一些问题

1.枚举允许继承类吗?
2.枚举允许实现接口吗?
3.枚举可以用等号比较吗?
4.可以继承枚举吗?
5.枚举可以实现单例模式吗?
6.当使用compareTo()比较枚举时,比较的是什么?
7.当使用equals()比较枚举的时候,比较的是什么?

枚举是什么?

枚举是JDK1.5新增的一种数据类型
枚举类是一种特殊的类,它和普通的类一样,有自己的成员变量、成员方法、构造器

简单新建一个枚举类

public enum Color {
    RED(1), YELLOW(2);
    public int code;

    Color(int code) {
        this.code = code;
    }
}

使用JAD反编译

public final class Color extends Enum
{

    public static Color[] values()
    {
        return (Color[])$VALUES.clone();
    }

    public static Color valueOf(String name)
    {
        return (Color)Enum.valueOf(com/example/demo/function20191030/Color, name);
    }

    private Color(String s, int i, int code)
    {
        super(s, i);
        this.code = code;
    }

    public static final Color RED;
    public static final Color YELLOW;
    public int code;
    private static final Color $VALUES[];

    static 
    {
        RED = new Color("RED", 0, 1);
        YELLOW = new Color("YELLOW", 1, 2);
        $VALUES = (new Color[] {
            RED, YELLOW
        });
    }
}

可以看到枚举类其实和普通的类一样,继承类Enum,这个稍后再看,由final修饰,因此无法被继承;
每个枚举实例都是一个static final修饰的类对象,并且由一个static final修饰的数组保存,因此枚举实例程序运行时无法修改;
一个静态代码块用于初始化静态类对象和数组;
一个静态的values()方法,用于返回所有的类对象;
一个静态的valueOf()方法,根据name参数返回对应的类实例;
构造函数是private修饰的,有两个默认参数和自定义参数;
在静态块里看到调用时的传参,可以看到第一个参数记录的是枚举实例的名称,第二个参数记录的是枚举实例创建时的顺序;
实例都是静态类型的,当一个java类第一次被真正使用要的时候静态资源被初始化,因此创建一个enum类型是线程安全的,并且没有延迟初始化;

再看看父类Enum

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
         /**
     * The name of this enum constant, as declared in the enum declaration.
     * Most programmers should use the {@link #toString} method rather than
     * accessing this field.
     */
    private final String name;

    public final String name() {
        return name;
    }
/**
     * The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this field.  It is designed
     * for use by sophisticated enum-based data structures, such as
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public String toString() {
        return name;
    }

    public final boolean equals(Object other) {
        return this==other;
    }

    public final int hashCode() {
        return super.hashCode();
    }

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
                self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    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 constant " + enumType.getCanonicalName() + "." + name);
    }
/**
     * enum classes cannot have finalize methods.
     */
    protected final void finalize() { }

    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}

可以看到Enum类有两个成员变量,name和ordinal,根据注释可以得知name保存枚举常量的名字,ordinal记录常量声明的顺序;
toString方法直接返回常量名称;
equals方法直接==比较内存地址;
compareTo方法首先比较clss,然后比较的是两者的声明顺序;
另外finalize被final定义不能重载,因为枚举变量永远不会被回收,它是一个常量

小结(回答开头的问题)

1.不允许,因为JVM生成枚举类的时候已经默认继承了Enum类,JAVA不允许多继承
2.可以,枚举其实就是一个普通的类
3.可以,枚举常量等号比较的就是内存地址
4.不可以,JVM生成的枚举类有final修饰
5.可以,枚举本身维护的类对象就比较相似于单例模式
6.由源码可知比较的是声明顺序
7.由源码可知比较的是内存地址

阿里巴巴对枚举的限制

【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象。
思考:可能考虑到了服务端与客户端版本不一致导致序列化协议问题,即
第一版接口,gender定义了MALE和FEMALE,返回给客户端,很正常。
第二版接口,gender增加了TRANSGENDER这个类型,客户端肯定不能及时升级,所以这时候,服务端返回TRANSGENDER,客户端没法反序列化,肯定出问题。
但是看看Apache Thrift的代码,人家根本就没有限制枚举,如果发序列化时发现了新的枚举值,则这个字段设置为null。

关于枚举的序列化问题

下面是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.
大体意思是枚举常量的序列化方式与普通可序列化对象或外部对象不同。若要序列化枚举常量,ObjectOutputStream将写入枚举常量的name方法返回的值。要反序列化枚举常量,ObjectInputStream从流中读取常量名称;然后通过调用java.lang.Enum.value eOf方法获得反序列化常量,并将常数的枚举类型连同接收的常量名称作为参数传递。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

   Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
T[] getEnumConstantsShared() {
        if (enumConstants == null) {
            if (!isEnum()) return null;
            try {
                final Method values = getMethod("values");
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                                values.setAccessible(true);
                                return null;
                            }
                        });
                @SuppressWarnings("unchecked")
                T[] temporaryConstants = (T[])values.invoke(null);
                enumConstants = temporaryConstants;
            }
            // These can happen when users concoct enum-like classes
            // that don't comply with the enum spec.
            catch (InvocationTargetException | NoSuchMethodException |
                   IllegalAccessException ex) { return null; }
        }
        return enumConstants;
    }

可以看到valueOf这个方法会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,进到enumConstantDirectory后最后会发现用了反射调用了enumType的values方法静态方法
接下来用代码侧面论证下:

首先枚举类
public enum Color {
    RED(1), YELLOW(2),GREEN(3);
    public int code;

    Color(int code) {
        this.code = code;
    }

}
 public static void out(File file,Object red) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(file));

        oos.writeObject(red);
        oos.close();
    }

    public static Object read(File file) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(file));
        Color aaa = (Color) ois.readObject();
        return aaa;
    }
    
首先先序列化
Color red = Color.RED;
File file = new File("/Users/huwq_galaxy/Desktop/out.ser");
out(file,red);
接着修改枚举常量RED的值
public enum Color {
    RED(5), YELLOW(2),GREEN(3);
    public int code;

    Color(int code) {
        this.code = code;
    }

}
执行反序列化方法
Color red = Color.RED;
File file = new File("/Users/huwq_galaxy/Desktop/out.ser");
Color aaa = (Color) read(file);
System.out.println(aaa==red);
System.out.println(aaa.code);

执行结果为

true
5

因此可以得知枚举序列化并没有序列化成员变量。
而删除了Color.RED到枚举常量后再反序列化则报错 java.io.InvalidObjectException: enum constant RED does not exist in class com…

同理分布式项目中不能在接口返回值定义枚举就是因为服务端定义了新的枚举类而客户端可能会找不到新的枚举值而导致调用失败。

枚举实现单例模式

因为枚举的序列化特性,所以用枚举实现序列化可以防止序列化对单例的破坏(序列化破坏单例另开一文讨论)

public enum SingletonCreate {
    INSTANCE;
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "SingletonCreate{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public static void out(File file,Object red) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(
            new FileOutputStream(file));

    oos.writeObject(red);
    oos.close();
}

public static Object read(File file) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream(file));
    SingletonCreate aaa = (SingletonCreate) ois.readObject();
    return aaa;
}

public static Object read(File file) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream(file));
    SingletonCreate aaa = (SingletonCreate) ois.readObject();
    return aaa;
}
先序列化
SingletonCreate instance = SingletonCreate.INSTANCE;
instance.setAge(18);
instance.setName("王武");
File file = new File("/Users/huwq_galaxy/Desktop/out.ser");
out(file,instance);
再反序列化比较
ic static void main(String args[]) throws IOException, ClassNotFoundException {
SingletonCreate instance = SingletonCreate.INSTANCE;
instance.setAge(18);
instance.setName("王武");
File file = new File("/Users/huwq_galaxy/Desktop/out.ser");
SingletonCreate aaa = (SingletonCreate) read(file);
System.out.println(aaa==instance);
System.out.println(aaa);

输出

true

可以看到枚举实现单例能够防止序列化对单例的破坏,JVM保证了每个枚举实例变量的唯一性
再试试反射能不能创建实例

Class<Color> colorClass = Color.class;
Constructor<?> cons = colorClass.getDeclaredConstructor(String.class,int.class,int.class);
cons.setAccessible(true);
Color two_instance = (Color) cons.newInstance("TWO_INSTANCE", 2, 2);
System.out.println(two_instance.code);

执行后报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

查看反射源码

    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

可以看到判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
因此也防止了反射对于单例模式的破坏

枚举常量内使用抽象方法
public enum Operation {
    PLUS {
        @Override
        public double calculate(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        public double calculate(double x, double y) {
            // TODO Auto-generated method stub
            return x - y;
        }
    },
    TIMES {
        @Override
        public double calculate(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        public double calculate(double x, double y) {
            return x / y;
        }
    };

    //为该枚举类定义一个抽象方法,枚举类中所有的枚举值都必须实现这个方法
    public abstract double calculate(double x, double y);

}
System.out.println("6 + 2 = " + Operation.PLUS.calculate(6, 3));
System.out.println("6 - 2 = " + Operation.MINUS.calculate(6, 2));
System.out.println("6 * 2 = " + Operation.TIMES.calculate(6, 2));
System.out.println("6 / 2 = " + Operation.DIVIDE.calculate(6, 2));
枚举常量中使用Map
public enum ReportSelectGroupItemEnum {
    TAXI("taxi", new HashMap() {
        {
            put("companyName", "GET_YG_COMPANY_NAME(COMPANY_ID) COMPANYNAME");
        }
    }),
    BUS("bus", new HashMap() {
        {
            put("routeName", "GET_BUS_LINE_ROUTENAME(ROUTEID) ROUTENAME");
            put("subrouteName", "GET_BUS_LINE_SUBROUTENAME(SUBROUTEID) SUBROUTENAME");
            put("segmentName", "GET_BUS_LINE_SEGMENTNAME(SEGMENTID) SEGMENTNAME");
            put("stationid", "STATION");
        }
    });
    private String reportType;
    private Map<String, String> groupItemsMap;

    ReportSelectGroupItemEnum(String reportType, Map<String, String> groupItemsMap) {
        this.reportType = reportType;
        this.groupItemsMap = groupItemsMap;
    }
}
总结
  1. 线程安全
  2. 延迟加载
  3. 序列化与反序列化安全
  4. 反射安全
  5. 是一个特殊的普通Java类,允许实现接口,不允许继承或被继承
  6. 所有枚举实例是一个个点静态类对象,由一个静态数组维护,由静态块在第一次被调用时初始化
  7. compareTo方法比较的是声明顺序
  8. equals比较的是内存地址
  9. 序列化的是name
  10. 不可回收

参考

https://blog.csdn.net/javazejian/article/details/71333103

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值