码迷,mamicode.com
首页 > 编程语言 > 详细

Java基础 -- 深入理解Java类型信息(Class对象)与反射机制

时间:2019-04-22 21:16:34      阅读:207      评论:0      收藏:0      [点我收藏+]

标签:robot   java语言   exti   rate   编程   str   调用   保存   handle   

一 RTTI概念

认识Claa对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RTTI的说法则是源于《Thinking in Java》一书,其作用是在运行时识别一个对象的类型和类的信息,这里分两种:

  • 传统的”RTTI”:它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好);
  • 反射机制,它允许我们在运行时发现和使用类型的信息。

二 Class对象

在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中,其部分源码如下:

public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static native void registerNatives();
    static {
        registerNatives();
    }

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.(私有构造,只能由JVM创建该类)
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

Class类被创建后的对象就是Class对象,注意,Class对象表示的是自己手动编写类的类型信息,比如创建一个Shapes类,那么,JVM就会创建一个Shapes对应Class类的Class对象,该Class对象保存了Shapes类相关的类型信息。

实际上在Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象),那为什么需要这样一个Class对象呢?是这样的,当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每个class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象,挺拗口,通过下图理解(内存中的简易现象图):
技术图片

到这我们也就可以得出以下几点信息:

  • Class类也是类的一种,与class关键字是不一样的;
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件),比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中;
  • 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象;
  • Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载;
  • Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。

1、Class对象的加载

前面我们已提到过,Class对象是由JVM加载的,那么其加载时机是?

实际上所有的类都是在对其第一次使用时动态加载到JVM中的,当程序第一次调用类的静态成员时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用new操作符创建类的新实例对象也会被当作调用类的静态成员(构造函数也是类的静态方法),由此看来Java程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的Class对象是否已被加载(类的实例对象创建时依据Class对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件(编译后Class对象被保存在同名的.class文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良Java代码(这是java的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于Class对象也就被载入内存了(毕竟.class字节码文件保存的就是Class对象),同时Class对象也就可以被用来创建这个类的所有实例对象

下面通过一个简单例子来说明Class对象被加载的时机问题(例子引用自Thinking in Java):

class Candy {
     static { 
         System.out.println("Loading Candy"); 
      }
}

class Gum {
    static { 
        System.out.println("Loading Gum"); 
    }
}

class Cookie {
    static {   
        System.out.println("Loading Cookie"); 
    }
}

public class SweetShop {
    
    public static void print(Object obj) {
        System.out.println(obj);
    }
    
    public static void main(String[] args) {  
        print("inside main");
        new Candy();
        print("After creating Candy");
        try {
            Class.forName("Gum");
        } catch(ClassNotFoundException e) {
            print("Couldn‘t find Gum");
        }
        print("After Class.forName(\"Gum\")");
        new Cookie();
        print("After creating Cookie");
  }
    
}

在上述代码中,每个类Candy、Gum、Cookie都存在一个static语句(static初始化是在类加载时进行的),这个语句会在类第一次被加载时执行,这个语句的作用就是告诉我们该类在什么时候被加载,执行结果:

inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

从结果来看,new一个Candy对象和Cookie对象,构造函数将被调用,属于静态方法的引用,Candy类的Class对象和Cookie的Class对象肯定会被加载,毕竟Candy实例对象的创建依据其Class对象。比较有意思的是:

Class.forName("Gum");

其中forName方法是Class类(所有Class对象都属于这个类)的一个static成员方法。Class对象和其他对象一样,我们可以获取并操作它的引用(这就是类加载器的工作)。这里通过forName方法,我们可以获取到Gum类对应的Class对象引用。从打印结果来看,调用forName方法将会导致Gum类被加载(前提是Gum类从来没有被加载过)。

2、Class.forName()方法

通过上述的案例,我们知道Class.forName()方法的调用将会返回一个对应类的Class对象,因此如果我们想获取一个类的运行时类型信息并加以使用时,可以调用Class.forName()方法获取该类对应的Class对象的引用,这样做的好处是无需通过持有该类的实例对象引用而去获取Class对象,如下的第2种方式是通过一个实例对象获取一个类的Class对象,其中的getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。

public static void main(String[] args) {

    try{
        //通过Class.forName获取Gum类的Class对象
        Class clazz=Class.forName("Gum");
        System.out.println("forName=clazz:"+clazz.getName());
    }catch (ClassNotFoundException e){
        e.printStackTrace();
    }

    //通过实例对象获取Gum的Class对象
    Gum gum = new Gum();
    Class clazz2 = gum.getClass();
    System.out.println("new=clazz2:"+clazz2.getName());
}

 注意调用forName方法时需要捕获一个名称为ClassNotFoundException的异常,因为forName方法在编译器是无法检测到其传递的字符串对应的类是否存在的,只能在程序运行时进行检查,如果不存在就会抛出ClassNotFoundException异常。

3、Class字面常量

在Java中存在另一种方式来生成Class对象的引用,它就是Class字面常量,如下:

//字面常量的方式获取Class对象
Class clazz = Gum.class;

这种方式相对前面两种方法更加简单,更安全。因为它在编译器就会受到编译器的检查同时由于无需调用forName方法效率也会更高,因为通过字面量的方法获取Class对象的引用不会自动初始化该类。更加有趣的是字面常量的获取Class对象引用方式不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,关于反射技术稍后会分析,由于基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下,一般情况下更倾向使用.class的形式,这样可以保持与普通类的形式统一。

boolean.class = Boolean.TYPE;
char.class = Character.TYPE;
byte.class = Byte.TYPE;
short.class = Short.TYPE;
int.class = Integer.TYPE;
long.class = Long.TYPE;
float.class = Float.TYPE;
double.class = Double.TYPE;
void.class = Void.TYPE;
上面的=表示等价的意思

前面提到过,使用字面常量的方式获取Class对象的引用不会触发类的初始化,这里我们可能需要简单了解一下类加载的过程,如下:

技术图片

  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象;
  • 链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用;
  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。

由此可知,我们获取字面常量的Class引用时,触发的应该是加载阶段,因为在这个阶段Class对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。下面通过小例子来验证这个过程:

import java.util.*;

class Initable {
      //编译期静态常量
      static final int staticFinal = 47;
      //非编期静态常量
      static final int staticFinal2 =
      ClassInitialization.rand.nextInt(1000);
      static {
            System.out.println("Initializing Initable");
      }
}

class Initable2 {
      //静态成员变量
      static int staticNonFinal = 147;
      static {
          System.out.println("Initializing Initable2");
      }
}

class Initable3 {
      //静态成员变量
      static int staticNonFinal = 74;
      static {
          System.out.println("Initializing Initable3");
      }
}

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
        //字面常量获取方式获取Class对象
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        //不触发类初始化
        System.out.println(Initable.staticFinal);
        //会触发类初始化
        System.out.println(Initable.staticFinal2);
        //会触发类初始化
        System.out.println(Initable2.staticNonFinal);
        //forName方法获取Class对象
        Class initable3 = Class.forName("Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}

 执行结果:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

从输出结果来看,可以发现,通过字面常量获取方式获取Initable类的Class对象并没有触发Initable类的初始化,这点也验证了前面的分析,。

同时发现调用Initable.staticFinal变量时也没有触发初始化,这是因为staticFinal属于编译期静态常量,在编译阶段通过常量传播优化的方式将Initable类的常量staticFinal存储到了一个称为NotInitialization类的常量池中,在以后对Initable类常量staticFinal的引用实际都转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在NotInitialization类的常量池获取,这也就是引用编译期静态常量不会触发Initable类初始化的重要原因。

但在之后调用了Initable.staticFinal2变量后就触发了Initable类的初始化,注意staticFinal2虽然被static和final修饰,但其值在编译期并不能确定,因此staticFinal2并不是编译期常量,使用该变量必须先初始化Initable类。

Initable2和Initable3类中都是静态成员变量并非编译期常量,引用都会触发初始化。

至于forName方法获取Class对象,肯定会触发初始化,这点在前面已分析过。到这几种获取Class对象的方式也都分析完,到此这里可以得出小结论:

  • 获取Class对象引用的方式3种,通过继承自Object类的getClass()方法,Class类的静态方法forName()以及字面常量的方式”.class”;
  • 其中实例类的getClass()方法和Class类的静态方法forName()都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化;
  • 初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。

关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有5种场景必须对类进行初始化:

  • 使用new关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段);
  • 使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要;
  • 当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化;
  • 当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类;
  • 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)。

4、理解泛化的Class对象引用

由于Class的引用总是指向某个类的Class对象,利用Class对象可以创建实例类,这也就足以说明Class对象的引用指向的对象确切的类型。在Java SE5引入泛型后,我们可以利用泛型来表示Class对象更具体的类型,即使在运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。如下:

public class ClazzDemo {

    public static void main(String[] args){
        //没有泛型
        Class intClass = int.class;

        //带泛型的Class对象
        Class<Integer> integerClass = int.class;

        integerClass = Integer.class;

        //没有泛型的约束,可以随意赋值
        intClass= double.class;

        //编译期错误,无法编译通过
        //integerClass = double.class
    }
}

从代码可以看出,声明普通的Class对象,在编译器并不会检查Class对象的确切类型是否符合要求,如果存在错误只有在运行时才得以暴露出来。但是通过泛型声明指明类型的Class对象,编译器在编译期将对带泛型的类进行额外的类型检查,确保在编译期就能保证类型的正确性,实际上Integer.class就是一个Class<Integer>类的对象。面对下述语句,确实可能令人困惑,但该语句确实是无法编译通过的。

//编译无法通过
Class<Number> numberClass=Integer.class;

我们或许会想Integer不就是Number的子类吗?然而事实并非这般简单,毕竟Integer的Class对象并非Number的Class对象的子类,前面提到过,所有的Class对象都只来源于Class类,看来事实确实如此。当然我们可以利用通配符“?”来解决问题:

Class<?> intClass = int.class;
intClass = double.class;

这样的语句并没有什么问题,毕竟通配符指明所有类型都适用,那么为什么不直接使用Class还要使用Class<?>呢?这样做的好处是告诉编译器,我们是确实是采用任意类型的泛型,而非忘记使用泛型约束,因此Class<?>总是优于直接使用Class,至少前者在编译器检查时不会产生警告信息。当然我们还可以使用extends关键字告诉编译器接收某个类型的子类,如解决前面Number与Integer的问题:

//编译通过!
Class<? extends Number> clazz = Integer.class;
//赋予其他类型
clazz = double.class;
clazz = Number.class;

上述的代码是行得通的,extends关键字的作用是告诉编译器,只要是Number的子类都可以赋值。这点与前面直接使用Class<Number>是不一样的。实际上,应该时刻记住向Class引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期

下面的示例使用了泛型类语法。它存储了一个类引用,稍后又产生了一个List,填充这个List的对象是使用newInstance()方法,通过该引用生成的:

import java.util.*;

class CountedInteger{
    private static long counter;
    //每次创建一个实例,都会执行一次初始化id=counter++
    private final long id = counter++;
    public String toString(){
        return Long.toString(id);
    }
}

public class FilledList<T>{
    private Class<T> type;
    public FilledList(Class<T> type){
        this.type = type;
    }
    public List<T> create(int nElements){
        List<T> result = new ArrayList<T>();
        try{
            for(int i=0;i<nElements;i++){
                //CountedInteger必须提供默认的构造函数,通过CountedInteger类的Class对象创建一个实例对象
                result.add(type.newInstance());
            }
        }catch(Exception e){
                throw new RuntimeException(e);
        }    
        return result;
    }
    
    public static void main(String[] args){
        FilledList<CountedInteger> f1 = new FilledList<CountedInteger>(CountedInteger.class);
        System.out.println(f1.create(15));
    }
}

输出如下:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

5、关于类型转换的问题

在Java SE5中新增一种使用Class对象进行类型转换的方式,即cast()方式:

class Building{}
class House extends Building{}

public class ClassCasts {
    public static void main(String[] args) {
        Building b = new House();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        h = (House)b;
    }
}

利用Class对象的cast()方法,其参数接收一个参数对象并将其转换为Class引用的类型。当然,如果仔细观察上面的代码,则会发现,与实现了相同功能的main()中最后一行相比,这种转型好像做了很多额外的工作。新的转型语法对于无法使用普通类型的情况显得非常有用,在编写泛型代码时,如果存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。

6、instanceof 关键字与isInstance方法

关于instanceof 关键字,它返回一个boolean类型的值,意在告诉我们对象是不是某个特定的类型实例。如下,在强制转换前利用instanceof检测obj是不是Animal类型的实例对象,如果返回true再进行类型转换,这样可以避免抛出类型转换的异常(ClassCastException)

    public void cast(Object obj){
        if(obj instanceof Animal){
              Animal animal= (Animal) obj;
          }
    }

而isInstance方法则是Class类中的一个Native方法,也是用于判断对象类型的,看个简单例子:

    public void cast2(Object obj){
        //isInstance方法
        if(Animal.class.isInstance(obj)){
            Animal animal= (Animal) obj;
        }
    }

事实上instanceOf 与isInstance方法产生的结果是相同的。对于instanceof是关键字只被用于对象引用变量,检查左边对象是不是右边类或接口的实例化。如果被测对象是null值,则测试结果总是false。一般形式:

//判断这个对象是不是这种类型
obj instanceof className

而isInstance方法则是Class类的Native方法,其中obj是被测试的对象或者变量,如果obj是调用这个方法的class或接口的实例,则返回true。如果被检测的对象是null或者基本类型,那么返回值是false;一般形式如下:

//判断这个对象能不能被转化为这个类
className.class.inInstance(obj)

最后这里给出一个简单实例,验证isInstance方法与instanceof等价性:

class A {}

class B extends A {}

public class Instance {
    
    public static void print(String msg) {
        System.out.println(msg);
    }
    
    public static void test(Object x) {
        print("Testing x of type: " + x.getClass());
        print("x instanceof A: " + (x instanceof A));
        print("x instanceof B: "+ (x instanceof B));
        print("A.isInstance(x): "+ A.class.isInstance(x));
        print("B.isInstance(x): " + B.class.isInstance(x));
        print("x.getClass() == A.class " + (x.getClass() == A.class));
        print("x.getClass() == B.class " + (x.getClass() == B.class));
        print("x.getClass().equals(A.class)) "+ (x.getClass().equals(A.class)));
        print("x.getClass().equals(B.class)) " + (x.getClass().equals(B.class)));
    }
    
    public static void main(String[] args) {
        test(new A());
        test(new B());
    } 
}

输出如下:

Testing x of type: class A
x instanceof A: true
x instanceof B: false
A.isInstance(x): true
B.isInstance(x): false
x.getClass() == A.class true
x.getClass() == B.class false
x.getClass().equals(A.class)) true
x.getClass().equals(B.class)) false
Testing x of type: class B
x instanceof A: true
x instanceof B: true
A.isInstance(x): true
B.isInstance(x): true
x.getClass() == A.class false
x.getClass() == B.class true
x.getClass().equals(A.class)) false
x.getClass().equals(B.class)) true

到此关于Class对象相关的知识点都分析完了,下面将结合Class对象的知识点分析反射技术。

三 反射技术

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。一直以来反射技术都是Java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private),下面将对这几个重要类进行分别说明。

1、Constructor类及其用法

Constructor类存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。获取Constructor对象是通过Class类中的方法获取的,Class类与Constructor相关的主要方法如下:

方法返回值 方法名称 方法说明
static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象
Constructor<T> getConstructor(Class<?>... parameterTypes) 返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[] getDeclaredConstructor() 返回所有声明的(包括private)构造函数对象
T newInstance() 创建此 Class 对象所表示的类的一个新实例。

 

下面看一个简单例子来了解Constructor对象的使用:

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
class User { private int age; private String name; //无参数构造 public User() { super(); } public void setName(String name) { // TODO Auto-generated method stub this.name = name; } public void setAge(int i) { // TODO Auto-generated method stub this.age = i; } //一个参数构造 public User(String name) { super(); this.name = name; } /** * 私有构造 * @param age * @param name */ @SuppressWarnings("unused") private User(int age, String name) { super(); this.age = age; this.name = name; } @Override public String toString() { // TODO Auto-generated method stub return "My name is " + name + ", age is " + age; } } @SuppressWarnings("serial") public class ReflectDemo implements Serializable{ public static void main(String[] args) throws Exception { Class<?> clazz = null; //获取User类对应的Class对象的引用 clazz = Class.forName("User"); //第一种方法,实例化默认构造方法,User必须有无参构造函数,否则将抛异常 User user = (User) clazz.newInstance(); user.setAge(20); user.setName("Rollen"); System.out.println("user:" + user); System.out.println("--------------------------------------------"); //获取带String参数的public构造函数 Constructor cs1 = clazz.getConstructor(String.class); //创建User User user1= (User) cs1.newInstance("Tom"); user1.setAge(22); System.out.println("user1:" + user1); System.out.println("--------------------------------------------"); //取得指定带int和String参数构造函数,该方法是私有构造private Constructor cs2 = clazz.getDeclaredConstructor(int.class,String.class); //由于是private必须设置可访问 cs2.setAccessible(true); //创建user对象 User user2= (User) cs2.newInstance(25,"Like"); System.out.println("user2:"+user2); System.out.println("--------------------------------------------"); //获取所有构造包含private Constructor<?>[] cons = clazz.getDeclaredConstructors(); //查看每个构造方法需要的参数 for (int i = 0; i < cons.length; i++) { //获取构造函数参数类型 Class<?> clazzs[] = cons[i].getParameterTypes(); System.out.println("构造函数["+i+"]:"+cons[i].toString() ); System.out.print("参数类型["+i+"]:("); for (int j = 0; j < clazzs.length; j++) { if (j == clazzs.length - 1) System.out.print(clazzs[j].getName()); else System.out.print(clazzs[j].getName() + ","); } System.out.println(")"); } } }

输出如下:

user:My name is Rollen, age is 20
--------------------------------------------
user1:My name is Tom, age is 22
--------------------------------------------
user2:My name is Like, age is 25
--------------------------------------------
构造函数[0]:private User(int,java.lang.String)
参数类型[0]:(int,java.lang.String)
构造函数[1]:public User(java.lang.String)
参数类型[1]:(java.lang.String)
构造函数[2]:public User()
参数类型[2]:()

 关于Constructor类本身一些常用方法如下(仅部分,其他可查API),

方法返回值 方法名称 方法说明
Class<T> getDeclaringClass() 返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类,其实就是返回真实类型(不包含参数)。
Type[] getGenericParameterTypes() 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型。
String getName() 以字符串形式返回此构造方法的名称。
Class<?>[] getParameterTypes() 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构造方法的形参类型。
T newInstance(Object... initargs) 使用此 Constructor对象表示的构造函数来创建新实例。
String toGenericString() 返回描述此 Constructor 的字符串,其中包括类型参数。

在上面程序追加如下内容:

        System.out.println("--------------------------------------------");
        //Constructor类本身一些常用方法
        System.out.println("------getDeclaredClass----------");
        Class uclazz = cs2.getDeclaringClass();            
        //Constructor对象表示的构造方法的类
        System.out.println("构造方法的类:" + uclazz.getName());
        
        System.out.println("------getGenericParameterTypes--------");
        //按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型
        Type[] tps = cs2.getGenericParameterTypes();
        for(Type tp:tps) {
            System.out.println("参数名称tp:" + tp);
        }
        
        System.out.println("------getParameterTypes--------");
        //按照声明顺序返回一组 Class 对象,返回的就是 Constructor对象构造函数的形参类型
        Class[] clazzs = cs2.getParameterTypes();
        for(Class claz:clazzs) {
            System.out.println("参数名称tp:" + claz.getName());
        } 
        
        System.out.println("------getName-------");
        //以字符串形式返回此构造方法的名称
        System.out.println("getName:" + cs2.getName());
        
        
        System.out.println("------toGenericString-------");
        //返回描述此 Constructor 的字符串,其中包括类型参数。
        System.out.println("toGenericString:" + cs2.toGenericString());

输出如下:

--------------------------------------------
------getDeclaredClass----------
构造方法的类:User
------getGenericParameterTypes--------
参数名称tp:int
参数名称tp:class java.lang.String
------getParameterTypes--------
参数名称tp:int
参数名称tp:java.lang.String
------getName-------
getName:User
------toGenericString-------
toGenericString:private User(int,java.lang.String)

其中关于Type类型这里简单说明一下,Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。getGenericParameterTypes 与 getParameterTypes 都是获取构成函数的参数类型,前者返回的是Type类型,后者返回的是Class类型,由于Type是顶级接口,Class也实现了该接口,因此Class类是Type的子类,Type 表示的全部类型而每个Class对象表示一个具体类型的实例,如String.class仅代表String类型。由此看来Type与 Class 表示类型几乎是相同的,只不过 Type表示的范围比Class要广得多而已。当然Type还有其他子类,如:

  • TypeVariable:表示类型参数,可以有上界,比如:T extends Number;
  • ParameterizedType:表示参数化的类型,有原始类型和具体的类型参数,比如:List<String>;
  • WildcardType:表示通配符类型,比如:?, ? extends Number, ? super Integer;

通过以上的分析,对于Constructor类已有比较清晰的理解,利用好Class类和Constructor类,我们可以在运行时动态创建任意对象,从而突破必须在编译期知道确切类型的障碍。

2、Field类及其用法

Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。同样的道理,我们可以通过Class类的提供的方法来获取代表字段信息的Field对象,Class类与Field对象相关方法如下:

方法返回值 方法名称 方法说明
Field getDeclaredField(String name) 获取指定name名称的(包含private修饰的)字段,不包括继承的字段
Field[] getDeclaredFields() 获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
Field getField(String name) 获取指定name名称、具有public修饰的字段,包含继承字段
Field[] getFields() 获取修饰符为public的字段,包含继承字段

下面的代码演示了上述方法的使用过程:

 

import java.lang.reflect.*;

class Person{
    public int age;
    public String name;
}

class Student extends Person{
    public String desc;
    private int score;
}


public class ReflectField {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException {
        Class<?> clazz = Class.forName("Student");
        
        //获取指定name名称、具有public修饰的字段,包含继承字段
        Field field = clazz.getField("age");
        System.out.println("field:" + field);
        System.out.println("-----------------------");
        
        //获取修饰符为public的字段,包含继承字段
        Field[] fields = clazz.getFields();
        for(Field f:fields) {
            System.out.println("field:" + f);
        }
        System.out.println("-----------------------");
        
        
        //获取指定name名称的(包含private修饰的)字段,不包括继承的字段
        Field field2 = clazz.getDeclaredField("desc");
        System.out.println("field2:" + field2);
        System.out.println("-----------------------");
        
        //获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
        Field[] fields2 = clazz.getDeclaredFields();
        for(Field f:fields2) {
            System.out.println("field:" + f);
        }
        System.out.println("-----------------------");
    }
    
}

输出结果如下:

field:public int Person.age
-----------------------
field:public java.lang.String Student.desc
field:public int Person.age
field:public java.lang.String Person.name
-----------------------
field2:public java.lang.String Student.desc
-----------------------
field:public java.lang.String Student.desc
field:private int Student.score
-----------------------

上述方法需要注意的是,如果我们不期望获取其父类的字段,则需使用Class类的getDeclaredField/getDeclaredFields方法来获取字段即可,倘若需要连带获取到父类的字段,那么请使用Class类的getField/getFields,但是也只能获取到public修饰的的字段,无法获取父类的私有字段。下面将通过Field类本身的方法对指定类属性赋值,代码演示如下:

        Student st = (Student)clazz.newInstance();
        //获取父类public字段并赋值
        Field ageField = clazz.getField("age");
        ageField.set(st, 18);
        Field nameField = clazz.getField("name");
        nameField.set(st, "王杰文");
        
        //只获取当前类的字段,不获取父类的字段
        Field descField = clazz.getDeclaredField("desc");
        descField.set(st, "I‘m a Student!");
        Field scoreField = clazz.getDeclaredField("score");
        //设置为可访问,score是private
        scoreField.setAccessible(true);
        scoreField.set(st, 88);
        
        System.out.println(st);

输出如下:

name:王杰文 age:18 desc:I‘m a Student! score:88

其中的set(Object obj, Object value)方法是Field类本身的方法,用于设置字段的值,相反get(Object obj)则是获取字段的值,当然关于Field类还有其他常用的方法如下:

方法返回值 方法名称 方法说明
void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
Object get(Object obj) 返回指定对象上此 Field 表示的字段的值
Class<?> getType() 返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。
boolean isEnumConstant() 如果此字段表示枚举类型的元素则返回 true;否则返回 false
String toGenericString() 返回一个描述此 Field(包括其一般类型)的字符串
String getName() 返回此 Field 对象表示的字段的名称
Class<?> getDeclaringClass() 返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段
void setAccessible(boolean flag) 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性

上述方法可能是较为常用的,事实上在设置值的方法上,Field类还提供了专门针对基本数据类型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法,这里就不全部列出了,需要时查API文档即可。需要特别注意的是被final关键字修饰的Field字段是安全的,在运行时可以接收任何修改,但最终其实际值是不会发生改变的。

 

 

参考文章

[1] 深入理解Java类型信息(Class对象)与反射机制(转载)

[2]Java编程思想

 

方法返回值方法名称方法说明voidset(Object obj, Object value)将指定对象变量上此 Field 对象表示的字段设置为指定的新值。Objectget(Object obj)返回指定对象上此 Field 表示的字段的值Class<?>getType()返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。booleanisEnumConstant()如果此字段表示枚举类型的元素则返回 true;否则返回 falseStringtoGenericString()返回一个描述此 Field(包括其一般类型)的字符串StringgetName()返回此 Field 对象表示的字段的名称Class<?>getDeclaringClass()返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段voidsetAccessible(boolean flag)将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性--------------------- 作者:zejian_ 来源:CSDN 原文:https://blog.csdn.net/javazejian/article/details/70768369 版权声明:本文为博主原创文章,转载请附上博文链接!

Java基础 -- 深入理解Java类型信息(Class对象)与反射机制

标签:robot   java语言   exti   rate   编程   str   调用   保存   handle   

原文地址:https://www.cnblogs.com/zyly/p/10727511.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!