枚举
简介
枚举:enumeration,jdk1.5中引入的新特性,用于管理和使用常量
入门案例
第一步:定义枚举,这里定义一个动物类,里面枚举了多种动物
public enum AnimalEnum {CAT, // 猫DOG, // 狗PIG // 猪
}
第二步:使用枚举类,这里会打印枚举类中的枚举值
public static void main(String[] args) {// 打印枚举类中的枚举值System.out.println("AnimalEnum.CAT = " + AnimalEnum.CAT); // CAT// 枚举值的字符串形式,和它的声明是一致的,可以把枚举值当做一个字符串常量来使用System.out.println("AnimalEnum.CAT.toString().equals(\"CAT\") = " + AnimalEnum.CAT.toString().equals("CAT")); // true
}
总结:可以把枚举类中的枚举值当做字符串常量去使用,入门案例中演示了枚举类最基本的使用方式。
基本使用
语法
枚举类中声明的常量,本质上,是以枚举类为数据类型的实例。
定义枚举类的格式:
[访问修饰符] enum <enum_name> {常量[(构造器参数)] [{ // 抽象方法的实现 }] [, ....] [;]成员变量;构造方法;自定义方法;抽象方法;
}
从格式上看,枚举类中可以定义构造方法、普通方法、抽象方法,因为枚举类中声明的常量本质上是以枚举类为数据类型的实例,这些实例中可以存储数据,也可以有自己的行为,不过在枚举类中定义方法时在语法上有一些要求。
编写枚举类的语法要求:
- 先定义枚举值,后定义方法,枚举值和方法之间用分号隔开。
- 为枚举类定义成员变量和构造方法:构造方法定义了枚举值中可以存储什么数据,构造方法默认被private修饰,所以用户无需为构造方法指明访问修饰符,这是为了防止枚举类被外部实例化。
- 为枚举类定义普通方法:普通方法可以为枚举值添加一些行为。
- 为枚举类定义抽象方法:枚举类中可以声明一个抽象方法,然后每个枚举类的实例实现此抽象方法。枚举类还可以实现某个接口,功能上类似于在枚举类中定义抽象方法。
案例1:为枚举类定义构造方法和普通方法
第一步:定义枚举类
public enum ColorEnum {// 枚举类中如果定义了构造方法,声明枚举值时要使用这个构造方法,它表示枚举值中可以存储的数据RED(1, "RED", "红色"),BLUE(2, "BLUE", "蓝色"),BLACK(3, "BLACK", "黑色");public Integer i;public String name;public String desc;// 构造方法ColorEnum(int i, String name, String desc) {this.i = i;this.name = name;this.desc = desc;}// 普通方法public void say() {System.out.println("我的颜色是:" + this.name);}
}
第二步:使用枚举类
public static void main(String[] args) {// 1. 使用枚举值System.out.println("ColorEnum.RED = " + ColorEnum.RED); // ColorEnum.RED = RED// 2. 枚举值中的普通方法ColorEnum.RED.say(); // 我的颜色是:RED
}
案例2:为枚举类定义抽象方法
第一步:定义枚举类
public enum OperationEnum {PLUS(1, "加法") {@Overridepublic double apply(double x, double y) {return x + y;}},MINUS (2, "减法") {@Overridepublic double apply(double x, double y) {return x - y;}};public final Integer id;public final String name;// 构造方法OperationEnum(Integer id, String name) {this.id = id;this.name = name;}// 声明抽象方法public abstract double apply(double x, double y);
}
第二步:使用枚举类
public static void main(String[] args) {// 枚举类中的抽象方法double apply = OperationEnum.PLUS.apply(1, 2);System.out.println("apply = " + apply); // 3
}
案例3:枚举类实现接口
// 定义接口
public interface Inter {double apply(double a, double b);
}// 定义枚举类
public enum Operation implements Inter {MINUS{public double apply(double a, double b){return a - b;}},PLUS{public double apply(double a, double b){return a + b;}};
}
使用方式和在枚举类中定义抽象方法基本类型。
枚举类的反编译
枚举类是一种特殊的类,枚举类和普通类一样,也会生成一个类文件,Java编译器会为枚举类添加许多方法。
案例:入门案例中的Animal类,使用javap命令来反编译 javap -v AnimalEnum.class
,这里只展示反编译后的部分结果。
// 枚举类的底层继承了Enum类
public final class org.wyj.enumeration.AnimalEnum extends java.lang.Enum<org.wyj.enumeration.AnimalEnum>
{public static final org.wyj.enumeration.AnimalEnum CAT; // CATdescriptor: Lorg/wyj/enumeration/AnimalEnum;flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUMpublic static final org.wyj.enumeration.AnimalEnum DOG; // DOGdescriptor: Lorg/wyj/enumeration/AnimalEnum;flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUMpublic static final org.wyj.enumeration.AnimalEnum PIG; // PIGdescriptor: Lorg/wyj/enumeration/AnimalEnum;flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM// values方法,返回枚举类中的所有实例public static org.wyj.enumeration.AnimalEnum[] values();descriptor: ()[Lorg/wyj/enumeration/AnimalEnum;flags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=0, args_size=00: getstatic #1 // Field $VALUES:[Lorg/wyj/enumeration/AnimalEnum;3: invokevirtual #2 // Method "[Lorg/wyj/enumeration/AnimalEnum;".clone:()Ljava/lang/Object;6: checkcast #3 // class "[Lorg/wyj/enumeration/AnimalEnum;"9: areturnLineNumberTable:line 3: 0// valueOf方法,根据字符串常量来查找枚举类实例public static org.wyj.enumeration.AnimalEnum valueOf(java.lang.String);descriptor: (Ljava/lang/String;)Lorg/wyj/enumeration/AnimalEnum;flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: ldc #4 // class org/wyj/enumeration/AnimalEnum2: aload_03: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;6: checkcast #4 // class org/wyj/enumeration/AnimalEnum9: areturnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 10 0 name Ljava/lang/String;
}
案例2:反编译有抽象方法的枚举类,javap -v OperationEnum.class
// 有抽象方法的枚举类,枚举类在底层实际上是一个抽象类
public abstract class org.wyj.enumeration.OperationEnum extends java.lang.Enum<org.wyj.enumeration.OperationEnum>
{public static final org.wyj.enumeration.OperationEnum PLUS;descriptor: Lorg/wyj/enumeration/OperationEnum;flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUMpublic static final org.wyj.enumeration.OperationEnum MINUS;descriptor: Lorg/wyj/enumeration/OperationEnum;flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
}
枚举类中的每个枚举值都会生成一个匿名内部类,javap -v "OperationEnum\$1.class"
,注意,这里美元符在命令行需要特殊处理
final class org.wyj.enumeration.OperationEnum$1 extends org.wyj.enumeration.OperationEnum
{// 构造方法org.wyj.enumeration.OperationEnum$1(java.lang.String, int, java.lang.Integer, java.lang.String);descriptor: (Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;)Vflags:Code:stack=6, locals=5, args_size=50: aload_01: aload_12: iload_23: aload_34: aload 46: aconst_null7: invokespecial #1 // Method org/wyj/enumeration/OperationEnum."<init>":(Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;Lorg/wyj/enumeration/OperationEnum$1;)V10: returnLineNumberTable:line 5: 0LocalVariableTable:Start Length Slot Name Signature0 11 0 this Lorg/wyj/enumeration/OperationEnum$1;0 11 3 id Ljava/lang/Integer;0 11 4 name Ljava/lang/String;// 实现父类中的抽象方法public double apply(double, double);descriptor: (DD)Dflags: ACC_PUBLICCode:stack=4, locals=5, args_size=30: dload_11: dload_32: dadd // add命令,证明这是加法3: dreturnLineNumberTable:line 8: 0LocalVariableTable:Start Length Slot Name Signature0 4 0 this Lorg/wyj/enumeration/OperationEnum$1;0 4 1 x D0 4 3 y D
}
总结:反编译结果中枚举类的成员,可以看到,
- 枚举类默认继承了Enum类
- 枚举类中有两个重要的静态方法:
- values():
public static 枚举类[] values();
:返回枚举类中所有常量组成的数组 - valueOf(String):
public static 枚举类 valueOf(java.lang.String);
:传入枚举类中常量的字符串形式,返回枚举类中的常量
- values():
- 声明了抽象方法的枚举类,枚举类中的每个枚举值都会使用一个匿名内部类来实现
枚举类的默认父类 Enum类
所有的枚举类都默认继承了Enum类。
源码分析:
public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable {// 枚举值的字符串形式private final String name;public final String name() {return name;}// 表示枚举值在枚举类中被声明的顺序,从0开始计数private final int ordinal;public final int ordinal() {return ordinal;}// 构造方法protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}// 重写Object类中的方法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() && // optimizationself.getDeclaringClass() != other.getDeclaringClass())throw new ClassCastException();return self.ordinal - other.ordinal;}// 阻止反序列化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");}
}
枚举值的默认父类几乎不会被使用到,这里只做了解
实战案例
案例1:switch和枚举类
如果switch语句对应的变量是一个枚举类的实例,case语句后必须直接使用枚举类中的常量
案例:
public static void main(String[] args) {AnimalEnum DOG = AnimalEnum.DOG;switch (DOG) {case DOG:System.out.println("狗"); // 狗break;case PIG:System.out.println("猪");break;case CAT:System.out.println("猫");break;default:System.out.println("未知");break;}
}
案例2:生产中定义枚举类的常见方式
public enum ColorEnum {// 枚举类中如果定义了构造方法,声明枚举值是要使用这个构造方法,它表示枚举值中可以存储的数据RED(1, "RED", "红色"),BLUE(2, "BLUE", "蓝色"),BLACK(3, "BLACK", "黑色");public final Integer id;public final String name;public final String desc;// 构造方法ColorEnum(int id, String name, String desc) {this.id = id;this.name = name;this.desc = desc;}// 根据id获取枚举值实例public ColorEnum getEnumById(Integer id) {ColorEnum[] values = values();for (ColorEnum value : values) {if (value.id.equals(id)) {return value;}}return null;}// 返回枚举值的集合public List<ColorEnum> getEnumList() {return Arrays.asList(values());}// 普通方法public void say() {System.out.println("我的颜色是:" + this.desc);}
}
这里需要注意的是,每个枚举值都有自己的id和name,id用于数据库的存储,name用于平时使用,同时,定义了根据id来获取枚举值的方法,这是实际开发中用的最多的。
注解
简介
注解:Annotation,Java1.5引入的功能,它可以被标注在类、方法、字段上,用于指明某种特性,也可以用它来存储配置信息。
注解的使用,有两种场景,
- 一种是在源码中,告诉程序员某个信息,例如@Override注解,告诉程序员当前方法是父类中某个方法的重写,
- 一种是在运行时,通过反射获取注解信息,此时,注解可以用于存储配置信息。
基本使用
java中的元注解
元注解:负责标注其它的注解,用于指明注解的特性,是用户自定义注解是需要用到的
java中的5个元注解:
1、@Retention:指定注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {RetentionPolicy value(); // 指定注解的生命周期
}
这里涉及到的两个枚举类:
a. 定义了注解的生命周期常量的枚举类:
// 定义了注解有哪些生命周期
public enum RetentionPolicy {// 注解只存在于源码中,会被编译器丢掉。注解只存在于源码阶段SOURCE,// 注解会被编译器编译到类文件中,但是运行时不会把注解信息加载到虚拟机中,这是默认的生命周期。// 注解只存在于源码阶段和编译阶段CLASS,// 注解会被加载到虚拟机中,此时可以通过反射获取注解中存储的信息,// 注解存在于源码阶段、编译阶段和运行时阶段RUNTIME
}
b. 定义了注解的使用位置常量的枚举类:
// 枚举类中的常量代表代表一个编译单元中的某个位置
public enum ElementType {// 类、接口或枚举TYPE,// 字段,包括枚举常量FIELD,// 方法声明METHOD,// 参数PARAMETER,// 构造器CONSTRUCTOR,// 局部变量LOCAL_VARIABLE,// 注解,一个可以应用于注解上的注解,类似于元注解。ANNOTATION_TYPE,// 包PACKAGE,// 表示注解可以应用于类型参数声明,案例 public class MyClass<@MyAnnotation T> {}TYPE_PARAMETER,// 表示注解可以应用于类型使用的地方,例如变量声明、方法返回类型、方法参数类型等TYPE_USE
}
2、@Documented:被@Documented注解的注解会被javadoc之类的工具处理,它们的信息会被添加到帮助文档中,默认情况下,注解是不会被添加到帮助文档中的。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
3、@Target:指明了注解可以出现在什么位置。默认情况下,枚举可以被应用到除了泛型、包以外的任何地方
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {// 一个ElementType类型的数组,表明注解可以被应用在什么位置ElementType[] value();
}
4、@Inherited:继承。表示一个注解可以被继承
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
5、@Repeatable:java1.8新增的注解,允许一个类型的注解在同一个程序元素上重复出现。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {// 指定哪个注解可以重复出现Class<? extends Annotation> value();
}
java中自带的注解
1、@Override:只能用于方法,只存在于源码阶段,表明当前方法重写了父类的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
2、@Deprecated:这个注解会被添加到文档中,存在于运行阶段,可以注解类、方法等。一个程序元素被@Deprecated注解,表明开发者不推荐用户使用这个程序元素
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
3、@SuppressWarnings:告诉编译器忽略指定类型的告警,可以标注在类、字段、方法、局部变量等位置。
使用方式:
- 告诉编译器忽略类型转换警告信息:@SuppressWarnings(“unchecked”)
- 告诉编译器忽略被过时告警:@SuppressWarnings(“deprecation”),使用了被@Deprecated标注的元素,编译器会发出过时告警
- 告诉编译器同时忽略多个告警信息:
- 第一种写法:@SuppressWarnings(“unchecked”, “deprecation”)
- 第二种写法:@SuppressWarnings(value={“unchecked”, “deprecation”})
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {// 指定告警类型String[] value();
}
4、@FunctionalInterface:这个注解会被添加到文档中,存在于运行阶段,可以注解接口,表明被它注解的接口是一个函数式接口,也就是接口中只有一个方法的接口。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
5、@SafeVarargs:这个注解会被添加到文档中,存在于运行阶段,可以注解构造器、方法。参数安全类型注解,它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {}
自定义注解
自定义注解的语法:
[public] @interface 注解名 { 参数类型 成员变量名() [default 值];....
}
语法讲解:
- 注解只有成员变量,没有成员方法。
- 声明成员变量的格式:
数据类型 成员变量名() [default 值]
。成员变量可以有默认值,通过default关键字来指定。 - 如果注解中只有一个属性,那么属性名称应该为value,这是一个默认的约定。
注解的使用:把注解放在合适的位置,@注解名(属性=值 [,...])
,
- 如果注解中没有属性,那么括号可以省略不写。
- 如果属性有默认值,那么在使用时就不需要为属性赋值了如果想要覆盖原有的属性也可以赋值。
- 如果属性名是value,value可以省略不写
注解的获取:在运行阶段,需要通过反射,获取注解中的信息,如果想要让注解被反射获取,注解必须要被@Retention(RetentionPolicy.RUNTIME)
注解,它表示注解的生命周期策略是运行时存在。
标记注解:不包含任何成员变量的注解
使用案例
案例1:自定义一个普通注解,指定一个类的初始化方法
第一步:定义注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface InitMethod {// 注解中存储的数据,这里没有什么意义,仅仅演示如何操作注解的属性String value();String name() default "im";
}
第二步:使用注解,注解如果没有指定使用范围,默认可以使用在类上、方法上
public class InitDemo {public InitDemo() { }@InitMethod(value = "1", name = "2")public void test1(int b) {int a = 1;System.out.println("执行test1方法");}
}
第三步:通过反射获取注解
public static void main(String[] args) {try {Class<?> aClass = Class.forName("org.wyj.anno.InitDemo");// 获取类上的注解Annotation[] declaredAnnotations = aClass.getDeclaredAnnotations();for (Annotation declaredAnnotation : declaredAnnotations) {System.out.println("declaredAnnotation = " + declaredAnnotation);}// 获取方法上的指定注解Method[] methods = aClass.getMethods();for (Method method : methods) {if (method.isAnnotationPresent(InitMethod.class)) {InitMethod annotation = method.getAnnotation(InitMethod.class);System.out.println(annotation);System.out.println(annotation.value() + ":" + annotation.name());}}} catch (ClassNotFoundException e) {throw new RuntimeException(e);}
}
总结:这个案例演示了注解的基本使用,实际开发中,这是使用最多的方式
案例2:可重复出现的注解
第一步:定义注解
// 可以重复使用的注解
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Persons.class) // 使用@Repeatable注解把当前注解定义为可重复注解时,要指定它的容器注解
public @interface Person {String role();
}// 注解的容器注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {Person[] value();
}
第二步:注解的使用
@Person(role = "艺术家")
@Person(role = "士兵")
@Person(role = "厨师")
public class Man {
}
第三步:通过反射获取注解
// 测试容器注解
public static void main(String[] args) {try {Class<?> aClass = Class.forName("org.wyj.anno.Man");Person[] annotationsByType = aClass.getAnnotationsByType(Person.class);for (Person person : annotationsByType) {System.out.println(person.role());}} catch (ClassNotFoundException e) {e.printStackTrace();}
}
总结:
- 在使用@Repeatable来注解一个可重复注解的时候,需要提供一个容器注解,容器注解用于装载可重复注解。
- 容器注解:@Repeatable注解需要使用到的工具注解,注解中的属性是一个数组,数组中元素的数据类型是被@Repeatable注解的注解。
案例3:测试注解的继承
第一步:定义注解,一个可以被继承的注解
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritedTest {
}
第二步:注解的使用
// 父类
@InheritedTest
public class InheritedDemo {@InitMethod("aaa")public void init() { }
}// 子类
public class InheritedDemoSon extends InheritedDemo { }
第三步:通过反射获取注解
public static void main(String[] args) {try {Class<?> aClass = Class.forName("org.wyj.anno.InheritedDemoSon");// 类上的注解System.out.println(aClass.isAnnotationPresent(InheritedTest.class)); // true// 方法上的注解for (Method method : aClass.getMethods()) {if (method.getName().equals("init")) {System.out.println("method.isAnnotationPresent(InitMethod.class) = "+ method.isAnnotationPresent(InitMethod.class)); // true}}} catch (ClassNotFoundException e) {throw new RuntimeException(e);}
}
总结:
- 注解的继承:在继承关系中,
- 对于方法上的注解,子类会继承父类的方法,同时也会方法上的注解,无论该注解有没有被@Inherited修饰,
- 对于类上的注解,如果它被@Inherited注解修饰,它才可以被子类继承,但也仅限于子类,不包括子类的子类
查看一个注解反编译后的字节码
案例:以@InitMethod为例,javap -v InitMethod.class
public interface org.wyj.anno.InitMethod extends java.lang.annotation.Annotation
{public abstract java.lang.String value();descriptor: ()Ljava/lang/String;flags: ACC_PUBLIC, ACC_ABSTRACTpublic abstract java.lang.String name();descriptor: ()Ljava/lang/String;flags: ACC_PUBLIC, ACC_ABSTRACT
}
总结:可以看到,注解实际上是一个接口,并且每个注解都默认继承Annotation接口,
注解的公共父接口 Annotation接口
所有的注解默认都会继承Annotation接口,这个动作由编译器来完成
源码:
public interface Annotation {boolean equals(Object obj);int hashCode();String toString();Class<? extends Annotation> annotationType();
}
总结
这里介绍了注解的基本特性,演示了注解的基本使用。如果有补充,欢迎评论。
Q&A
1、枚举类的构造方法,为什么不可以是public?
由于枚举实例是固定的,开发者不能在运行时创建新的枚举实例。因此,构造方法不能是public的,Java编译器自动将枚举的构造方法设为private,以确保枚举实例只能在枚举类内部定义。这是为了确保枚举实例的唯一性和安全性。
2、枚举类可以同时是泛型类吗?
不可以,枚举类不可以是泛型类,但是枚举类中可以定义泛型方法,因为枚举类不可以从外部实例化,所以在枚举类上声明泛型没有意义。