枚舉
簡介
枚舉: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、枚舉類可以同時是泛型類嗎?
不可以,枚舉類不可以是泛型類,但是枚舉類中可以定義泛型方法,因為枚舉類不可以從外部實例化,所以在枚舉類上聲明泛型沒有意義。