JavaSE基础——注解(Annotation)和反射-----009

注解(Annotation)和反射

1、什么是注解

1、概念
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。

  • Annotation是从JDK5.0开始引入的技术

  • Annotation的作用

    • 不是程序本身,可以对程序做出解释
    • 可以被其他程序(比如:编译器等)读取
  • 格式:

    • 注解是以”@注释名”在代码中存在的,还可以添加一些参数值例如

      @suppressWarnings(value=”unchecked”).

  • 在哪里使用?

    • 可以附加在package, class,method,filed等上面,相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问。

2、常见注解
@Override:用于标记重写某个方法

@FunctionalInterface:函数式接口

@Deprecated:已废弃注解

@SuppressWarnings:用于镇压警告

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}):用来描述注解的使用范围

@Retention:表示需要在什么级别保存该注解的信息。用于描述注解的生命周期RUNTIME > CLASS > SOURCE)

@Document:说明该注解将被包含在Javadoc中

@Inherited:说明子类可以继承父类中 的该注解

package com.Annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

//测试元注解
public class Test1 {
    
    @MyAnnotation
    public void test(){
        
    }
}

//定义一个注解
@Target(value = ElementType.METHOD)//method 表示注解只能放在方法上
@interface MyAnnotation{
    
}

加上ElementType.TYPE表示可以作用在类上

package com.Annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

//测试元注解
@MyAnnotation
public class Test1 {

    
    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@interface MyAnnotation{

}

@Retention:描述注解的生命周期RUNTIME > CLASS > SOURCE),表示注解在什么地方还有效。分别是运行级别,编译成字节码级别,源码级别

package com.Annotation;

import java.lang.annotation.*;

//测试元注解
@MyAnnotation
public class Test1 {


    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented 
@Inherited
@interface MyAnnotation{

}

注解的参数 :参数类型 +参数名()

package com.Annotation;

import java.lang.annotation.*;

//自定义注解
@MyAnnotation1(name = "小明")
public class Test2 {


    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)

@interface MyAnnotation1{
    //注解的参数:参数类型 + 参数名();
    String name();


}

可以定义默认值:注解的参数 :参数类型 +参数名()default “”; 这里定义默认值为空,上面可以不写name = “小明”

如果没有定义默认值:就必须显式给注解赋值

package com.Annotation;

import java.lang.annotation.*;

//自定义注解
@MyAnnotation1()
public class Test2 {


    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)

@interface MyAnnotation1{
    //注解的参数:参数类型 + 参数名();
    String name() default  "";


}

package com.Annotation;

import java.lang.annotation.*;

//自定义注解
@MyAnnotation1(schools = {"清华大学","北京大学"})
public class Test2 {


    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)

@interface MyAnnotation1{
    //注解的参数:参数类型 + 参数名();
    String name() default  "";
    int age() default 0;
    int id() default -1;//如果默认值为-1,代表不存在 
    String[] schools();


}

如果自定义的注解只有一个参数,并且参数名为value()时,可以在使用时不用写value = xxx

package com.Annotation;

import java.lang.annotation.*;

//测试元注解
@MyAnnotation3("小明")
public class Test3 {


    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@interface MyAnnotation3{
     String value();
}

反射机制(Reflection)

1、静态 VS 动态语言

  • 动态语言
    • 是一在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点就是在运行时代码可以根据某些条件改变自身结构
    • 主要动态语言:Object-C,C#,JavaScript, PHP, python等
  • 静态语言
    • 与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
    • Java可以称之为”准动态语言”。即Java有一定的动态性,可以利用反射机制获得类似于动态语言的特性。Java的动态性让编程的时候更加灵活!

2、Java Reflection

  • Reflection(反射)是 Java 被视为动态语言的关键,反射机制允许在执行期借助于Reflection API取得任何类的内部信息,并能直操作任意对象的内部属性及方法
Class c = Class.forName("java.lang.String")
  • 加载完类以后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像是一面镜子,通过这个镜子看到类的结构,所以我们形象地称之为:反射

正常方式:引入需要的”包类”名称 -> 通过new 实例化 -> 取得实例化对象

反射方式:实例化对象 -> getClass方法 -> 得到完整的 “包类” 名称

package reflection;
//什么叫反射
public class test01 {
    public static void main(String[] args) throws ClassNotFoundException {
        //通过反射获得类的 class 对象
        Class c1 = Class.forName("reflection.User");
        System.out.println(c1);
        Class c2 = Class.forName("reflection.User");
        Class c3 = Class.forName("reflection.User");
        Class c4 = Class.forName("reflection.User");
        //一个类在内存中只用一个 Class 对象
        //一个类被加载后,类的整个结构都会被封装在 class 对象中。
        System.out.println(c2.hashCode());
        System.out.println(c3.hashCode());
        System.out.println(c4.hashCode());
    }
}
//实体类也叫 pojo,或者entity
class User{
    private String name;
    private int age;
    private int id;
    public User(){}
    public User(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void setId(int id) {
        this.id = id;
    }
}

3、获取 class 类的实例

  • 若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高。

    Class clazz = Person.class;
    
  • 已知某个类的实例,调用该实例的getClass()方法获取Class对象

    Class clazz = person.getClass();
    
  • 已知一个类的全类名,且该类在类路径下,可以通过Class类的静态方法forName() 获取,可能抛出 ClassNotFoundException

    Class clazz = Class.forName("demo01.Student");
    
  • 内置基本数据类型可以直接用 类名.Type

  • 还可以利用 ClassLoader

package reflection;
public class test02 {
    public static void main(String[] args) throws ClassNotFoundException {
        Person person = new Student();
        System.out.println("这个是" + person.name);
        //方法一:通过对象获得
        Class c1 = person.getClass();
        System.out.println(c1.hashCode());
        //方法二:forName  获得
        Class c2 = Class.forName("reflection.Student");
        System.out.println(c2.hashCode());
        //方法三:通过类名.class获得
        Class c3 = Student.class;
        System.out.println(c3.hashCode());
        //方法四:基本内置类的包装类都有一个Type属性
        Class c4 = Integer.TYPE;
        System.out.println(c4);
        //获得父类类型
        Class c5 = c1.getSuperclass();
        System.out.println(c5);
    }
}
class Person{
    public String name;
    public int age;
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class Student extends Person{
    public Student() {
        this.name = "student";
    }
}
class Teacher extends Person{
    public Teacher() {
        this.name = "teacher";
    }
}

4、所有类型的 Class 对象

package reflection;
import java.lang.annotation.ElementType;
public class test03 {
    public static void main(String[] args) {
        Class c1 = Object.class;// 类
        Class c2 = Comparable.class; //接口
        Class c3 = String[].class; //一位数组
        Class c4 = int[][].class; //二维数组
        Class c5 = Override.class; //注解
        Class c6 = ElementType.class; //枚举
        Class c7 = Integer.class; //基本数据类型
        Class c8 = int.class;
        Class c9 = void.class;
        Class c10 = Class.class;
        System.out.println(c1 );
        System.out.println(c2 );
        System.out.println(c3 );
        System.out.println(c4 );
        System.out.println(c5 );
        System.out.println(c6 );
        System.out.println(c7 );
        System.out.println(c8 );
        System.out.println(c9 );
        System.out.println(c10);
        //只要元素类型和维度一样,就是同一个Class
        int[] a = new int[10];
        int[] b = new int[100];
        System.out.println(a.getClass().hashCode());
        System.out.println(b.getClass().hashCode());
    }
}

3.类的加载与ClassLoader的理解

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

在这里插入图片描述

  • 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。
  • 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
    • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 初始化:
    • 执行类构造器( )方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一个类的( )方法在多线程环境中被正确加锁和同步。
package reflection;
public class test04 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(A.m);
        /**
         * 1、加载到内存,会产生一个类对应的class对象
         * 2、链接,连接结束后 m=0
         * 3、初始化
         *      <clinit>(){
         *              system.out.println("A类静态代码块初始化");
         *              m=300;
         *              m=100
         *      }
         *      m=100
         */
    }
}
class A{
    static int m = 100;
    static {
        System.out.println("A类静态代码块初始化");
        m = 300;
    }
    public A(){
        System.out.println("构造函数初始化");
    } 
}

什么时候会发生类的初始化

  • 类的主动引用(一定会发生类的初始化)
    • 当虚拟机启动,先初始化main方法所在的类
    • new 一个类的对象
    • 调用类的静态成员(除了final常量)和静态方法
    • 使用java.lang.reflect包的方法对类进行反射调用
    • 当初始化一个类,如果其父类没有被初始化,则先初始化他的父类
  • 类的被动引用(不会发生类的初始化)
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在连接阶段就存入调用类的常量池中了)
package reflection;
public class test06 {
    static {
        System.out.println("main 类被加载");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        //1、主动引用
//        Son son = new Son();
        //2、通过反射引用
//        Class c1 = Class.forName("reflection.Son");
        //3、调用类的静态成员和静态方法
//        System.out.println(Son.m);
        /**
         * 被动引用
         */
        //当通过子类引用父类的静态变量,不会导致子类初始化
        //System.out.println(Son.b);
        //通过数组定义类引用,不会触发此类的初始化
        //Son[] sons = new Son[100];
        //引用常量不会触发此类的初始化(常量在连接阶段就存入调用类的常量池中了)
        System.out.println(Son.M);
    }
}
class Father{
    static int b = 2;
    static {
        System.out.println("父类被加载");
    }
}
class Son extends Father{
    static {
        System.out.println("子类被加载");
        m = 300;
    }
    static  int m = 100;
    public final static int M = 1;
}

3.3 ClassLoader的理解

在这里插入图片描述

  • 类加载器的作用:
    • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
    • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
  • 类加载器作用是用来把类(class)装载进内存的。JVM 规范定义了如下类型的类的加载器。

在这里插入图片描述

/**
 * 了解类的加载器
 */
public class ClassLoaderTest {
    @Test
    public void test1(){
        //对于自定义类,使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);
        
        ClassLoader classLoader1 = classLoader.getParent();//调用系统类加载器的getParent():获取扩展类加载器
        System.out.println(classLoader1);
        
        //调用扩展类加载器的getParent():无法获取引导类加载器
        //引导类加载器(根加载器)主要负责加载java的核心类库,无法加载自定义类的。
        ClassLoader classLoader2 = classLoader1.getParent();
        System.out.println(classLoader2); //null
        //String类属于java的核心类库,而核心类库是属于引导类加载器的,所以无法获取
        ClassLoader classLoader3 = String.class.getClassLoader();
        System.out.println(classLoader3); //null
    }
}

3.4 使用ClassLoader加载配置文件

用途

在这里插入图片描述

/**
 * 了解类的加载器
 */
public class ClassLoaderTest {
    /*
    Properties:用来读取配置文件。
     */
    @Test
    public void test2() throws Exception {
        Properties pros =  new Properties();
        //此时的文件默认在当前的module下。
        //读取配置文件的方式一:
//        FileInputStream fis = new FileInputStream("jdbc.properties");
//        FileInputStream fis = new FileInputStream("src/jdbc.properties");
//        pros.load(fis);
        //读取配置文件的方式二:使用ClassLoader
        //配置文件默认识别为:当前module的src下
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("jdbc.properties");
        pros.load(is);
        String user = pros.getProperty("user");
        String password = pros.getProperty("password");
        System.out.println("user = " + user + ",password = " + password);
    }
}

3.5 通过反射,创建运行时类的对象

/**
 * 通过反射创建对应的运行时类的对象
 */
public class NewInstanceTest {
    @Test
    public void test1() throws Exception {
        Class<Person> clazz = Person.class;
        /*
        newInstance():调用此方法,创建对应的运行时类的对象。
                        内部调用了运行时类的空参的构造器。
        要想此方法正常的创建运行时类的对象,要求:
        1.运行时类必须提供空参的构造器
        2.空参的构造器的访问权限得够。通常,设置为public。
        在javabean中要求提供一个public的空参构造器。原因:
        1.便于通过反射,创建运行时类的对象
        2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
         */
        Person person = clazz.newInstance();
        System.out.println(person);
    }
}

4.创建运行时类的对象

有了Class对象,能做什么?

创建类的对象:调用Class对象的newInstance()方法

  • 类必须有一个无参数的构造器。
  • 类的构造器的访问权限需要足够。

难道没有无参的构造器就不能创建对象了吗?

  • 不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。 步骤如下:
    • 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类 型的构造器
    • 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
    • 通过Constructor实例化对象。
package com.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test05 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //1.根据全类名获取对应的Class对象
        Class c1 = Class.forName("com.reflection.Person");
        //2.调用指定参数结构的构造器,生成Constructor的实例

        Constructor con = c1.getConstructor(String.class);
        //3.通过Constructor的实例创建对应类的对象,并初始化类属性
        Person p2 = (Person) con.newInstance("Peter");
        System.out.println(p2.name);
    }

}

5.获取运行时类的完整结构

5.1获取字段

对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。

我们先看看如何通过Class实例获取字段信息。Class类提供了以下几个方法来获取字段:

  • Field getField(name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
package com.reflection;

public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student1.class;
        // 获取public字段"score":
        System.out.println(stdClass.getField("score"));
        // 获取继承的public字段"name":
        System.out.println(stdClass.getField("name"));
        // 获取private字段"grade":
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student1 extends Person1 {
    public int score;
    private int grade;
}

class Person1 {
    public String name;
}


---------------
    public int com.reflection.Student1.score
    public java.lang.String com.reflection.Person1.name
    private int com.reflection.Student1.grade


一个Field对象包含了一个字段的所有信息:

  • getName():返回字段名称,例如,"name"
  • getType():返回字段类型,也是一个Class实例,例如,String.class
  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

String类的value字段为例,它的定义是:

public final class String {
    private final byte[] value;
}

我们用反射获取该字段的信息,代码如下:

Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B      表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false

获取字段值

利用反射拿到字段的一个Field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。

例如,对于一个Person2实例,我们可以先拿到name字段对应的Field,再获取这个实例的name字段的值:

package com.reflection;

import java.lang.reflect.Field;

public class Main01 {
    public static void main(String[] args) throws Exception {
            Person2 p = new Person2("Xiao Ming");
            Class c = p.getClass();
            Field name = c.getDeclaredField("name");//name 用private修饰,用getDeclaredField来获取

            String value = name.getName();//使用getName获取
            System.out.println(value); // "Xiao Ming"
        }
    }



    class Person2 {
        private String name;

        public Person2(String name) {
            this.name = name;
        }
    }


或者加上 f.setAccessible(true);调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。

public class Main {

    public static void main(String[] args) throws Exception {
        Object p = new Person("Xiao Ming");
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
      f.setAccessible(true);
        Object value = f.get(p);
        System.out.println(value); // "Xiao Ming"
    }
}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }
}

如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

答案是正常情况下,我们总是通过p.name来访问Personname字段,编译器会根据publicprotectedprivate决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。

此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。

设置字段值

通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。示例代码如下:

public class Main {

    public static void main(String[] args) throws Exception {
        Person p = new Person("Xiao Ming");
        System.out.println(p.getName()); // "Xiao Ming"
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
        f.setAccessible(true);
        f.set(p, "Xiao Hong");
        System.out.println(p.getName()); // "Xiao Hong"
    }
}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}


----------------------
    Xiao Ming
    Xiao Hong
    

运行上述代码,打印的name字段从Xiao Ming变成了Xiao Hong,说明通过反射可以直接修改字段的值。

同样的,修改非public字段,需要首先调用setAccessible(true)

小结

Java的反射API提供的Field类封装了字段的所有信息:

通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()

通过Field实例可以获取字段信息:getName()getType()getModifiers()

通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。

通过反射读写字段是一种非常规方法,它会破坏对象的封装。

5.2调用方法

我们已经能通过Class实例获取所有Field对象,同样的,可以通过Class实例获取所有Method信息。Class类提供了以下几个方法来获取Method

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取public方法getScore,参数为String:
        System.out.println(stdClass.getMethod("getScore", String.class));
        // 获取继承的public方法getName,无参数:
        System.out.println(stdClass.getMethod("getName"));
        // 获取private方法getGrade,参数为int:
        System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
    }
}

class Student extends Person {
    public int getScore(String type) {
        return 99;
    }
    private int getGrade(int year) {
        return 1;
    }
}

class Person {
    public String getName() {
        return "Person";
    }
}

---------------
    public int Student.getScore(java.lang.String)
    public java.lang.String Person.getName()
    private int Student.getGrade(int)

上述代码首先获取StudentClass实例,然后,分别获取public方法、继承的public方法以及private方法,打印出的Method类似:

public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)

一个Method对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。

调用方法

当我们获取到一个Method对象时,就可以对它进行调用。我们以下面的代码为例:

String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射来调用substring方法,需要以下代码:

public class Main {
    public static void main(String[] args) throws Exception {
        // String对象:
        String s = "Hello world";
        // 获取String substring(int)方法,参数为int:
        Method m = String.class.getMethod("substring", int.class);
        // 在s对象上调用该方法并获取结果:
        String r = (String) m.invoke(s, 6);
        // 打印调用结果:
        System.out.println(r);
    }
}
------------
    world

注意到substring()有两个重载方法,我们获取的是String substring(int)这个方法。思考一下如何获取String substring(int, int)方法。

Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。

调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)为例:

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取Integer.parseInt(String)方法,参数为String:
        Method m = Integer.class.getMethod("parseInt", String.class);
        // 调用该静态方法并获取结果:
        Integer n = (Integer) m.invoke(null, "12345");
        // 打印调用结果:
        System.out.println(n);
    }
}

-------------------
    12345

调用非public方法

和Field类似,对于非public方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name);
    }
}

class Person {
    String name;
    private void setName(String name) {
        this.name = name;
    }
}

----------
    Bob

此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。

多态

我们来考察这样一种情况:一个Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时,调用的方法到底是哪个?

// reflection
import java.lang.reflect.Method;
public class Main {
    public static void main(String[] args) throws Exception {
        // 获取Person的hello方法:
        Method h = Person.class.getMethod("hello");
        // 对Student实例调用hello方法:
        h.invoke(new Student());
    }
}

class Person {
    public void hello() {
        System.out.println("Person:hello");
    }
}

class Student extends Person {
    public void hello() {
        System.out.println("Student:hello");
    }
}


---------------
   Student:hello

运行上述代码,发现打印出的是Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:

Method m = Person.class.getMethod("hello");
m.invoke(new Student());

实际上相当于:

Person p = new Student();
p.hello();

小结

Java的反射API提供的Method对象封装了方法的所有信息:

通过Class实例的方法可以获取Method实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()

通过Method实例可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()

通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)

通过设置setAccessible(true)来访问非public方法;

通过反射调用方法时,仍然遵循多态原则。

5.3调用构造方法

我们通常使用new操作符创建新的实例:

Person p = new Person();

如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();

调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。

为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:

import java.lang.reflect.Constructor;
public class Main {
    public static void main(String[] args) throws Exception {
        // 获取构造方法Integer(int):
        Constructor cons1 = Integer.class.getConstructor(int.class);
        // 调用构造方法:
        Integer n1 = (Integer) cons1.newInstance(123);
        System.out.println(n1);

        // 获取构造方法Integer(String)
        Constructor cons2 = Integer.class.getConstructor(String.class);
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}


---------------
    123
    456

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

小结

Constructor对象封装了构造方法的所有信息;

通过Class实例的方法可以获取Constructor实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()

通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。

5.4获取继承关系

当我们获取到某个Class对象时,实际上就获取到了一个类的类型:

Class cls = String.class; // 获取到String的Class

还可以用实例的getClass()方法获取:

String s = "";
Class cls = s.getClass(); // s是String,因此获取到String的Class

最后一种获取Class的方法是通过Class.forName(""),传入Class的完整类名获取:

Class s = Class.forName("java.lang.String");

这三种方式获取的Class实例都是同一个实例,因为JVM对每个加载的Class只创建一个Class实例来表示它的类型。

获取父类的Class

有了Class实例,我们还可以获取它的父类的Class

public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}

------------------
    class java.lang.Number
    class java.lang.Object
    null

运行上述代码,可以看到,Integer的父类类型是NumberNumber的父类是ObjectObject的父类是null。除Object外,其他任何非interfaceClass都必定存在一个父类类型。

获取interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

// reflection import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class;
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}
-----------------
    interface java.lang.Comparable
    interface java.lang.constant.Constable
    interface java.lang.constant.ConstantDesc

运行上述代码可知,Integer实现的接口有:

  • java.lang.Comparable
  • java.lang.constant.Constable
  • java.lang.constant.ConstantDesc

要特别注意:getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class.getSuperclass();
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}

-------------
    interface java.io.Serializable

Integer的父类是NumberNumber实现的接口是java.io.Serializable

此外,对所有interfaceClass调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()

System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream

System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()

如果一个类没有实现任何interface,那么getInterfaces()返回空数组。

继承关系

当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()

// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

小结

通过Class对象可以获取继承关系:

  • Class getSuperclass():获取父类类型;
  • Class[] getInterfaces():获取当前类实现的所有接口。

通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值