这是本菜鸡面试时被问到的一个问题,觉得挺有意思的,遂打算写一篇文章简单的谈下自己的看法。
在讨论这个问题之前,让我们先来简单复习一下Java反射的知识。
一、类型信息
在回顾Java反射之前,我们先来看一下RTTI(Run-Time Type Identification),运行时类型识别,它能在运行时就能够自动识别每个编译时已知的类型。
理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似类型转换这样的操作。
二、反射
如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。并利用这些信息做一些有用的事情。换句话说,在编译的时候,编译器必须知道所有要通过RTTI来处理的类。
而反射则不然,它并不需要编译时获取类的信息!
Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,用以表示未知类里对应的成员。这样的话就可以使用Contructor创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,以返回表示字段、方法、以及构造器对象的数组,这样,对象信息可以在运行时被完全确定下来,而在编译时不需要知道关于类的任何事情。
接下来,本文就Field简单的举个例,看看反射有多强大。
首先来简单的定义一个小白鼠类。。。
class Person{
public Person() {
}
public String name = "小明";
public int age = 15;
private int height = 170;
private double weight = 55.6;
@Override
public String toString() {
return "我叫"+name+",我今年"+age+"岁了,我的身高为"+height+"cm,体重为"+weight+"kg";
}
}
之后我们开始使用反射来实现一些骚操作~
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
System.out.println("通过反射获取Person的class对象");
Class clazz = Class.forName("com.util.xgb.Person");
System.out.println("根据Person的class对象来创建一个Person对象");
Object obj = clazz.getConstructor().newInstance();
System.out.println("先来给大伙自我介绍一下。。。");
System.out.println(obj);
System.out.println("----------------------------------------------------");
System.out.println("通过反射拿到person对象的所有成员变量!连私有的我也要知晓!");
Field[] fields = clazz.getDeclaredFields();
for(Field f : fields) {
System.out.println(f);
}
System.out.println("----------------------------------------------------");
System.out.println("现在见证反射的强大威力,穿透Person的private屏障!改变私有字段的值!");
Field field1 = clazz.getDeclaredField("weight");
Field field2 = clazz.getDeclaredField("height");
System.out.println("暴力反射,解除私有限定,使得我们可以更改私有属性的值!");
field1.setAccessible(true);
field1.set(obj, 60.5);
field2.setAccessible(true);
field2.set(obj, 160);
System.out.println("再来介绍一下自己吧。。。");
System.out.println(obj);
}
我们看一下打印输出结果。
通过反射获取Person的class对象
根据Person的class对象来创建一个Person对象
先来给大伙自我介绍一下。。。
我叫小明,我今年15岁了,我的身高为170cm,体重为55.6kg
----------------------------------------------------
通过反射拿到person对象的所有成员变量!连私有的我也要知晓!
public java.lang.String com.util.xgb.Person.name
public int com.util.xgb.Person.age
private int com.util.xgb.Person.height
private double com.util.xgb.Person.weight
----------------------------------------------------
现在见证反射的强大威力,穿透Person的private屏障!改变私有字段的值!
暴力反射,解除私有限定,使得我们可以更改私有属性的值!
再来介绍一下自己吧。。。
我叫小明,我今年15岁了,我的身高为160cm,体重为60.5kg
尽管举的例子很low,但是已经能够表达意思了,不管字段和方法被何种权限修饰符修饰,在反射面前,它们都是一丝不挂的。。。。
其实反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:
我们知道,封装、继承、多态是Java的三大特性!而我们通过上面简单的示例也看到了,通过反射,我们可以轻而易举的获取对私有成员的操作权利。那么这就是所谓的封装性遭受到破坏了吗?Java这样做岂不是搬起石头砸自己的脚吗?
我们不妨先来回想一下我们自己在使用Java撸代码的时候,声明私有方法时的出发点是什么?是不是大多数的情况下,这样做是为了支撑某一个我们对外提供的方法?并且,很多时候,我们为了代码的可读性会将一整个public方法拆分成很多个言简意赅、易读性更强的private方法和一个组合这些private方法的public方法。对于私有的成员变量,也是同理,使用private修饰变量,也是为了更好的服务于我们编写的某个对外提供的功能,而这些私有的成员变量和成员方法,仅在本类内有意义,如果对外开发的话,不仅毫无意义,甚至会严重影响到我们打算对外提供的功能。下面笔者拿ArrayList这个大家都很熟悉的类的源码举个例子:
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
比如大家最熟悉的ArrayList为我们提供的add方法,我们可以看到它仅有短短的三行代码,但是其中有一个非常重要的操作就是需要确保底层的数组空间足够,如果不够需要执行相应的扩容。那么我们可以清晰的看到这些涉及检查容量/扩容的方法几乎都是private的,它对使用ArrayList的用户完全透明,但是却举足轻重,都是为了使public的add方法更加完整更加安全。
笔者挑选的这几个简单的私有成员来进行举例,它们无不是为了服务于ArrayList对外为我们提供的那些功能而存在,同时,如果这些私有成员对外开放,它们中的任何一个单独拿出来都将不具备任何意义,并且如果放宽这些私有成员的存取权限,很可能会导致对ArrayList的操作结果出乎我们的意料,甚至很有可能会出错。
现在,我们再回过头来想一想,Java反射破坏了封装性吗?
通过上述的分析我们现在应该有了答案
写在最后: