您的位置:首页 > 职场人生

Java篇 - 反射机制分析(附面试中的坑)

2019-01-14 00:56 471 查看

反射的运用很广泛,很多库都运用了反射,如Junit,EventBus,Gson,Retrofit,Spring等。动态代理,Android的Hook技术也离不开反射的身影。

 

目录:

  1. 反射的概念
  2. 反射的使用
  3. 反射的优缺点
  4. 如何提高反射效率
  5. 面试中的反射问题

 

1. 反射的概念

在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。
这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。

通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法。

看一张图大致感受下反射的机制:

对JVM不熟悉的同学,可以翻看我前面的文章《JVM篇 - JVM原理》https://blog.csdn.net/u014294681/article/details/85104210

 

 

2. 反射的使用

 

  • 2.1 获取Class对象

有三种方式可以获取Class对象:

(1)  Class类的

forName
静态方法

Class类不能被混淆,否则通过名称将找不到Class对象。

[code]    public static void main(String[] args) {
// 1. Class类的forName静态方法获取Class对象
try {
Class<?> cls = Class.forName("io.kzw.advance.csdn_blog.TestReflection");
System.out.printf(cls.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

执行输出:class io.kzw.advance.csdn_blog.TestReflection

Class 的静态 forName() 方法有两个版本,上面的代码是只指定类名称的版本,而另一个版本可以让你指定类名称、加载时是否运行静态区块、指定类加载器:

[code]static Class forName(String name, boolean initialize, ClassLoader loader)

默认在加载类的时候,如果类中有定义静态块则会运行它。你可以使用 forName() 的第二个版本,将 initialize 设定为 false,这样在加载类时并不会立即运行静态块,而会在使用类建立对象时才运行静态块。

[code]public class Coder {

public String name;
public int age;

static {
System.out.println("Coder static block");
}
}
[code]public class TestReflection {

public static void main(String[] args) {
// 1. Class类的forName静态方法获取Class对象
//        try {
//            /**
//             * 输出
//             * Coder static block
//             * class io.kzw.advance.csdn_blog.Coder
//             */
//            Class<?> coderCls = Class.forName("io.kzw.advance.csdn_blog.Coder");
//            System.out.println(coderCls.toString());
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        }

try {
/**
* 输出
* class io.kzw.advance.csdn_blog.Coder
*/
Class<?> personCls = Class.forName("io.kzw.advance.csdn_blog.Coder",
false, ClassLoader.getSystemClassLoader());
System.out.println(personCls.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

/**
* 输出Coder static block
*/
Coder coder = new Coder();
}
}

(2) 直接获取某一个类的class对象

[code]    public static void main(String[] args) {
// 输出 class io.kzw.advance.csdn_blog.Coder
System.out.println(Coder.class.toString());
}

(3) 调用某个对象的

getClass()

[code]    public static void main(String[] args) {
Coder coder = new Coder();
// 输出 class io.kzw.advance.csdn_blog.Coder
System.out.println(coder.getClass().toString());
}

类名.class和对象.getClass()几乎没有区别,因为一个类被类加载器加载后,就是唯一的一个类对象。

 

  • 2.2 判断是否为某个类的实例

一般地,我们用

instanceof
关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的
isInstance()
方法来判断是否为某个类的实例,它是一个 native 方法:

public native boolean isInstance(Object obj)

[code]    public static void main(String[] args) {
Coder coder = new Coder();
try {
Class<?> cls = Class.forName("io.kzw.advance.csdn_blog.Coder");
// 输出true
System.out.println(cls.isInstance(coder));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

 

  • 2.3 创建实例

通过反射来生成对象主要有两种方式:

(1) 使用Class对象的newInstance()方法来创建Class对象对应类的实例

[code]    public static void main(String[] args) {
Class<?> cls = Coder.class;
try {
Coder coder = (Coder) cls.newInstance();
coder.name = "k";
coder.age = 18;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

(2) 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例

[code]public class Coder {

public String name;
public int age;

public Coder() {

}

public Coder(String name, int age) {
this.name = name;
this.age = age;
}
}
[code]    public static void main(String[] args) {
try {
Class<?> c = Coder.class;
Constructor constructor = c.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("k", 18);
System.out.println(obj);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

 

  • 2.4 获取方法

获取某个Class对象的方法集合,主要有以下几个方法:

(1) getDeclaredMethods
 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法

public Method[] getDeclaredMethods() throws SecurityException

(2) 

getMethods
 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法

public Method[] getMethods() throws SecurityException

(3) 

getMethod
 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象

public Method getMethod(String name, Class<?>... parameterTypes)

看一个例子:

[code]public class Coder {

public String name;
public int age;

public Coder() {
}

public Coder(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
[code]    public static void main(String[] args) {
Class<?> c = Coder.class;
try {
Object object = c.newInstance();
Method[] methods = c.getMethods();
Method[] declaredMethods = c.getDeclaredMethods();
Method method = c.getMethod("setName", String.class);
System.out.println("getMethods获取的方法:");
for(Method m:methods)
System.out.println(m);
System.out.println("getDeclaredMethods获取的方法:");
for(Method m:declaredMethods)
System.out.println(m);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

执行输出:

getMethods获取的方法:
public java.lang.String io.kzw.advance.csdn_blog.Coder.getName()
public void io.kzw.advance.csdn_blog.Coder.setName(java.lang.String)
public int io.kzw.advance.csdn_blog.Coder.getAge()
public void io.kzw.advance.csdn_blog.Coder.setAge(int)
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
getDeclaredMethods获取的方法:
public java.lang.String io.kzw.advance.csdn_blog.Coder.getName()
public void io.kzw.advance.csdn_blog.Coder.setName(java.lang.String)
public int io.kzw.advance.csdn_blog.Coder.getAge()
public void io.kzw.advance.csdn_blog.Coder.setAge(int)

 

  • 2.5 获取构造器信息

获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

public T newInstance(Object ... initargs)

此方法可以根据传入的参数来调用对应的Constructor创建对象实例。

 

  • 2.6 获取类的成员变量(字段)信息

主要是这几个方法,在此不再赘述:

  • getFiled
    :访问公有的成员变量
  • getDeclaredField
    :所有已声明的成员变量,但不能得到其父类的成员变量

getFileds
 和 
getDeclaredFields
 方法用法同上(参照 Method)。

 

  • 2.7 调用方法 
[code]    public static void main(String[] args) {
Class<?> c = Coder.class;
try {
Coder coder = (Coder) c.newInstance();
Method method = c.getMethod("setName", String.class);
method.invoke(coder, "k");
// 输出k
System.out.println(coder.getName());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

上面的setName方法是public的,如果改成private呢?

会执行报错:

java.lang.NoSuchMethodException: io.kzw.advance.csdn_blog.Coder.setName(java.lang.String)
    at java.lang.Class.getMethod(Class.java:1786)
    at io.kzw.advance.csdn_blog.TestReflection.main(TestReflection.java:13)

[code]    public static void main(String[] args) {
Class<?> c = Coder.class;
try {
Coder coder = (Coder) c.newInstance();
// 改成getDeclaredMethod
Method method = c.getDeclaredMethod("setName", String.class);
// 设权限可以访问
method.setAccessible(true);
method.invoke(coder, "k");
System.out.println(coder.getName());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

这样就可以了,获取私有变量也是一样。

 

  • 2.8 利用反射创建数组

数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:

[code]    public static void main(String[] args) {
try {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
Array.set(array,0,"a");
Array.set(array,1,"b");
Array.set(array,2,"c");
Array.set(array,3,"d");
Array.set(array,4,"e");
// 输出d
System.out.println(Array.get(array,3));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

 

 

3. 反射的优缺点

反射被广泛地用于那些需要在运行时检测或修改程序行为的程序中。这是一个相对高级的特性,只有那些语言基础非常扎实的开发者才应该使用它。如果能把这句警示时刻放在心里,那么反射机制就会成为一项强大的技术,可以让应用程序做一些几乎不可能做到的事情。

反射的缺点:

  • 反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。
  • 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用:代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
  • 反射破坏了代码的封装性。

 

 

4. 如何提高反射效率

  • 尽量不要
    getMethods()
    后再遍历筛选,而直接用
    getMethod(methodName)
    来根据方法名获取方法。
  • 需要多次动态创建一个类的实例的时候,有缓存的写法会比没有缓存要快很多。
[code]void createInstance(String className){
    cachedClass = cache.get(className);
    if (cachedClass == null) {
// Class.forName耗时
        cachedClass = Class.forName(className);
        cache.set(className, cachedClass);
    }
    return cachedClass.newInstance();
}
  • 使用高性能的反射库,比自己写缓存效果好,如joor,或者apache的commons相关工具类。
  • 使用高版本JDK也很重要,反射性能一直在提高。

 

 

5. 面试中的反射问题

如果你看了上面的问题,我相信大部分反射面试题都没问题,但是现在的面试题一般不是死记硬背式,而是结合实际例子,考察你会不会活学活用,我这边分享2个问题。

 

  • 5.1 常量是否能被反射更改?
[code]public class SysConstant {

public static final Integer INT_VALUE = 100;
}
[code]    public static void main(String[] args) {
try {
System.out.println(SysConstant.INT_VALUE);
Field field = SysConstant.class.getDeclaredField("INT_VALUE");
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, 200);
System.out.println(SysConstant.INT_VALUE);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

执行输出:

100
200

说明反射常量成功。

 

我们再试试String:

[code]public class SysConstant {

public static final String VERSION = "1.0.0";
}
[code]    public static void main(String[] args) {
try {
System.out.println(SysConstant.VERSION);
Field field = SysConstant.class.getDeclaredField("VERSION");
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, "2.0.0");
System.out.println(SysConstant.VERSION);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

执行输出:

1.0.0
1.0.0

反射没有生效,除了String,然后又试了int、long、boolean,都是反射没生效。

原因:

对于基本类型的静态常量,JAVA在编译的时候就会把代码中对此常量中引用的地方替换成相应常量值。

 

  • 5.2 反射常量会影响程序逻辑吗?

通过第一个问题,就很容易回答了,如果是基本数据类型,包括String,不会影响程序逻辑。

[code]public class SysConstant {

public static final String VERSION = "1.0.0";
public static final boolean DEBUG = true;

public static void print() {
if (VERSION.equals("1.0.0")) {
System.out.println("1.0.0");
}
}

public static void register() {
if (DEBUG) {
System.out.println("register");
}
}
}

看看编译后的SysConstant.class:

[code]public class SysConstant {
public static final String VERSION = "1.0.0";
public static final boolean DEBUG = true;

public SysConstant() {
}

public static void print() {
if ("1.0.0".equals("1.0.0")) {
System.out.println("1.0.0");
}

}

public static void register() {
System.out.println("register");
}
}

对于基本类型的静态常量,JAVA在编译的时候就会把代码中对此常量中引用的地方替换成相应常量值。

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: