谜题78:反射的污染

这个谜题举例说明了一个关于反射的简单应用。这个程序会打印出什么呢?

import java.util.*;
import java.lang.reflect.*;
public class Reflector {
public static void main(String[] args) throws Exception {
Set<String> s = new HashSet<String>();
s.add("foo");
Iterator it = s.iterator();
Method m = it.getClass().getMethod("hasNext");
System.out.println(m.invoke(it));
}
}

  


这个程序首先创建了一个只包含单个元素的集合(set),获得了该集合上的迭代
器,然后利用反射调用了迭代器的 hasNext 方法,最后打印出此该方法调用的结
果。由于该迭代器尚未返回该集合中那个唯一的元素,hasNext 方法应该返回
true。然而,运行这个程序却得到了截然不同的结果:

Exception in thread "main" java.lang.IllegalAccessException:
Class Reflector can not access a member of class HashMap$HashIterator
with modifiers "public"
at Reflection.ensureMemberAccess(Reflection.java:65)
at Method.invoke(Method.java:578)
at Reflector.main(Reflector.java:11)

  


这是怎么发生的呢?正如这个异常所显示的,hasNext 方法当然是公共的,所以
它在任何地方都是可以被访问的。那么为什么这个基于反射的方法调用是非法的
呢?这里的问题并不在于该方法的访问级别(access level),而在于该方法所
在的类型的访问级别。这个类型所扮演的角色和一个普通方法调用中的限定类型
(qualifying type)是相同的[JLS 13.1]。在这个程序中,该方法是从某个类
中选择出来的,而这个类型是由从 it.getClass 方法返回的 Class 对象表示的。
这是迭代器的动态类型(dynamic type),它恰好是私有的嵌套类(nested class)
java.util.HashMap.KeyIterator。出现 IllegalAccessException 异常的原因
就是这个类不是公共的,它来自另外一个包:访问位于其他包中的非公共类型的
成员是不合法的[JLS 6.6.1]。无论是一般的访问还是通过反射的访问,上述的
禁律都是有效的。下面这段没有使用反射的程序也违反了这条规则。

package library;
public class Api{

static class PackagePrivate{}
public static PackagePrivate member = new PackagePrivate();
}
package client;
import library.Api;
class Client{
public static void main(String[] args){
System.out.println(Api.member.hashCode());
}
}

  

尝试编译这段程序会得到如下的错误:

Client.java:5: Object.hashCode() isn't defined in a public
class or interface; can't be accessed from outside package
System.out.println(Api.member.hashCode());

  


这个错误与前面那个由含有反射的程序所产生的运行期错误具有相同的意义。
Object 类型和 hashCode 方法都是公共的。问题在于 hashCode 方法是通过一个
限定类型调用的,但用户访问不到这个类型。该方法调用的限定类型是
library.Api.PackagePrivate,这是一个位于其他包的非公共类型。
这并不意味着 Client 就不能调用 Api.member 的 hashCode 方法。要做到这一点,
只需要使用一个可访问的限定类型即可,在这里可以将 Api.member 转型成
Object。经过这样的修改之后,Client 类就可以顺利地编译和运行了:

System.out.println(((Object)Api.member).hashCode());


实际上,这个问题并不会在普通的非反射的访问中出现,因为 API 的编写者在他
们的公共 API 中只会使用公共的类型。即使这个问题有可能发生,它也会以编译
期错误的形式显现出来,所以比较容易修改。而使用反射的访问就不同了,
object.getClass().getMethod(“methodName”) 这种惯用法虽然很常见,但是
却有问题的,它不应该被使用。就像我们在前面的程序中看到的那样,这种用法
很容易在运行期产生一个 IllegalAccessException。
在使用反射访问某个类型时,请使用表示某种可访问类型的 Class 对象。回到我
们前面的那个程序,hasNext 方法是声明在一个公共类型 java.util.Iterator
中的,所以它的类对象应该被用来进行反射访问。经过这样的修改后,这个
Reflector 程序就会打印出 true:

Method m = Iterator.class.getMethod("hasNext");

你完全可以避免这一类的问题,你应该只有在实例化时才使用反射,而方法调用
都通过使用接口进行[EJ Item 35]。这种使用反射的用法,可以将那些调用方法
的类与那些实现这些方法的类隔离开,并且提供了更高程度的类型安全。这种用
法在“服务提供者框架”(Service Provider Frameworks)中很常见。这种模
式并不能解决反射访问中的所有问题,但是如果它可以解决你所遇到的问题,请
务必使用它。
总之,访问其他包中的非公共类型的成员是不合法的,即使这个成员同时也被声
明为某个公共类型的公共成员也是如此。不论这个成员是否是通过反射被访问
的,上述规则都是成立的。这个问题很有可能只在反射访问中才会出现。对于平
台的设计者来说,这里的教训与谜题 67 中的一样,应该让错误症状尽可能清晰
地显示出来。对于运行期的异常和编译期的提示都还有些东西需要改进。

posted @ 2018-10-24 02:09  尐鱼儿  阅读(153)  评论(0编辑  收藏  举报