枚举、注解、反射与内省
作者:互联网
枚举
一、枚举类型
JDK1.5引入了新的类型——枚举。 在JDK1.5 之前,我们定义常量都是: public static fianl… 。很难管理。 枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
用于定义有限数量的一组同类常量,例如: 错误级别: 低、中、高、急 一年的四季: 春、夏、秋、冬 商品的类型: 美妆、手机、电脑、男装、女装… 在枚举类型中定义的常量是该枚举类型的实例。
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
定义格式
权限修饰符 enum 枚举名称 { 实例1,实例2,实例3,实例4; }
二、枚举的定义语法在没有枚举类型时定义常量常见的方式
public class DayDemo {
public static final int MONDAY =1;
public static final int TUESDAY=2;
public static final int WEDNESDAY=3;
public static final int THURSDAY=4;
public static final int FRIDAY=5;
public static final int SATURDAY=6;
public static final int SUNDAY=7;
}
上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,容易混淆,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,定义周一到周日的常量
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。
枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性和便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。
以上是写法,写好后该如何使用呢?如下:
public class EnumDemo {
public static void main(String[] args){
//直接引用
Day day =Day.MONDAY;
}
}
就像上述代码那样,直接引用枚举的值即可,这便是枚举类型的最简单模型。
三、为什么可以这样用,看看枚举实现原理就知道了
我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。我们可以看看反编译的结果!
结论:从反编译的代码可以看出编译器确实帮助我们生成了一个Day类而且该类继承自java.lang.Enum类,该类是一个抽象类,除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期。还为我们生成了两个静态方法,分别是values()和 valueOf(),到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好常量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;
四、编译器生成的Values方法与ValueOf方法
values()方法和valueOf(String name)方法是编译器生成的static方法,后面我们自己定义的枚举类的父类Enum的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:
Day[] days2 = Day.values();
System.out.println("day2:"+Arrays.toString(days2));
Day day = Day.valueOf("MONDAY");
System.out.println("day:"+day);
输出结果:
day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
day:MONDAY
从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。
五、Enum抽象类常见方法
Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。 每个枚举对象,都可以实现自己的抽象方法 public interface LShow{ void show(); }public enum Level implements LShow{ LOW(30){ @Override public void show(){ //… } }, MEDIUM(15){ @Override public void show(){ //… } },HIGH(7){ @Override public void show(){ //… } },URGENT(1){ @Override public void show(){ //… } };private int levelValue;
注意事项 一旦定义了枚举,最好不要妄图修改里面的值,除非修改是必要的。
枚举类默认继承的是java.lang.Enum类而不是Object类 枚举类不能有子类,因为其枚举类默认被final修饰
只能有private构造方法 switch中使用枚举时,直接使用常量名,不用携带类名
不能定义name属性,因为自带name属性
不要为枚举类中的属性提供set方法,不符合枚举最初设计初衷。
六、枚举的进阶用法
向enum类添加方法与自定义属性和构造函数 重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下:
public enum Day2 {
MONDAY("星期一",1),
TUESDAY("星期二",2),
WEDNESDAY("星期三",3),
THURSDAY("星期四",4),
FRIDAY("星期五",5),
SATURDAY("星期六",6),
SUNDAY("星期日",7);//记住要用分号结束
private String desc;//文字描述
private Integer code; //对应的代码
/**
* 私有构造,防止被外部调用
* @param desc
*/
private Day2(String desc,Integer code){
this.desc=desc;
this.code=code;
}
/**
* 定义方法,返回描述,跟常规类的定义没区别
* @return
*/
public String getDesc(){
return desc;
}
/**
* 定义方法,返回代码,跟常规类的定义没区别
* @return
*/
public int getCode(){
return code;
}
public static void main(String[] args){
for (Day2 day:Day2.values()) {
System.out.println("name:"+day.name()+
",desc:"+day.getDesc());
}
}
输出结果:
name:MONDAY,desc:星期一
name:TUESDAY,desc:星期二
name:WEDNESDAY,desc:星期三
name:THURSDAY,desc:星期四
name:FRIDAY,desc:星期五
name:SATURDAY,desc:星期六
name:SUNDAY,desc:星期日
注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注。和注释不同,Java 标注可以通过反射获取标
注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行
时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
主要用于:
- 编译格式检查
- 反射中解析
- 生成帮助文档
- 跟踪代码依赖
- 等
学习的重点
理解 Annotation 的关键,是理解 Annotation 的语法和用法.
学习步骤:
1.概念
2. 怎么使用内置注解
3. 怎么自定义注解
4. 反射中怎么获取注解内容
为什么要引入注解?
使用【注解】之前(甚至在使用之后),【XML】被广泛的应用于描述元数据,得到各大框架的青睐,它以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越庞大,【XML】的内容也越来越复杂,一些应用开发人员和架构师发现维护成本变高。
他们希望使用一些和代码紧耦合的东西,于是就有人提出来一种标记式高耦合的配置方式【注解】。
方法上可以进行注解,类上也可以注解,字段属性上也可以注解,反正几乎需要配置的地方都可以进行注解。
下面我们通过一个例子来理解这两者的区别。
假如你想为应用设置很多的常量或参数,这种情况下,【XML】是一个很好的选择,因为它不会同特定的代码耦合。如果你想把某个方法声明为服务,那么使用【注解】会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须认识到这点。
同时,【注解】定义了一种标准的描述元数据的方式。
关于【注解】和【XML】两种不同的配置模式,争论了好多年,各有各的优劣,注解可以提供更大的便捷性,易于维护修改,但耦合度高,而 【XML】 相对于注解则是相反的。追求低耦合就要抛弃高效率,追求效率必然会遇到耦合。目前,许多框架将【XML】和【注解】两种方式结合使用,平衡两者之间的利弊。
本文不再辨析两者谁优谁劣,而在于以最简单的语言介绍注解相关的基本知识。
什么是注解
注解也叫元数据,即一种描述数据的数据。例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解
Annotation接口中有下面这句话来描述注解:Annotation 是所有注解继承的公共接口
The common interface extended by all annotation types.
注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你可以去反编译任意一个注解类得到结果。
一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。
而解析一个类或者方法的注解往往有两种形式,一种是编译期直接的扫描,一种是运行期反射。反射的事情我们先不讨论,而编译器的扫描指的是编译器在对 Java 代码编译成字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。
@Override
public String toString() {
return "Hello Annotation";
}
上面的代码中,我重写了toString()方法并使用了@Override注解。但是,即使我不使用@Override注解标记代码,程序也能够正常执行。那么,该注解表示什么?这么写有什么好处吗?事实上,@Override告诉编译器这个方法是一个重写方法(描述方法的元数据),如果父类中不存在该方法,编译器便会报错,提示该方法没有重写父类中的方法。如果我不小心拼写错误,例如将toString()写成了toStrring(),而且我也没有使用@Override注解,那程序依然能编译运行。但运行结果会和我期望的大不相同。现在我们了解了什么是注解,并且使用注解有助于提高代码的可读性。
注解的用途
生成文档,通过代码里标识的元数据生成javadoc文档。 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例
内置注解
@Override : 重写 *
- 定义在java.lang.Override
@Deprecated:废弃 *
- 概念:定义在java.lang.Deprecated
@SafeVarargs
- Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface: 函数式接口 *
- Java 8 开始支持,标识一个匿名函数或函数式接口。
@Repeatable:标识某注解可以在同一个声明上使用多次
- Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
@SuppressWarnings:抑制编译时的警告信息。 *
- 定义在java.lang.SuppressWarnings
- 三种使用方式
关键字 用途
**
元注解
**
要想真正掌握怎么使用注解,还需要先学习一下元注解。
元注解是用于修饰注解的注解
元注解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。如Java内置注解,@Override、@Deprecated、@SuppressWarnning等
RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。如SpringMvc中的@Controller、@Autowired、@RequestMapping等。
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解 ElementType.CONSTRUCTOR
可以给构造方法进行注解 ElementType.FIELD 可以给属性进行注解 ElementType.LOCAL_VARIABLE
可以给局部变量进行注解 ElementType.METHOD 可以给方法进行注解 ElementType.PACKAGE
可以给一个包进行注解 ElementType.PARAMETER 可以给一个方法内的参数进行注解 ElementType.TYPE
可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类使用了@Inherited 注解,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
说的比较抽象。代码来解释。
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
Repeatable使用场景:在需要对同一种注解多次使用时,往往需要借助@Repeatable。
下面举例说明一下,在生活中一个人往往是具有多种身份,如果我把每种身份当成一种注解该如何使用
先声明一个Persons类用来包含所有的身份
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
Person[] value();
}
这里@Target是声明Persons注解的作用范围,参数ElementType.Type代表可以给一个类进行注解
@Retention是注解的有效时间,RetentionPolicy.RUNTIME是指程序运行的时候。
Person注解
@Repeatable(Persons.class)
public @interface Person{
String role() default "";
}
@Repeatable括号内的就相当于用来保存该注解内容的容器。
声明一个Man类,给该类加上一些身份。
@Person(role="CEO")
@Person(role="husband")
@Person(role="father")
@Person(role="son")
public class Man {
String name="";
}
在主方法中访问该注解。
public static void main(String[] args) {
Annotation[] annotations = Man.class.getAnnotations();
System.out.println(annotations.length);
Persons p1=(Persons) annotations[0];
for(Person t:p1.value()){
System.out.println(t.role());
}
}
运行结果
CEO
husband
father
son
总结:
注解架构图
- Annotation与RetentionPolicy 与ElementType 。 每 1 个 Annotation 对象,都会有唯一的 RetentionPolicy 属性;至于 ElementType 属性,则有 1~n
个。
(02) ElementType(注解的用途类型) 1. 子类会继承父类使用的注解中被@Inherited修饰的注解 2. 接口继承关系中,子接口不会继承父接口中的任何注解,不管父接口中使用的注解有没有 被@Inherited修饰 3. 类实现接口时不会继承任何接口中定义的注解
“每 1 个 Annotation” 都与 “1~n 个 ElementType” 关联。当 Annotation 与某个 ElementType 关联
时,就意味着:Annotation有了某种用途。例如,若一个 Annotation 对象是 METHOD 类型,则该
Annotation 只能用来修饰方法。
(03) RetentionPolicy(注解作用域策略)。
“每 1 个 Annotation” 都与 “1 个 RetentionPolicy” 关联。
定义格式
@interface 自定义注解名{}
注意事项
- 定义的注解,自动继承了java.lang,annotation.Annotation接口
- 注解中的每一个方法,实际是声明的注解配置参数
- 可以通过default来声明参数的默认值
- 如果只有一个参数成员,一般参数名为value
- 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。
案例
上面的作用是定义一个 Annotation,我们可以在代码中通过 “@MyAnnotation1” 来使用它。
@Documented, @Target, @Retention, @interface 都是来修饰 MyAnnotation1 的。含义:
(01) @interface
使用 @interface 定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是
一个Annotation。 定义 Annotation 时,@interface 是必须的。 注意:它和我们通常的 implemented
实现接口的方法不同。Annotation 接口的实现细节都由编译器完 成。通过 @interface
定义注解后,该注解不能继承其他的注解或接口。
(02) @Documented
类和方法的 Annotation 在缺省情况下是不出现在 javadoc 中的。如果使用 @Documented 修饰该
Annotation,则表示它可以出现在 javadoc 中。 定义 Annotation 时,@Documented
可有可无;若没有定义,则 Annotation 不会出现在 javadoc 中。
(03) @Target(ElementType.TYPE)
前面我们说过,ElementType 是 Annotation 的类型属性。而 @Target 的作用,就是来指定 Annotation
的类型属性。 @Target(ElementType.TYPE) 的意思就是指定该 Annotation 的类型是
ElementType.TYPE。这就意味 着,MyAnnotation1 是来修饰"类、接口(包括注释类型)或枚举声明"的注解。 定义
Annotation 时,@Target 可有可无。若有 @Target,则该 Annotation 只能用于它所指定的地 方;若没有
@Target,则该 Annotation 可以用于任何地方。
(04) @Retention(RetentionPolicy.RUNTIME)
前面我们说过,RetentionPolicy 是 Annotation 的策略属性,而 @Retention 的作用,就是指定
Annotation 的策略属性。 @Retention(RetentionPolicy.RUNTIME) 的意思就是指定该
Annotation 的策略是 RetentionPolicy.RUNTIME。这就意味着,编译器会将该 Annotation 信息保留在
.class 文件中,并且 能被虚拟机读取。 定义 Annotation 时,@Retention 可有可无。若没有
@Retention,则默认是 RetentionPolicy.CLASS。
反射
概述
JAVA反射机制是在运行状态中,获取任意一个类的结构 , 创建对象 , 得到方法,执行方法 , 属性 !;
这种在运行状态动态获取信息以及动态调用对象方法的功能被称为java语言的反射机制。
类加载器
类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与
文件系统。学习类加载器时,掌握Java的委派概念很重要。
双亲委派模型:如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求
转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的
启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)
时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。
源码
加载配置文件
通过类加载器加载资源文件
默认加载的是src路径下的文件,但是当项目存在resource root目录时,就变为了加载
resource root下的文件了。
所有类型的Class对象
要想了解一个类,必须先要获取到该类的字节码文件对象. 在Java中,每一个字节码文件,被夹在到内存后,都存在一个对应的Class类型的对象
得到Class的几种方式
**
获取Constructor
**
通过class对象 获取一个类的构造方法
Constructor 创建对象
获取Method
通过class对象 获取一个类的方法
Method 执行方法
获取Field
通过class对象 获取一个类的属性
Field 属性的对象类型
常用方法:
- get(Object o ); 参数: 要获取属性的对象 获取指定对象的此属性值
- set(Object o , Object value);
参数1. 要设置属性值的 对象
参数2. 要设置的值 设置指定对象的属性的值
- getName() 获取属性的名称
- setAccessible(boolean flag) 如果flag为true 则表示忽略访问权限检查 !(可以访问任何权限的属性)
获取类/属性/方法的全部注解对象
根据类型获取类/属性/方法的注解对象
注解类型 对象名 = (注解类型) c.getAnnotation(注解类型.class);
内省
基于反射 , java所提供的一套应用到JavaBean的API 一个定义在包中的类 , 拥有无参构造器 所有属性私有,
所有属性提供get/set方法 实现了序列化接口 这种类, 我们称其为 bean类 . Java提供了一套java.beans包的api ,
对于反射的操作, 进行了封装 !
Introspector
BeanInfo
MethodDescriptor
标签:定义,内省,Annotation,枚举,注解,方法,public 来源: https://blog.csdn.net/alwayschw/article/details/118456229