欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java基础教程(26)--反射

程序员文章站 2022-10-08 14:18:38
反射是指在运行时动态获取某个类型或对象的类型信息,并通过这些信息执行创建对象、调用方法、修改属性等操作的行为。反射机制是构建框架技术的基础所在,在阅读各种框架的源码时,我们会发现大量的反射代码。与反射有关的类和接口均位于java.lang.reflect包中。本文将会对Java中反射的内容进行全面的... ......

一.类

  对于每一种类型的对象,java虚拟机都会实例化一个java.lang.class类的不可变实例。该实例提供了获取对象的运行时属性的方法,包括它的成员和类型信息。class类还提供了创建新实例的方法。最重要的是,它是所有反射api的入口。下面介绍最常用的涉及到类的反射操作。

1.获取class对象

对象.getclass()

  如果要从一个对象获取它的对应的class对象,最简单的方法就是调用getclass()方法,这个方法继承自object类,因此每个对象都可以调用这个方法:

import java.util.hashset;
import java.util.set;
import static reflect.reflectdemo.e.a;

public class reflectdemo {
    static enum e {a, b}
    public static void main(string[] args) {
        class c1 = "foo".getclass();
        class c2 = a.getclass();
        byte[] bytes = new byte[1024];
        class c3 = bytes.getclass();
        set<string> s = new hashset<>();
        class c4 = s.getclass();
    }
}

类名.class

  如果类型没有可用实例,可以通过“类名.class”来获取class对象:

import java.util.set;
import static reflect.reflectdemo.e.a;

public class reflectdemo {
    static enum e {a, b}
    public static void main(string[] args) {
        class c1 = string.class;
        class c2 = e.class;
        class c3 = byte[].class;
        class c4 = set.class;
    }
}

  需要注意的是,当对基本数据类型的变量调用getclass()方法时,会产生编译错误,但是可以对基本数据类型使用“.class”,例如:

boolean b = true;
class c1 = b.getclass();             //编译错误

class c2 = boolean.class;

class.forname()

  如果类的完全限定类名可用,则可以使用class类的静态方法forname来获取class实例,例如:

class c = class.forname("com.maconn.reflectdemo");

  如果找不到完全限定类名对应的类型,则该方法会抛出一个classnotfound异常。此外,这种方式不能用于基本数据类型,下面的程序将会抛出异常:

try {
    class c = class.forname("int");
} catch (classnotfoundexception e) {
    e.printstacktrace();
}

基本类型包装类的type域

  每个基本数据类型以及void都有一个包装类,这些类都包含一个静态域type,该域的值就是被包装的基本类型。例如:

class c = integer.type;

2.获取类的信息

  一个类可能含有以下修饰元素:

  • public、protected或private:控制类的访问权限
  • abstract:限制类必须要被继承
  • static:仅用于内部类。创建该类对象时,无需先创建外部类的实例。
  • final:限制类不能被继承
  • strictfp:限制该类内部所有浮点运算严格按照ieee-754规范执行。
  • 注解:为类提供一些额外的信息或标记。

注:strictfp 的意思是fp-strict,也就是说精确浮点的意思。在java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,java的编译器以及运 行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令你满意。而一旦使用了strictfp来声明一个 类、接口或者方法时,那么所声明的范围内java的编译器以及运行环境会完全依照浮点规范ieee-754来执行。因此如果你想让你的浮点运算更加精确, 而且不会因为不同的硬件平台所执行的结果不一致的话,那就请用关键字strictfp。(节选自《java语言中关键字strictfp的用途》一文,原文链接)。

  下面通过一个程序来演示获取类信息的一些方法,该程序打印出了string类的各项信息:

import java.lang.annotation.annotation;
import java.lang.reflect.modifier;
import java.lang.reflect.type;
import java.lang.reflect.typevariable;
import java.util.arraylist;
import java.util.list;

import static java.lang.system.out;

public class classdeclarationspy {
    public static void main(string[] args) {
        try {
            class c = string.class;
            out.format("class:%n  %s%n%n", c.getcanonicalname());
            out.format("modifiers:%n  %s%n%n",
                    modifier.tostring(c.getmodifiers()));

            out.format("type parameters:%n");
            typevariable[] tv = c.gettypeparameters();
            if (tv.length != 0) {
                out.format("  ");
                for (typevariable t : tv)
                    out.format("%s ", t.getname());
                out.format("%n%n");
            } else {
                out.format("  -- no type parameters --%n%n");
            }

            out.format("implemented interfaces:%n");
            type[] intfs = c.getgenericinterfaces();
            if (intfs.length != 0) {
                for (type intf : intfs)
                    out.format("  %s%n", intf.tostring());
                out.format("%n");
            } else {
                out.format("  -- no implemented interfaces --%n%n");
            }

            out.format("inheritance path:%n");
            list<class> l = new arraylist<class>();
            printancestor(c, l);
            if (l.size() != 0) {
                for (class<?> cl : l)
                    out.format("  %s%n", cl.getcanonicalname());
                out.format("%n");
            } else {
                out.format("  -- no super classes --%n%n");
            }

            out.format("annotations:%n");
            annotation[] ann = c.getannotations();
            if (ann.length != 0) {
                for (annotation a : ann)
                    out.format("  %s%n", a.tostring());
                out.format("%n");
            } else {
                out.format("  -- no annotations --%n%n");
            }
        } catch (classnotfoundexception x) {
            x.printstacktrace();
        }
    }

    private static void printancestor(class<?> c, list<class> l) {
        class<?> ancestor = c.getsuperclass();
        if (ancestor != null) {
            l.add(ancestor);
            printancestor(ancestor, l);
        }
    }
}

  下面是该程序的输出:

class:
  java.lang.string

modifiers:
  public final

type parameters:
  -- no type parameters --

implemented interfaces:
  interface java.io.serializable
  java.lang.comparable<java.lang.string>
  interface java.lang.charsequence
  interface java.lang.constant.constable
  interface java.lang.constant.constantdesc

inheritance path:
  java.lang.object

annotations:
  -- no annotations --

  需要注意的是,并非所有的注解都可以通过反射获得。只有那些使用了元注解@retention{retentionpolicy.runtime}的注解才会保留到运行时。

3.发现成员

  可以通过获得的class对象来进一步获取类的成员,例如成员变量、成员方法和构造器。下面依次介绍获取这些成员的方法。

获取域

方法 描述 继承的成员 私有的成员
getdeclaredfield​(string name) 根据名称获取该类声明的成员变量 不包含 包含
getdeclaredfields() 获取该类声明的所有成员变量 不包含 包含
getfield(string name) 根据名称获取成员变量 包含 不包含
getfields() 获取所有的成员变量 包含 不包含

获取方法

方法 描述 继承的成员 私有的成员
getdeclaredmethod​(string name, class<?>... parametertypes) 根据名称和参数类型获取该类声明的方法 不包含 包含
getdeclaredmethods() 获取该类声明的所有方法 不包含 包含
getmethod​(string name, class<?>... parametertypes) 根据名称和参数类型获取成员方法 包含 不包含
getmethod​s() 获取所有的成员方法 包含 不包含

获取构造器

方法 描述 私有的成员
getdeclaredconstructor​(class<?>... parametertypes) 根据参数类型获取指定的构造器 包含
getdeclaredconstructors() 获取所有的构造器 包含
getconstructor​(class<?>... parametertypes) 根据参数类型获取指定的构造器 不包含
getconstructors() 获取所有的构造器 不包含

二.成员

  反射api中定义了一个表示成员的接口member,field、method和constructor类都实现了这个接口。在上一节中已经对获取这三种成员的方法进行了简单的了解,本节将会对三种成员的方法进行介绍。

1.域

  在反射api中,使用field类来表示对象的域。上文中已经介绍了如何通过class对象来获取field对象,现在我们来讨论有关field类的内容。

获取域的类型

  通过field类的gettype和getgenerictype可以获取当前域的类型。不同的是,gettype方法返回的是class对象,而getgenerictype返回的则是type类型的对象。此外,如果某个域是泛型类型的,gettype方法不能准确地返回该域的类型,而getgenerictype则可以返回该域的泛型类型。

type是java.lang.reflect包中的一个接口,该接口表示所有类型的高级公共接口,class类就是type接口的一个实现类。type包括原始类型、参数化类型、数组类型、类型变量和基本类型。

  下面的例子依次打印出了对每个域调用gettype和getgenerictype的结果:

import java.lang.reflect.field;
import java.util.list;

public class fieldspy<t> {
    public string name  = "alice";
    public list<integer> list;
    public t val;

    public static void main(string... args) {
        try {
            class<?> c = fieldspy.class;
            field f1 = c.getfield("name");
            system.out.println("field name:");
            system.out.format("type: %s%n", f1.gettype());
            system.out.format("generictype: %s%n", f1.getgenerictype());
            field f2 = c.getfield("list");
            system.out.println("field list:");
            system.out.format("type: %s%n", f2.gettype());
            system.out.format("generictype: %s%n", f2.getgenerictype());
            field f3 = c.getfield("val");
            system.out.println("field val:");
            system.out.format("type: %s%n", f3.gettype());
            system.out.format("generictype: %s%n", f3.getgenerictype());
        } catch (nosuchfieldexception x) {
            x.printstacktrace();
        }
    }
}

  上面的程序会产生以下输出:

field name:
type: class java.lang.string
generictype: class java.lang.string
field list:
type: interface java.util.list
generictype: java.util.list<java.lang.integer>
field val:
type: class java.lang.object
generictype: t

  val域的类型是object,这是因为编译器会在编译期间擦除有关泛型类型的所有信息。在这种情况下,将会使用类型变量的上界代替,在这个例子中,就是object。

获取值

  可以获取当前field所表示的域在指定对象的上的值。若该域是引用类型,则使用get(object obj)方法,该方法会返回一个object对象;若该域是基本数据类型,则可以使用getxxx(object obj)方法,xxx为boolean、byte、char、double、float、int、long、short中的一种。
  下面的程序分别获取了foo和bar域的值:

import java.lang.reflect.field;

public class fielddemo {
    private string foo;
    private int bar;

    public fielddemo(string stringfield, int intfield) {
        this.foo = stringfield;
        this.bar = intfield;
    }

    public static void main(string[] args) {
        class<?> c = fielddemo.class;
        fielddemo fielddemo = new fielddemo("foo", 1);
        try {
            field stringfield = c.getdeclaredfield("foo");
            system.out.println(stringfield.get(fielddemo));
            field intfield = c.getdeclaredfield("bar");
            system.out.println(intfield.getint(fielddemo));
        } catch (nosuchfieldexception | illegalaccessexception e) {
            e.printstacktrace();
        }
    }
}

  也可以为指定对象的域设置值。若该域是引用类型,则使用set(object obj, object value)方法;若该域是基本数据类型,则可以使用getxxx(object obj, xxx value)方法,xxx为boolean、byte、char、double、float、int、long、short中的一种。例如,可以通过下面的代码为上面的foo域和bar域设置值:

field stringfield = c.getdeclaredfield("foo");
stringfield.set(c, "fool");
field intfield = c.getdeclaredfield("bar");
intfield.set(c, 2);

获取其他信息

  • <t extends annotation> t getannotation​(class<t> annotationclass)
    返回该域上指定类型的注解。
  • <t extends annotation> t[] getannotationsbytype​(class<t> annotationclass)
    返回指定类型的注解,用数组返回。该方法可用于获取重复注解。
  • class<?> getdeclaringclass()
    返回声明该域的类。
  • int getmodifiers()
    用一个整数返回该域的修饰符。该整数需要使用modifier类来解码。
  • string getname()
    返回该域的名称。
  • boolean issynthetic()
    若该域是编译器自动生成的则返回true,否则返回false。

2.方法

  在反射api中,使用method类来表示方法。下面是对method类中一些常见方法的介绍。

获取方法的信息

  • <t extends annotation> t getannotation​(class<t> annotationclass)
    返回作用于该方法的指定类型的注解。
  • annotation[] getdeclaredannotations()
    返回直接出现在该方法上的注解。
  • class<?> getdeclaringclass()
    返回声明该方法的类。
  • type[] getgenericexceptiontypes()
    返回该方法抛出的异常
  • type[] getgenericparametertypes()
    返回该方法的参数类型。
  • class<?> getreturntype()
    返回该方法的返回值类型。
  • type getgenericreturntype()
    返回该方法的返回值类型。不能准确地返回使用了泛型的返回值类型。
  • string getname()
    返回方法名称。

调用方法

  通过method类的invoke方法可以对指定对象调用方法。下面是invoke方法的签名:
    object invoke​(object obj, object... args)

  第一个参数是要调用方法的对象,后面的可变参数则是该方法需要接收的参数。下面的程序获取了一个方法并调用了它:

import java.lang.reflect.method;

public class methodinvokedemo {
    public static void main(string[] args) {
        try {
            method method = foo.class.getmethod("bar", int.class, int.class);
            foo foo = new foo();
            method.invoke(foo, 1, 2);
        } catch (exception e) {
            e.printstacktrace();
        }
    }
}
class foo {
    public void bar(int a, int b) {
        system.out.println(a + b);
    }
}

3.构造器

  在反射api中,使用constructor类来表示构造器。因为constructor类中的大部分方法都和method类相似,此处不再赘述。这里主要讲述如何通过constructor类来构造类的实例。
  constructor类的newinstance​(object... initargs)方法可以用于创建新的实例。实际上,class类也有一个newinstance方法,但是推荐使用constructor类的newinstance方法来创建实例,而后者已经被标记为deprecated,不建议使用,这是因为:

  • class.newinstance()只能调用类的无参构造器,而constructor.newinstance​(object... initargs)可以调用类的任何构造器。
  • class.newinstance()要求构造函数必须可见,而在某些情况下constructor.newinstance()可以调用private构造函数。

  下面的程序通过constructor类的newinstance创建了person类的一个实例:

import java.lang.reflect.constructor;

public class newinstancedemo {
    public static void main(string[] args) {
        class<person> c = person.class;
        try {
            constructor<person> constructor = c.getconstructor(string.class, int.class);
            person p = constructor.newinstance("foo", 26);
            system.out.println(p);
        } catch (exception e) {
            e.printstacktrace();
        }
    }
}
class person {
    private string name;
    private int age;

    public person(string name, int age) {
        this.name = name;
        this.age = age;
    }

    @override
    public string tostring() {
        return "person{name=" + name + ", age=" + age + "}";
    }
}

  对于field、method和constructor所表示的成员来说,如果当前所在的类没有对这些成员的访问权限,那么也无法通过反射去访问这些成员。这种情况下,需要手动调用这些成员的setaccessible(boolean flag)方法将flag设置为true才能继续访问。

三.数组和枚举

1.数组

  数组是对一组固定数量的、相同类型的元素的引用,这些元素的类型可以是基本数据类型,也可以是引用类型。反射api中提供了访问数组类型,创建新数组,获取和设置元素的值的方法。

识别数组

  如果我们对数组引用调用.getclass()方法,会返回什么?下面来做一个实验:

public class arraydemo {
    public static void main(string[] args) {
        byte[] b = {1, 2, 3};
        integer[] i = {4, 5, 6};
        string[] s = {"hello", "world"};
        system.out.println(b.getclass());
        system.out.println(i.getclass());
        system.out.println(s.getclass());
    }
}

  上面的程序将会输出:

class [b
class [ljava.lang.integer;
class [ljava.lang.string;

  与一般的类不同的是,数组的类型前面都会加上左方括号([)。如果是二维数组,则会有两个左方括号,依次类推。如果是基本数据类型的数组,方括号右边只有一个大写字母,这个字母就是对应的基本类型的首字母。如果是引用类型,方括号右边是一个大写字母l然后加上数组元素的完全限定类名,以及最后的分号。
  虽然可以根据getclass()返回的结果来判断一个对象是不是数组,但是这样并不直观。class类提供了一个isarray()方法,通过这个方法可以直接判断某个对象是不是数组类型。

创建数组

  反射api中使用array类来表示数组。通过这个类的newinstance​(class<?> componenttype, int length)方法可以快速创建一个数组:

public static object createarray(class<?> c, int length) {
    return array.newinstance(c, length);
}

  上面的方法根据指定的类型和长度创建了一个数组并返回了对数组的引用,调用者需要将其强制转换为真正类型的数组来使用,例如integer[]。

获取和设置值

  array类提供了许多get和set方法。其中get(object array, int index)用来从引用类型的数组中获取值,而getxxx(object array, int index)则用于从基本数据类型的数组中获取值;set(object array, int index, object value)用来给引用类型的数组赋值,而setxxx(object array, int index, object value)则用于给基本数据类型的数组赋值。下面是一个例子:

public class arrayassignmentdemo {
    public static void main(string[] args) {
        int[] i = new int[2];
        string[] s = new string[2];
        array.setint(i, 0, 1);
        array.set(s, 0, "hello");
        system.out.println(array.getint(i, 0));
        system.out.println(array.get(s, 0));
    }
}

2.枚举

  反射api中提供了以下几个枚举专用的方法:

  • class.isenum()
    若该类是枚举类型,则返回true。
  • class.getenumconstants()
    按照声明的顺序返回枚举类型中的所有枚举常量。
  • java.lang.reflect.field.isenumconstant()
    若该域是枚举类型,则返回true。