在开发过程中遇到需要通过给定的值来获取对应的枚举值,如下例枚举类中,需要通过传入"春"来获取SPRING。
public enum Season {
SPRING("春"),
SUMMER("夏"),
AUTUMN("秋"),
WINTER("冬");
String season;
Season(String season) {
this.season = season;
}
}
开始尝试通过Enum的内置方法valueOf()获取,使用报错:
public static void main(String[] args) {
System.out.println(Season.valueOf("春"));
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant model.Season.春
at java.lang.Enum.valueOf(Enum.java:238)
at model.Season.valueOf(Season.java:10)
at Demo.main(Demo.java:12)
我们打开valueOf源码看看他是通过什么值获取,
方法注释:
/**
* Returns the enum constant of the specified enum type with the
* specified name. The name must match exactly an identifier used
* to declare an enum constant in this type. (Extraneous whitespace
* characters are not permitted.)
**/
翻译为
返回具有指定名称的指定枚举类型的枚举常量。 名称必须与用于声明此类型的枚举常量的标识符完全匹配。 (不允许使用多余的空格字符。)
需要通过与声明枚举类型的标识完全匹配,就是说我要通过"SPRING"字符串来获取SPRING枚举类,但我需要的是通过“春”来获取到SPRING。
后来我想到一种实现方法,通过获取枚举类的构造方法,然后反射执行其构造方法来获取。先显示的通过反射看看Season的构造方法长什么样子。
public static void main(String[] args) {
Constructor[] constructors = Season.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}
}
输出:
private model.Season(java.lang.String,int,java.lang.String)
???,小朋友头上多了好多问号,明明我的构造函数只有java.lang.String类型,为什么变成了三个参数了。
于是,我在Season中新增了一个无参构造,再次运行上述代码。
输出:
private model.Season(java.lang.String,int,java.lang.String)
private model.Season(java.lang.String,int)
发现无参构造默认加了两个参数,我们打开抽象类Enum看看,发现里面存在下列构造方法,且通过注释可以得知,程序员无需调用此构造方法,当声明枚举类时编译器会自动实现该方法,而参数name、ordinal分别是“与声明枚举类型标识完全匹配的字符串”和“枚举声明在枚举类中的序号,从0开始”。
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
所以我需要通过传入name、ordinal、和自定义的值("春")来获取对应的SPRING。但是我可能不知道其name和ordinal。或者当我知道其name的情况下,我直接使用valueOf(name)直接就可以获取相关的枚举值了。所有此类方法行不通,而且Class类中的newInstance方法有这么一段代码,就是说如果通过newInstance来反射获取枚举类型的对象将抛出IllegalArgumentException异常。我们用代码试验一下:
newInstance源码:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
Constructor constructor = Season.class.getDeclaredConstructor(String.class, int.class, String.class);
constructor.setAccessible(true);
Season season = constructor.newInstance("SPRING", 0, "春");
System.out.println(season);
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at Demo.main(Demo.java:23)
奇怪的知识又多了。这个时候我想到通过利用我在上篇文章中的方法获取。(枚举工具类-反射获取枚举类中变量的所有值)
非反射代码实现如下,可以更好的理解设计思路:(需要在枚举类Season类中添加getSeason()方法)
public static void main(String[] args) {
System.out.println(valueOf("春"));
}
public static Season valueOf(String season) {
Season[] values = Season.values();
for (Season value : values) {
String seasonName = value.getSeason();
if (seasonName.equals("春")) {
return value;
}
}
return null;
}
}
对应的反射实现的工具类如下:
public class Demo {
public static void main(String[] args) {
System.out.println("正常方式获取:" + valueOf("春"));
System.out.println("反射工具类获取:" + EnumUtils.valueOf(Season.class, "春"));
}
public static Season valueOf(String season) {
Season[] values = Season.values();
for (Season value : values) {
String seasonName = value.getSeason();
if (seasonName.equals("春")) {
return value;
}
}
return null;
}
}
class EnumUtils {
/**
* 获取枚举类中变量的列表
* @param enumClass 枚举类的class
* @param paramClass 参数类型的class
* @param
* @return
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
public static , T> List getValues(Class enumClass, Class paramClass)
throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
E[] values = values(enumClass);
Field field = getField(enumClass, paramClass);
field.setAccessible(true);
List resultList = new ArrayList<>();
for (E e : values) {
T t = (T)field.get(e);
resultList.add(t);
}
return resultList;
}
/**
* 通过反射调用枚举类的values获取所有枚举类
* @param enumClass
* @param
* @return
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
private static > E[] values(Class enumClass)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method valuesMethod = enumClass.getMethod("values");
Object valuesObj = valuesMethod.invoke(enumClass);
E[] values = (E[]) valuesObj;
return values;
}
/**
* 通过field的类型获取field
* 说明:
* 因为枚举类中只有Enum类型的Field,所有可以通过指定Field的类型来获取Field
* @param enumClass
* @param paramClass
* @param
* @param
* @return
*/
private static Field getField(Class enumClass, Class paramClass) throws IllegalAccessException {
//如果是包装类获取包装类的基本类型
Class basicClass = getBasicClass(paramClass);
//获取类型相同的Field(类型相同或与其基本类型相同)
List fieldList = Arrays.stream(enumClass.getDeclaredFields())
.filter(f -> f.getType() == paramClass || f.getType() == basicClass).collect(Collectors.toList());
if (fieldList.size() != 1) {
//抛出异常,只支持一个属性
throw new IllegalArgumentException(paramClass + "类型属性数量异常。");
}
return fieldList.get(0);
}
/**
* 获取class的基本类型的class
* 说明:
* 存在基本类型将返回其基本类型,否则返回null
* 如传入Integer.class将返回int.class,传入String.class返回null
* @param paramClass
* @return
* @throws IllegalAccessException
*/
private static Class getBasicClass(Class paramClass) throws IllegalAccessException {
Field typeField = null;
try {
//尝试获取包装类的TYPE
typeField = paramClass.getField("TYPE");
} catch (NoSuchFieldException e) {
return null;
}
//获取包装类TYPE成功,获取TYPE属性值(因为类型为static,所以传入null)
return (Class) typeField.get(null);
}
public static > E valueOf(Class eClass, Object... parameterTypes) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class[] paramClassArr = new Class[parameterTypes.length + 2];
String name = (String) eClass.getMethod("name").invoke(null);
int ordinal = (int) eClass.getMethod("ordinal").invoke(null);
paramClassArr[0] = name.getClass();
paramClassArr[1] = int.class;
for (int i = 2; i < parameterTypes.length; i++) {
paramClassArr[i] = parameterTypes[i].getClass();
}
Constructor constructor = eClass.getDeclaredConstructor(paramClassArr);
constructor.setAccessible(true);
Object o = constructor.newInstance(name, ordinal, parameterTypes);
return (E)o;
}
/**
* 通过指定参数值获取枚举类型
* @param enumClass
* @param value
* @return
*/
public static > E valueOf(Class enumClass, Object value) {
E[] values = null;
Field field = null;
try {
field = getField(enumClass, value.getClass());
field.setAccessible(true);
values = values(enumClass);
for (E e : values) {
Object t = field.get(e);
if (Objects.equals(t, value)) {
return e;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
输出:
正常方式获取:SPRING
反射工具类获取:SPRING
注:上述好多基本方法在文章 枚举工具类-反射获取枚举类中变量的所有值 中已详细说明,此处不冗余讲述,如有不清楚的地方可留言。也请大佬指出不足之处。
通过上述工具类的开发,触及到了好多知识盲区,不过正是这样也拓展了知识,同时加深了对知识点的理解。
当然在使用反射工具类时也有一些弊端,如需要=抛出比较多的异常。还有,上述工具类只能针对一个自定义的值,当存在多个自定义值时将不能实现。
这里我想到了一种实现方式,能够解决这种多值问题,但只针对多个自定义值的类型不一样的情况,如一个值类型时String,一个是int,这样可以通过类型获取到指定的值。但如果存在两个自定义的值都是String类型时,将无法通过反射知道两个String将如何分配,这里我没有想到好的方式解决。
由于时间关系,类型不同的多值实现将在下次更新。