查看原文
其他

枚举与反射浅析

树莓蛋黄派 Java规途 2023-07-04


  • 一、枚举类

  • 二、反射

    • (一)何为反射?

    • (二)Class类

    • (三)Class类的使用

  • 往期内容回顾


枚举类与反射机制

一、枚举类

以下是一个定义枚举类的典型例子

public enum Direction{
    UP,DOWN,RIGHT,LEFT
}

实际上这样的声明定义的是一个「类」,并且拥有4个实例,不能够「构造新的对象」

所在对于枚举类值的比较,「只需要用“==”就可以」,因为没有对象,所以也用不着“equals”方法。

我们可以依据需要为枚举类增加「构造器,字段或者方法」。当然构造器的使用十分有限,只是在构造「枚举常量」的时候调用。

例:

注意

  • 枚举类的构造器总是「私有的」,如果声明一个enum的构造器为public或者protected会报错。
  • 所有的枚举类型都是Enum类的子类,他们继承了这个类的许多方法。
  • 枚举类中最有用的方法就是toString方法,其会返回枚举常量名。
  • toString的逆方法是静态方法valueOf(),如Size s=Enum.valueOf(Size.class,“SMALL”),这个会将s设置为Size.SMALL
  • 每个枚举类都有一个静态的values()方法,返回一个包含「全部枚举值」的数组,如:Size[] values=Size.values()
  • ordinal()方法返回enum声明中枚举常量的位置,从「0」开始计数。

以下的例子将演示如何处理枚举类型:

二、反射

(一)何为反射?

什么是反射?

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。

——百度百科

反射的出现使Java语言充满生机,从某种意义上来说,反射的出现使Java拥有了「动态类型语言」的某些特性。

这里需要了解一下什么是动态类型语言,什么又是静态类型语言。

简单来说:

「动态类型语言就是在运行的时候才会去做数据类型检查的语言」

反之,「静态类型语言就是程序的数据类型是在编译其间确定的或者说运行之前确定的,编写代码的时候要明确确定变量的数据类型」

这样看起来似乎动态类型语言更加灵活多变了。

其实不然,无论动态类型语言还是静态类型语言都有着各自的特点,具体的使用需要依据实际的情况而定,而没有绝对的优与劣。

常见的动态类型语言有:Python、Ruby、Erlang、JavaScript、swift、PHP、Perl等。

常见的静态类型语言有:C、C++、C#、Java、Object-C等

个人认为:「能够分析类能力的程序就可以被称为反射。」

反射是一种「功能强大且复杂」的机制,一般来说,反射机制属于偏语言底层的一种特性。在对某些功能进行封装或者开发工具类的时候,反射是一种功能强大的机制,如果仅仅对于开发一般应用程序来说,反射的作用似乎也不是很明显。

那么反射机制究竟运用在哪些方面呢?

反射机制的作用可以简单概括为四点:

  • 「在运行的时候分析类」
  • 「在运行时检查对象」
  • 「实现泛型数组操作代码」
  • 「可以利用Method对象,这个类似于C++中的函数指针。」

接下来简单介绍一下反射的有关内容

(二)Class类

在Java程序运行期间,Java的运行时系统始终为所有的对象维护一个「运行时类型标识」,这个信息会跟踪每个对象所属的类,虚拟机(JVM)可以利用运行时类型信息选择要执行的正确方法。

在Java中为了保存这个信息,使用了一个特殊的Java类来访问与保存这些信息。这就是Class类

那么如何获取Class类呢?

简单来说一共有4种方式。

1. 通过运行时类对象来获取

其实刚开始学Java的时候调用方法就会注意到有一个getClass()方法,似乎无论是自己自定义的类还是官方提供的类都有这样一个方法。

以Employee类为例子

Class cl=new Employee().getClass();

使用了该类(Employee)的对象调用getClass方法获取Class对象。

同样的我们也可以使用Class对象反过来获取主类的全类名

 Employee employee = new Employee();
 Class<? extends Employee> aClass = employee.getClass();
 String EmployeeName=aClass.getName();
 //获取到了Employee的全类名(包名+类名)

2. 通过运行时类本身的Class属性获取

还是以Employee类为例子,Employee类同样也能利用本身的Class属性获取到Class类。

Class<Employee> employeeClass = Employee.class;
String EmployeeName=employeeClass.getName();

同样我们也能够获取到Class类,并且可以通过Class类对象的方法获取到对应的全类名。

一个Class对象实际上表示的是一个类型,为什么不说它是类呢?因为它有可能表示的是类,也可能表示的不是类,例如int.class,但是int并不是一个类。

3. 通过Class类的静态方法获取

该种获取Class类的方式体现了「反射的动态性。」

简单来说就是直接调用Class类的静态forName()方法直接获取Class类对象。

String className="java.util.Random";
Class<?> aClass = Class.forName(className);

这次我们以Java工具类库中的Random类为例,通过调用Class类的froName方法,我们获取到了对应类的的Class对象,该对象的全称应该是class java.util.Random

同样的从上面的方法调用来说,除了可以直接传入类名,还可以传入一个类名,布尔类型的初始化符号,和类加载器。

需要注意的是Class.forName方法在使用的时候会要求你处理一个检查型异常。一般情况下我们可以进行捕获或者直接向上抛出。

「一般可以选择利用try-catch进行捕获」。因为向上抛出相当于将异常的处理进行了转移,不论转移到何处,最后都会需要我们进行处理。

4. 通过类加载器获取

什么是类加载器?

「Java类加载器」(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。

通过类加载器来获取Class可以大致表示成如下:

 String className="java.util.Random";
classLoader classLoader = this.getClass().getClassLoader();
Class<?> aClass = classLoader.loadClass(className);
System.out.println(aClass);

通过当前对象获取Class,再获取getClassLoader,最后通过ClassLoader加载指定类名的类,获取到了对应的Class对象。

通过上面的介绍,我们可以发现很多时候获取的Class类对象,都是以Class<?>或Class<? extends xxx>类型的,「这表明实际上Class是一个泛型类」,尽管如此,但在大多时候,我们还是会忽略类型参数,使用原始的Class,这是因为,类「型参数的出现使这个原本就已经略显复杂的的抽象概念变得更加复杂。」

5. Class类型的常用方法

常用方法名方法表示的含义
Field[] getFieldsgetFields方法将返回一个包含Field对象的数组,这些对象对应这个类或者超类的公共字段
Field[] getDeclaredFieldsgetDeclaredFields也返回包含Field对象的数组,这些对象对应这个类的全部字段。如果类中没有字段,或Class对象描述的是基本类型或者数组类型,这些方法将返回一个长度为0的数组。
Method[] getMethods返回包含Method对象的数组,该方法将返回所有的公共方法,包括从超类继承而来的公共方法
Method[]  getDeclaredMethods返回这个类或者接口的全部方法,但不包括从超类继承而来的方法。
Constructor[] getConstructors返回包含Constructor对象的数组,其中包含Class对象所表示的类所有的公共构造器
Contructor[] getDeclaredConstructors返回包含Constructor对象的数组,其中包含Class对象所表示的类的全部构造器
String getPackageName得到包含这个类型的包的包名,如果这个类型是一个数组类型,则返回元素类型所属的包,如果这个类型是一个基本类型,则返回“java.lang”

(三)Class类的使用

我们已经知道了在Java反射中可以通过四种基本的方式来获取到对应的Class类型对象。

并且Java为反射机制提供了专属的工具类库,包含在JDK中,下载安装之后可以自由使用。即:自由使用java.lang.reflect包。

该包中有3个类Field、Method和Constructor分别用于描述属性、方法、构造器。

1. 获取某个类的构造器Constructor

通过Class类对象获取某个类的构造器:

这里可以看到有两个选项,一个是getConstructor,另一个是getConstructors

很明显第一个是获取单个的构造器,要求传入参数类型的数组。

第二个要求的是获取该类的所有的构造器,返回的是构造器的数组。

获取单个构造器的方法源码如下:

    @CallerSensitive
    public Constructor<T> getConstructor(Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException 
{
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        return getConstructor0(parameterTypes, Member.PUBLIC);
    }
(1)获取构造器创建对象

还是以Employee类为例子,这里我们使用获取单个构造器的方法:

// 方法一
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee employee = constructor.newInstance();
System.out.println(employee);

// 方法二
Constructor<Employee> declaredConstructor = Employee.class.getDeclaredConstructor();
Employee employee = declaredConstructor.newInstance();

这里需要注意以下几点:

  • 获取到构造器对象之后,可以使用newInstance方法创建一个新的对象employee,这与我们使用new Employee()创建的对象一样。
  • 这里的newInstance方法处依旧需要进行异常处理,一般我们可以选择使用try-catch块进行异常的处理。
  • 这里的两种方法都可以获取到某个类的构造器。但是getConstructor获取到的构造器只能获取公有的构造器。然而getDeclaredConstructor能够获取公有构造器和私有构造器(获取私有构造器需要进行相关的设置)
(2)访问私有构造器

构造器一般都是公有的,但是也会存在一些特殊的情况,必须要把构造器进行私有化。(最典型的就是单例设计模式)

获取私有构造器的方法如下:

//存在一个私有的构造器
private Employee(String name){
        this.name=name;
}
//通过反射获取私有构造器
Constructor<Employee> declaredConstructor = Employee.class.getDeclaredConstructor(String.class);
//设置关闭安全检查
declaredConstructor.setAccessible(true);

Parameter[] parameters = declaredConstructor.getParameters();
for (Parameter parameter : parameters) {
 System.out.println(parameter.getType());
    //最后的输出结果是 :class java.lang.String,即似有构造方法的参数类型
}

这里需要注意以下几点:

  • 存在一个私有构造方法
  • 使用getDeclaredConstructor并传入String.class参数类型获取到对应私有构造方法的对象 declaredConstructor
  • setAccessible作用就是启动和禁用访问安全检查的开关。
  • declaredConstructor.setAccessible(true)设置为true表示抑制Java语言访问检查,设置为false或者不设置(默认是false)的时候表示不抑制Java语言访问检查。
  • 如果使用getDeclaredMethods方法可以获取到一个构造器的数组,该数组内包含所有构造器的名字,这时候可以获取到私有构造器的名字。(并不需要设置Java语言检查访问)
(3)获取所有的构造方法参数类型
  • 方法一:

主要使用getParameters()获取所有的构造方法参数,最后形成一个参数的数组。

Constructor<Employee> declaredConstructor =Employee.class.getDeclaredConstructor(long.class,String.class,double.class);
Parameter[] parameters = declaredConstructor.getParameters();
for (Parameter parameter : parameters) {
    System.out.println(parameter.getType());
    /**结果是
long
class java.lang.String
double
    */
    
}

与前面的类似,可以获取某个构造方法的所有的参数对象并进行相应的操作。

  • 方法二:

主要使用getParameterTypes一次性获取构造方法的所有参数的类型,最后返回一个Class类型的类型数组对象,依次遍历每个Class对象就可以获取每个构造方法参数的类型名。

Constructor<Employee> declaredConstructor = Employee.class.getDeclaredConstructor(long.class,String.class,double.class);
Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
for (Class<?> aClass : parameterTypes) {
    System.out.println(aClass.getName());
/**最后的结果如下:
long
java.lang.String
double

*/
    
}

2. 获取某个类的所有方法

获取某个类的方法主要有两种形式:一种是getMethods获取所有的公共方法,「并包括所有从父类继承的公共方法」。另一种是getDeclaredMethods方法,获取该类或者接口所有的方法,「但是不包括从父类继承的方法」。,当然也存在getMethod获取单个公共方法和getDeclaredMethod获取单个方法(不仅限于公共方法)。

以Employee类为例子

public class Employee {
    private long num;
    private String name;
    private double salary;

    public Employee(long num, String name, double salary) {
        this.num = num;
        this.name = name;
        this.salary = salary;
    }

    public Employee() {
    }

    private Employee(String name){
        this.name=name;
    }

    public long getNum() {
        return num;
    }

    public void setNum(long num) {
        this.num = num;
    }

    public String getName() {
        return name;
    }

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

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {return true;}
        if (!(o instanceof Employee)){ return false;}
        Employee employee = (Employee) o;
        return getNum() == employee.getNum() && Double.compare(employee.getSalary(), getSalary()) == 0 && getName().equals(employee.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getNum(), getName(), getSalary());
    }

    public static void introduceEmployee(Employee employee){
        System.out.println("我的员工编号是:"+employee.getNum()+",我的名字是:"+employee.getName()+",我的薪水是:"+employee.getSalary());

    }

    private void hello(){
        System.out.println("这是一个私有方法");
    }
}

这里存在一个静态方法,一个私有方法,和多个公共方法。

(1)获取所有公共方法

使用getMethods获取所有的公共方法,包括从父类继承的公共方法。

Class<Employee> employeeClass = Employee.class;
 Method[] methods = employeeClass.getMethods();
 for (Method method : methods) {
     System.out.println(method.getName());
/**返回的结果如下:
getName
main
equals
hashCode
setName
getNum
getSalary
setNum
setSalary
introduceEmployee
wait
wait
wait
toString
getClass
notify
notifyAll
*/
          
 }

从输出结果中可以看出:

  • getMethods方法返回了一个包含所有公共方法的Method对象数组。
  • 该方法获取的Method数组不包含私有方法,返回的所有的方法都是公有的。
  • 这里多了很多并不“属于”Employee类的方法,例如notify等,这里是Object类的公共方法。
(2)获取所有的方法

使用getDeclaredMethods方法可以获取到所有的方法,但不包括父类的方法。

Class<Employee> employeeClass = Employee.class;
 Method[] declaredMethods = employeeClass.getDeclaredMethods();
 for (Method declaredMethod : declaredMethods) {
     System.out.println(declaredMethod.getName());
 }
/**
获取到的结果如下:
getNum
setNum
getSalary
setSalary
introduceEmployee
hello
equals
hashCode
getName
setName
*/

  • 由上可知,它并没有获取到直接父类Object的有关的notify方法和wait方法,只获取到本类的所有方法,包括重写的父类方法hashCode以及equals
  • 这里需要注意的是获取到了该类的「私有方法」hello,这是getMethods方法所没有的。
  • 该方法也获取到了一个方法对象的数组。只是与getMethod方法获取到的内容不一样而已。

3. 获取某个类的域

获取某个类的域主要有两种形式,一种是getFields方法获取到所有的公共域,另一种是getDeclaredFields方法可以获取到本类的所有域,也包括私有的域。

(1)获取某个类的所有域

使用getDeclaredFields方法获取所有的域。

Class<Employee> employeeClass = Employee.class;
Field[] declaredFields = employeeClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
    System.out.println(declaredField.getName());
}
/**
获取到的所有域如下:
num
name
salary
sex
*/

  • 以上的程序主要获取到了某个类所有的域。
  • 结果包括所有的域,也包括私有的域。方法getDeclaredFields获取到的是一个域的数组。
(2) 获取某个类的所有公共域

使用getFields方法获取到某个类的所有公共域

Class<Employee> employeeClass = Employee.class;
Field[] fields = employeeClass.getFields();
for (Field field : fields) {
     System.out.println(field.getName());
}
/**
该程序获取到的结果如下:
sex
*/

  • 以上程序主要获取到了某个类的所有的公共域
  • 结果只包括了该类的所有公共域。方法getFields获取到的是一个域的数组,但数组中的元素只有一个。

最后,需要注意的是setAccessible方法无论是构造器、方法、域都可以设置,其主要是设置Java的访问安全检查开关。主要用于单个对象的设置,很多情况下将其设置为true,可以提高效率。

往期内容回顾

你不可不知的语言---JAVA

Java基本程序设计结构——数据类型

变量+运算=?

字符串详解

输入输出与流程

数组与大数

类初识

自定义类与时间类

方法参数与对象构造

包、注释及JAR文件

继承及其子类

Object类及其方法

泛型数组列表及包装类


<<< 左右滑动见更多 >>>


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存