第11章_反射

gong_yz大约 12 分钟JAVAJAVASE

1、反射的概念及作用

1.1 java.lang.Class

Java的Class类是java反射机制的基础,通过Class类我们可以获得关于一个类的相关信息。

JVM会为每个类的都创建一个Class对象,运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。

通过以下3种方式可以获取一个Java类的Class对象。

  1. 调用对象的getClass()方法获取Class对象:

    MyObject object = new Object();
    Class clazz = object.getClass();
    
  2. 根据类名.class获取Class对象:

    Class clazz = MyObject.class;
    
  3. 根据Class中的静态方法Class.forName()获取Class对象:

    Class clazz = Class.forName("MyObject");
    

1.2 通过反射创建对象

在Java中,一般使用new关键字创建对象,其实使用反射也可以创建对象,且有两种方式。

第一种方式是使用Class类的newInstance方法,这个newInstance方法是通过调用类中定义的无参的构造函数来创建对象的。

当我们通过前面介绍的3种方式中的任意一种获取一个Class对象之后,就可以调用newInstance创建对象了:

Class clazz = MyObject.class;
MyObject myObj = clazz.newInstance();

和Class类的 newInstance方法类似,第二种方式就是利用java.lang.reflect.Constructor类中的newInstance方法。我们可以通过newInstance方法调用有参数的构造函数和私有的构造函数。

事实上Class的newInstance方法内部也是通过调用Constructor的newInstance方法实现的:

Constructor<MyObject>  constructor = MyObject.class.getConstructor();
MyObject myObj = constructor.newInstance();

1.3 通过反射获取类的属性、方法和注解等

我们用Class表示一个Java类,用Filed表示类中的属性,用Method表示类中的方法,用Annotation表示类中的注解,用Constructor表示类的构造方法。 所以,在Class类中可以找到一下方法:

Field[] getFields()
Method[] getMethods()
Annotation[] getAnnotations()
Constructor<?>[] getConstructors()

这些方法分别用来获取一个类中定义的属性,方法和注解,以及构造函数的列表。

需要注意的是,上面的几个方法是无法获取私有的方法、属性等的。如果想获取私有内容,则需要使用以下几个方法:

Field[] getDeclaredFields()
Method[] getDeclaredMethods()
Annotation[] getDeclaredAnnotations()
Constructor<?>[] getDeclaredConstructors()

上面的方法的返回值都是数组类型的,如果想在运行期获取指定的方法、属性和注解,则可以使用以下方法:

Field getDeclaredField(String name)
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
<A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

1.4 反射的优缺点

优点:

  • 可以在运行期间获得类的信息并操作一个类中的方法,提高了程序的灵活性和扩展性。

缺点:

  • 反射的代码的可读性和可维护性都比较低
  • 反射的代码的执行性能低
  • 反射破坏了封装性

2、反射是如何破坏单例模式的

相关信息

反射可以在运行期间获取并调用一个类的方法,包括私有方法,可以利用反射创建一个新对象。

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

双重锁校验实现单例模式如下:

package com.gyz.test.testdemo.test;
import java.io.Serializable;
/**
 *
 * @Author GongYuZhuo
 * @Version 1.0.0
 */
public class Singleton {
	private static volatile Singleton singleton;
	private Singleton() {
	}
	public static Singleton getInstance() {
		if (singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

以上代码可以保证Singleton类的对象只有一个。其做法就是将Singleton设置为私有,并且在getInstance() 方法中做了并发防控。

但是,反射可以在运行期间调用一个类的私有方法。所以,使用反射是可以破坏单例的。可以通过如下方式利用反射创建一个新对象:

Singleton singleton1 = Singleton.getInstance();
//通过反射获取构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1 == singleton2);

输出结果为false。创建了一个新对象。

通过setAccessible(true) ,在使用反射对象时可以取消访问限制检查,使得私有的构造函数能够被访问。

相关信息

如何避免单例对象被反射破坏呢

反射是调用默认的构造函数创建对象的,我们只需要修改构造函数,使其在反射调用时识别对象是不是被创建即可:

private Singleton() {
    if (singleton!=null){
        throw new RuntimeException("单例对象只能创建一次...");
    }
}

3、利用反射与工厂模式实现Spring IoC

3.1 反射机制介绍

3.1.1 反射机制概念

我们.java文件在编译后会变成.class文件,这就像是个镜面,本身是.java,在镜中是.class,他们其实是一样的;那么同理,我们看到镜子的反射是.class,就能通过反编译,了解到.java文件的本来面目。

对于反射,官方给出的概念

反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个Java类获取它所有的成员变量和方法并且显示出来。

反射主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。

反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高!类中有什么信息,利用反射机制就能可以获得什么信息,不过前提是得知道类的名字。

3.1.2 反射机制的作用

在运行时判断任意一个对象所属的类;

在运行时获取类的对象;

在运行时访问java对象的属性,方法,构造方法等。

首先要搞清楚为什么要用反射机制?直接创建对象不就可以了吗,这就涉及到了动态与静态的概念。

  • 静态编译:在编译时确定类型,绑定对象,即通过。
  • 动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,用以降低类之间的藕合性。

3.1.3 反射机制的优缺点

反射机制的优点:

  • 可以实现动态创建对象和编译,体现出很大的灵活性(特别是在J2EE的开发中它的灵活性就表现的十分明显)。通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
  • 比如,一个大型的软件,不可能一次就把它设计得很完美,把这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时动态地创建和编译,就可以实现该功能。

反射机制的缺点:

  • 对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且让它满足我们的要求。这类操作总是慢于直接执行相同的操作。

3.2 反射与工厂模式实现IOC

Spring中的IoC的实现原理就是工厂模式加反射机制。 我们首先看一下不用反射机制时的工厂模式:

interface fruit{
    public abstract void eat();
} 
class Apple implements fruit{
     public void eat(){
         System.out.println("Apple");
     }
} 
class Orange implements fruit{
     public void eat(){
         System.out.println("Orange");
     }
}
//构造工厂类
//也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
class Factory{
     public static fruit getInstance(String fruitName){
         fruit f=null;
         if("Apple".equals(fruitName)){
             f=new Apple();
         }
         if("Orange".equals(fruitName)){
             f=new Orange();
         }
         return f;
     }
}
class hello{
     public static void main(String[] a){
         fruit f=Factory.getInstance("Orange");
         f.eat();
     }
}

上面写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改动就会很多。下面用反射机制实现工厂模式:

interface fruit{
     public abstract void eat();
}
class Apple implements fruit{
public void eat(){
         System.out.println("Apple");
     }
}
class Orange implements fruit{
public void eat(){
        System.out.println("Orange");
    }
}
class Factory{
    public static fruit getInstance(String ClassName){
        fruit f=null;
        try{
            f=(fruit)Class.forName(ClassName).newInstance();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}
class hello{
    public static void main(String[] a){
        fruit f=Factory.getInstance("Reflect.Apple");
        if(f!=null){
            f.eat();
        }
    }
}

现在就算我们添加任意多个子类的时候,工厂类都不需要修改。使用反射机制实现的工厂模式可以通过反射取得接口的实例,但是需要传入完整的包和类名。而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。

下面编写使用反射机制并结合属性文件的工厂模式(即IoC)。首先创建一个fruit.properties的资源文件:

apple=Reflect.Apple
orange=Reflect.Orange

然后编写主类代码:

interface fruit{
    public abstract void eat();
}
class Apple implements fruit{
    public void eat(){
        System.out.println("Apple");
    }
}
class Orange implements fruit{
    public void eat(){
        System.out.println("Orange");
    }
}
//操作属性文件类
class init{
    public static Properties getPro() throws FileNotFoundException, IOException{
        Properties pro=new Properties();
        File f=new File("fruit.properties");
        if(f.exists()){
            pro.load(new FileInputStream(f));
        }else{
            pro.setProperty("apple", "Reflect.Apple");
            pro.setProperty("orange", "Reflect.Orange");
            pro.store(new FileOutputStream(f), "FRUIT CLASS");
        }
        return pro;
    }
}
class Factory{
    public static fruit getInstance(String ClassName){
        fruit f=null;
        try{
            f=(fruit)Class.forName(ClassName).newInstance();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}
class hello{
    public static void main(String[] a) throws FileNotFoundException, IOException{
        Properties pro=init.getPro();
        fruit f=Factory.getInstance(pro.getProperty("apple"));
        if(f!=null){
            f.eat();
        }
    }
}

运行结果:Apple

3.3 IOC容器的技术剖析

IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只要是在Spring中生产的对象都要在配置文件中给出定义,目的就是提高灵活性和可维护性。

反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得10倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2倍的差距。

我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

3.4 使用IOC框架应该注意什么

使用IOC框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC框架的缺点,做到心中有数,杜绝滥用框架。

  • 软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
  • 由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
  • 具体到IOC框架产品(比如Spring)来讲,需要进行大量的配置工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
  • IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。

我们大体可以得出这样的结论:

  • 一些工作量不大的项目或者产品,不太适合使用IOC框架产品。
  • 另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。
  • 最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。