早在介绍多态的时候,曾经提到公鸡实例的性别属性可能被篡改为雌性,不过面向对象的三大特性包含了封装、继承和多态,只要把性别属性设置为private私有级别,也不提供setSex这样的性别修改方法,那么性别属性就被严严实实地封装了起来,不但外部无法修改性别属性,连公鸡类的子类都无法修改。如此一来,公鸡实例的性别属性可谓防护周全,压根不存在被篡改的可能性。但是Java给面向对象留了个后门,也就是反射技术,利用反射技术竟然能够攻破封装的防护网,使得篡改私有属性从理想变成了现实,赶紧来看看反射技术是怎样做到这点的。
上一篇文章讲到通过字符串可以获得该串所代表的Class对象,那么通过字段名称字符串也能获得对应的字段对象,其中的获取操作用到了Class对象的getDeclaredField方法,完整的字段对象获取代码如下所示:
try { Class cls = Chicken.class; // 获得Chicken类的基因类型 // 通过字段名称获取该类的字段对象 Field sexField = cls.getDeclaredField("sex"); } catch (NoSuchFieldException e) { // 捕捉到了无此字段异常 e.printStackTrace(); } catch (SecurityException e) { // 捕捉到了安全异常 e.printStackTrace(); }
注意调用getDeclaredField方法之时需要捕捉两种异常,包括无此字段异常NoSuchFieldException和安全异常SecurityException。现在得到的Field对象便隐藏着sex属性的内在信息,要想从Field对象挖掘出sex属性的数值,还得继续下列两个步骤的处理:
1、调用Field对象的setAccessible方法,并传入true值,表示将该字段设置为允许访问,以解除private的限制;
2、调用Field对象的getInt方法,并传入鸡类实例,表示准备从该示例中获取指定字段的整型值。同理调用getBoolean方法获取的是布尔值,调用getString方法获取的是字符串值。倘若是获取基本类型以外的类型值,则需先调用get方法获得Object对象,再强制转换为目标类型。
整合以上的两个处理步骤,得到以下的字段数值获取代码:
if (sexField != null) { sexField.setAccessible(true); // 将该字段设置为允许访问 try { sex = sexField.getInt(chicken); // 获取某实例的字段值 } catch (IllegalArgumentException e) { // 捕捉到了非法参数异常 e.printStackTrace(); } catch (IllegalAccessException e) { // 捕捉到了非法入口异常 e.printStackTrace(); } }
注意字段对象的getInt方法在调用时也要捕捉两种异常,包括非法参数异常IllegalArgumentException,以及非法入口异常IllegalAccessException。这里的两种异常加上之前调用getDeclaredField方法的两种异常,寥寥数行的反射代码竟要手工捕捉四种异常,未免太大动干戈了。其实程序员可以相信自己,保证反射过程中的操作代码完全正确,这样便无需逐个捕捉某种异常,只要一次性捕捉总的异常即Exception就行了。于是简化了异常捕捉逻辑的反射代码变成了下面这般:
// 通过反射来获得某个实例的私有属性 private static int getReflectSex(Chicken chicken) { int sex = -1; try { Class cls = Chicken.class; // 获得Chicken类的基因类型 // 通过字段名称获取该类的字段对象 Field sexField = cls.getDeclaredField("sex"); if (sexField != null) { sexField.setAccessible(true); // 将该字段设置为允许访问 sex = sexField.getInt(chicken); // 获取某实例的字段值 } } catch (Exception e) { // 捕捉到了任何一种异常(错误除外) e.printStackTrace(); } return sex; }
然而上面的代码仅仅通过反射取到性别字段的数值,仍旧没能修改该字段的数值,若想真正改变性别字段的取值,需要把getInt方法改为setInt方法,并给setInt方法的第二个参数传入修改后的数值。此时利用反射技术篡改字段值的代码示例如下:
// 通过反射来修改某个实例的私有属性 private static void setReflectSex(Chicken chicken, int sex) { try { Class cls = Chicken.class; // 获得Chicken类的基因类型 // 通过字段名称获取该类的字段对象 Field sexField = cls.getDeclaredField("sex"); if (sexField != null) { sexField.setAccessible(true); // 将该字段设置为允许访问 sexField.setInt(chicken, sex); // 将某实例的该字段修改为指定数值 } } catch (Exception e) { // 捕捉到了任何一种异常(错误除外) e.printStackTrace(); } }
从上述的setReflectSex代码可知,该方法传入一个鸡类实例和新的性别,目的是把这只鸡的性别变过来。这下有了getReflectSex方法可读取性别属性,还有setReflectSex方法可写入性别属性,再由外部接连调用这两个方法,从而验证反射技术的执行效果。下面是外部先后篡改公鸡实例性别、篡改母鸡实例性别的演示代码:
Cock cock = new Cock(); // 创建一个公鸡实例 System.out.println("准备修理公鸡,性别取值 = "+getReflectSex(cock)); setReflectSex(cock, cock.FEMALE); // 把公鸡实例的性别篡改为“雌性” System.out.println("结束修理公鸡,性别取值 = "+getReflectSex(cock)); Hen hen = new Hen(); // 创建一个母鸡实例 System.out.println("准备修理母鸡,性别取值 = "+getReflectSex(hen)); setReflectSex(hen, hen.MALE); // 把母鸡实例的性别篡改为“雄性” System.out.println("结束修理母鸡,性别取值 = "+getReflectSex(hen));
运行以上的演示代码,观察到下列的日志描述:
准备修理公鸡,性别取值 = 0 结束修理公鸡,性别取值 = 1 准备修理母鸡,性别取值 = 1 结束修理母鸡,性别取值 = 0
可见尽管鸡类的sex属性被声明为private,但是公鸡实例的性别依然被篡改为雌性,母鸡实例的性别依然被篡改为雄性了。
更多Java技术文章参见《Java开发笔记(序)章节目录》