Java 反射机制应用实践

引言

Java反射机制是一个非常强大的功能,在很多大型项目比如Spring,
Mybatis都可以看见反射的身影。通过反射机制我们可以在运行期间获取对象的类型信息,利用这一特性我们可以实现工厂模式和代理模式等设计模式,同时也可以解决Java泛型擦除等令人苦恼的问题。本文我们就从实际应用的角度出发,来应用一下Java的反射机制。

 泛型是Java SE
1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
Java语言引入泛型的好处是安全简单。

反射基础

p.s:
本文需要读者对反射机制的API有一定程度的了解,如果之前没有接触过的话,建议先看一下官方文档的Quick
Start。

在应用反射机制之前,首先我们先来看一下如何获取一个对象对应的反射类Class,在Java中我们有三种方法可以获取一个对象的反射类。

在Java SE
1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

通过getClass方法

在Java中,每一个Object都有一个getClass方法,通过getClass方法我们可以获取到这个对象对应的反射类:

String s = "ziwenxie";
Class<?> c = s.getClass();

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

通过forName方法

我们也可以调用Class类的静态方法forName

Class<?> c = Class.forName("java.lang.String");

1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

使用.class

或者我们也可以直接使用.class

Class<?> c = String.class;

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

获取类型信息

在文章开头我们就提到反射的一大好处就是可以允许我们在运行期间获取对象的类型信息,下面我们通过一个例子来具体看一下。

首先我们在typeinfo.interfacea包下面新建一个接口A澳门新葡亰3522平台游戏,:

package typeinfo.interfacea;
public interface A { void f(); }

接着我们在typeinfo.packageaccess包下面新建一个接口C,接口C继承自接口A,并且我们还另外创建了几个用于测试的方法,注意下面几个方法的权限都是不同的。

package typeinfo.packageaccess;
import typeinfo.interfacea.A;
class C implements A {
    public void f() { System.out.println("public C.f()"); }
    public void g() { System.out.println("public C.g()"); }
    protected void v () { System.out.println("protected C.v()"); }
    void u() { System.out.println("package C.u()"); }
    private void w() { System.out.println("private C.w()"); }
}
public class HiddenC {
    public static A makeA() { return new C(); }
}

callHiddenMethod()方法中我们用到了几个新的API,其中getDeclaredMethod()根据方法名用于获取Class类指代对象的某个方法,然后我们通过调用invoke()方法传入实际的对象就可以触发对象的相关方法:

package typeinfo;
import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;
import java.lang.reflect.Method;
public class HiddenImplementation {
    public static void main(String[] args) throws Exception {
        A a = HiddenC.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Oops! Reflection still allows us to call g():
        callHiddenMethod(a, "g");
        // And even methods that are less accessible!
        callHiddenMethod(a, "u");
        callHiddenMethod(a, "v");
        callHiddenMethod(a, "w");
    }
    static void callHiddenMethod(Object a, String methodName) throws Exception {
        Method g = a.getClass().getDeclaredMethod(methodName);
        g.setAccessible(true);
        g.invoke(a);
    }
}

从输出结果我们可以看出来,不管是publicdefaultprotect还是pricate方法,通过反射类我们都可以自由调用。当然这里我们只是为了显示反射的强大威力,在实际开发中这种技巧还是不提倡。

public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()

3、泛型的类型参数可以有多个。

应用实践

我们有下面这样一个业务场景,我们有一个泛型集合类List<Class<? extends Pet>>,我们需要统计出这个集合类中每种具体的Pet有多少个。由于Java的泛型擦除,注意类似List<? extends Pet>的做法肯定是不行的,因为编译器做了静态类型检查之后,到了运行期间JVM会将集合中的对象都视为Pet,但是并不会知道Pet代表的究竟是Cat还是Dog,所以到了运行期间对象的类型信息其实全部丢失了。p.s:
关于泛型擦除:我在上一篇文章里面有详细解释,感兴趣的朋友可以看一看。

为了实现我们上面的例子,我们先来定义几个类:

public class Pet extends Individual {
    public Pet(String name) { super(name); }
    public Pet() { super(); }
}
public class Cat extends Pet {
    public Cat(String name) { super(name); }
    public Cat() { super(); }
}
public class Dog extends Pet {
    public Dog(String name) { super(name); }
    public Dog() { super(); }
}
public class EgyptianMau extends Cat {
    public EgyptianMau(String name) { super(name); }
    public EgyptianMau() { super(); }
}
public class Mutt extends Dog {
    public Mutt(String name) { super(name); }
    public Mutt() { super(); }
}

上面的Pet类继承自IndividualIndividual类的的实现稍微复杂一点,我们实现了Comparable接口,重新自定义了类的比较规则,如果不是很明白的话,也没有关系,我们已经将它抽象出来了,所以不理解实现原理也没有关系。

public class Individual implements Comparable<Individual> {
    private static long counter = 0;
    private final long id = counter++;
    private String name; // name is optional
    public Individual(String name) { this.name = name; }
    public Individual() {}
    public String toString() {
        return getClass().getSimpleName() + (name == null ? "" : " " + name);
    }
    public long id() { return id; }
    public boolean equals(Object o) {
        return o instanceof Individual && id == ((Individual)o).id;
    }
    public int hashCode() {
        int result = 17;
        if (name != null) {
            result = 37 * result + name.hashCode();
        }
        result = 37 * result + (int) id;
        return result;
    }
    public int compareTo(Individual arg) {
        // Compare by class name first:
        String first = getClass().getSimpleName();
        String argFirst = arg.getClass().getSimpleName();
        int firstCompare = first.compareTo(argFirst);
        if (firstCompare != 0) {
            return firstCompare;
        }
        if (name != null && arg.name != null) {
            int secendCompare = name.compareTo(arg.name);
            if (secendCompare != 0) {
                return secendCompare;
            }
        }
        return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
    }
}

下面创建了一个抽象类PetCreator,以后我们通过调用arrayList()方法便可以直接获取相关Pet类的集合。这里使用到了我们上面没有提及的newInstance()方法,它会返回Class类所真正指代的类的实例,这是什么意思呢?比如说声明new Dog().getClass().newInstance()和直接new Dog()是等价的。

public abstract class PetCreator {
    private Random rand = new Random(47);
    // The List of the different getTypes of Pet to create:
    public abstract List<Class<? extends Pet>> getTypes();
    public Pet randomPet() {
        // Create one random Pet
        int n = rand.nextInt(getTypes().size());
        try {
            return getTypes().get(n).newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    public Pet[] createArray(int size) {
        Pet[] result = new Pet[size];
        for (int i = 0; i < size; i++) {
           result[i] = randomPet();
        }
        return result;
    }
    public ArrayList<Pet> arrayList(int size) {
        ArrayList<Pet> result = new ArrayList<Pet>();
        Collections.addAll(result, createArray(size));
        return result;
    }
}

接下来我们来实现上面这一个抽象类,解释一下下面的代码,在下面的代码中,我们声明了两个集合类,allTypestypes,其中allTypes中包含了我们呢上面所声明的所有类,但是我们具体的类型实际上只有两种即MuttEgypianMau,所以我们真正需要new出来的宠物只是types中所包含的类型,以后我们通过调用getTypes()便可以得到types中所包含的所哟类型。

public class LiteralPetCreator extends PetCreator {
    @SuppressWarnings("unchecked")
    public static final List<Class<? extends Pet>> allTypes = Collections.unmodifiableList(
        Arrays.asList(Pet.class, Dog.class, Cat.class, Mutt.class, EgyptianMau.class));
    private static final List<Class<? extends Pet>> types = allTypes.subList(
        allTypes.indexOf(Mutt.class), allTypes.size());
    public List<Class<? extends Pet>> getTypes() {
        return types;
    }
}

总体的逻辑已经完成了,最后我们实现用来统计集合中相关Pet类个数的TypeCounter类。解释一下isAssignalbeFrom()方法,它可以判断一个反射类是某个反射类的子类或者间接子类。而getSuperclass()顾名思义就是得到某个反射类的父类了。

public class TypeCounter extends HashMap<Class<?>, Integer> {
    private Class<?> baseType;
    public TypeCounter(Class<?> baseType) {
        this.baseType = baseType;
    }
    public void count(Object obj) {
        Class<?> type = obj.getClass();
        if (!baseType.isAssignableFrom(type)) {
            throw new RuntimeException(
                obj + " incorrect type " + type + ", should be type or subtype of " + baseType);
        }
        countClass(type);
    }
    private void countClass(Class<?> type) {
        Integer quantity = get(type);
        put(type, quantity == null ? 1 : quantity + 1);
        Class<?> superClass = type.getSuperclass();
        if (superClass != null && baseType.isAssignableFrom(superClass)) {
            countClass(superClass);
        }
    }
    @Override
    public String toString() {
        StringBuilder result = new StringBuilder("{");
        for (Map.Entry<Class<?>, Integer> pair : entrySet()) {
            result.append(pair.getKey().getSimpleName());
            result.append("=");
            result.append(pair.getValue());
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("} ");
        return result.toString();
    }
}

4、泛型的参数类型可以使用extends语句,例如<T extends
superclass>。习惯上称为“有界类型”。 

5、泛型的参数类型还可以是通配符类型。例如Class<?> classType =
Class.forName(“java.lang.String”);

泛型擦除以及相关的概念

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

 

类型擦除引起的问题及解决方法

 1、先检查,在编译,以及检查编译的对象和引用传递的问题

2、自动类型转换

3、类型擦除与多态的冲突和解决方法

4、泛型类型变量不能是基本数据类型

5、运行时类型查询

6、异常中使用泛型的问题

7、数组(这个不属于类型擦除引起的问题)

9、类型擦除后的冲突

10、泛型在静态方法和静态类中的问题

 

1. 问:Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别? 

答:泛型是 Java SE 1.5
的新特性,泛型的本质是参数化类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。在
Java SE 1.5 之前没有泛型的情况的下只能通过对类型 Object
的引用来实现参数的任意化,其带来的缺点是要做显式强制类型转换,而这种强制转换编译期是不做检查的,容易把问题留到运行时,所以
泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率,避免在运行时出现
ClassCastException。

JDK 1.5 引入了泛型来允许强类型在编译时进行类型检查;JDK 1.7
泛型实例化类型具备了自动推断能力,譬如 List<String> list = new
ArrayList<String>(); 可以写成 List<String> llist = new
ArrayList<>(); 了,JDK
具备自动推断能力。下面几种写法可以说是不同版本的兼容性了:

//JDK 1.5 推荐使用的写法
List<String> list = new ArrayList<String>();
//JDK 1.7 推荐使用的写法
List<String> llist = new ArrayList<>();

//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告

List<String> list = new ArrayList();

//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告

List list = new ArrayList<String>(); 

 

2. **问:Java 泛型是如何工作的?什么是类型擦除?**

答:泛型是通过类型擦除来实现的,编译器在编译时擦除了所有泛型类型相关的信息,所以在运行时不存在任何泛型类型相关的信息,譬如
List<Integer> 在运行时仅用一个 List 来表示,这样做的目的是为了和
Java 1.5
之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

 

3. 问:Java 泛型类、泛型接口、泛型方法有什么区别?

 答:泛型类是在实例化类的对象时才能确定的类型,其定义譬如 class
Test<T> {},在实例化该类时必须指明泛型 T 的具体类型。

泛型接口与泛型类一样,其定义譬如 interface Generator<E> { E dunc(E
e); }。

泛型方法所在的类可以是泛型类也可以是非泛型类,是否拥有泛型方法与所在的类无关,所以在我们应用中应该尽可能使用泛型方法,不要放大作用空间,尤其是在
static 方法时 static 方法无法访问泛型类的类型参数,所以更应该使用泛型的
static 方法(声明泛型一定要写在 static
后返回值类型前)。泛型方法的定义譬如 <T> void func(T val) {}。 

 

4. 问:Java 如何优雅的实现元组?

 答:元组其实是关系数据库中的一个学术名词,一条记录就是一个元组,一个表就是一个关系,纪录组成表,元组生成关系,这就是关系数据库的核心理念。很多语言天生支持元组,譬如
Python 等,在语法本身支持元组的语言中元组是用括号表示的,如 (int, bool,
string) 就是一个三元组类型,不过在 Java、C
等语言中就比较坑爹,语言语法本身不具备这个特性,所以在 Java
中我们如果想优雅实现元组就可以借助泛型类实现,如下是一个三元组类型的实现:

Triplet<A, B, C> {

   private A a;
   private B a;
   private C a;
   public Triplet(A a, B b, C c) {
       this.a = a;
       this.b = b;
       this.c = c;
   }
}

 

5. 问:下面程序块的运行结果是什么,为什么?

Class c1 = new
ArrayList<String>().getClass();

Class c2 = new ArrayList<String>().getClass();

System.out.println(c1 == c2);

答:上面代码段结果为 true,解释如下。

因为 load 的是同一个 class 文件,存在 ArrayList.class 文件但是不存在
ArrayList<String>.class 文件,即便是通过 class.getTypeParameters()
方法获取类型信息也只能获取到 [T]
一样的泛型参数占位符。泛型是通过擦除来实现的,所以编译后任何具体的泛型类型都被擦除了(替换为非泛型上边界,如果没有指定边界则为
Object 类型),泛型类型只有在静态类型检查期间才出现,上面都被擦除成了
ArrayList 类型,所以运行时加载的是同一个 class 文件。

 

6. 问:为什么 Java 泛型要通过擦除来实现?擦除有什么坏处或者说代价?

 答:可以说 Java
泛型的存在就是一个不得已的妥协,正因为这种妥协导致了 Java
泛型的混乱,甚至说是 JDK 泛型设计的失败。Java
之所以要通过擦除来实现泛型机制其实是为了兼容性考虑,只有这样才能让非泛化代码到泛化代码的转变过程建立在不破坏现有类库的实现上。正是因为这种兼容也带来了一些代价,譬如泛型不能显式地引用运行时类型的操作之中(如向上向下转型、instanceof
操作等),因为所有关于参数的信息都丢失了,所以任何时候使用泛型都要提醒自己背后的真实擦除类型到底是什么;此外擦除和兼容性导致了使用泛型并不是强制的(如
List<String> list = new ArrayList();
等写法);其次擦除会导致我们在编写代码时十分谨慎(如不想被擦除为 Object
类型时不要忘了添加上边界操作等)。