A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 郭宁 中级黑马   /  2012-6-4 13:52  /  2114 人查看  /  4 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

今天早空间看到一个关于反射迭代器的问题,虽然当时知道哪里出问题了,但自己也不明白,中午回来查阅了JAVA解惑,得到了答案 给大家分享一下。


谜题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中的一样,应该让错误症状尽可能清晰地显示出来。对于运行期的异常和编译期的提示都还有些东西需要改进。

评分

参与人数 1技术分 +1 收起 理由
黄奕豪 + 1 赞一个!

查看全部评分

4 个回复

倒序浏览
总结的不错,非常感谢
回复 使用道具 举报
感谢分享!学习了!
回复 使用道具 举报
本帖最后由 刘蕴学 于 2012-6-12 07:58 编辑

给你个建议,你可以试着阅读并自己实现一套collection framework 的所有成员,你这个帖子的问题在这些数据结构的迭代器实现那里看代码更直观

1.反射时有个很蛋疼的问题,即引用类型和实例类型,当然这个得看具体情况。
比如下边这段代码,你更应该看是在什么情况下才选择那种方式。
  1.   Collection a = new ArrayList();
  2.                 Collection.class.getMethod("add", null);
  3.                 a.getClass().getMethod("add", null);
复制代码
2.反射的另外一个弊端是,基本类型处理方面,你需要将类型进行转换为包装类型,反射时并不会自动装箱和拆箱。
这段代码是动态代理的代理代码生成中的基本类型处理代码,功能是将返回值返回到代理方法调用处,问题就是这么产生的,返回值因为可能存在范型,所以是Object的,但是基本类型不能被转成基本类型,所以如此蛋疼的代码就产生了。
  1.                  //写入返回值,不过要注意的是基本类型
  2.                         //如果直接返回不强制转换为包装类型的
  3.                         //话会出现ClassCastException
  4.                         //Object cannot be cast to primitive type
  5.                         if(returnType != void.class)
  6.                         {
  7.                                 if(returnType == boolean.class)
  8.                                         sb.append(" (Boolean)");
  9.                                 else if(returnType == int.class)
  10.                                         sb.append(" (Integer)");
  11.                                 else if(returnType == byte.class)
  12.                                         sb.append(" (Byte)");
  13.                                 else if(returnType == short.class)
  14.                                         sb.append(" (Short)");
  15.                                 else if(returnType == long.class)
  16.                                         sb.append(" (Long)");
  17.                                 else if(returnType == float.class)
  18.                                         sb.append(" (Float)");
  19.                                 else if(returnType == double.class)
  20.                                         sb.append(" (Double)");
  21.                                 else if(returnType == char.class)
  22.                                         sb.append(" (Character)");
  23.                                 else
  24.                                         sb.append(" ("+returnType.getCanonicalName()+")");
  25.                                 sb.append("obj");
  26.                         }
复制代码
3.关于变量,被 static final 标识的将无法被反射。

4.关于方法,反射时的Method类型实例,也依赖于类实例的类型,并不是说我有一个跟他一样的方法,就可以使用该method实例来进行相同的访问。
比如下面的代码,方法的反射是错误的,当然变量的访问也不行。
  1. public class asd
  2. {
  3.         public static void main(String[] args) throws Exception
  4.         {
  5.                 A.class.getField("a").get(new B());
  6.                 A.class.getMethod("asd", new Class[]{}).invoke(new B(), new Object[]{});
  7.         }        
  8.         
  9.         public static class A
  10.         {
  11.                 public int a = 0;
  12.                 public void asd(){}
  13.         }
  14.         
  15.         public static class B
  16.         {
  17.                 public int a = 0;
  18.                 public void asd(){}
  19.         }
  20. }
复制代码
5.还有一些限定方法很复杂,比如Object的clone,如果你的类不实现这个Cloneable的接口(虽然这个clone方法已经被Object声明了),反射时会有让你发狂的问题。
  1. public class asd
  2. {
  3.         public static void main(String[] args) throws Exception
  4.         {
  5.                 B.class.getMethod("clone", new Class<?>[]{}).invoke(new B(), new Object[]{});
  6.                 A.class.getMethod("clone", new Class<?>[]{}).invoke(new A(), new Object[]{});
  7.         }        
  8.         
  9.         public static class A{}
  10.         public static class B implements Cloneable
  11.         {
  12.                 @Override
  13.                 //Overrides: clone() in Object
  14.                 public Object clone()
  15.                 {
  16.                         return this;
  17.                 }
  18.         }
  19. }
复制代码
如果猜想Object中的clone方法是protected的,使用暴力反射来试着访问,就像下边这段代码,结果大吃一惊,还是NoSuchMethod。。。
  1. public class asd
  2. {
  3.         public static void main(String[] args) throws Exception
  4.         {
  5.                 B.class.getMethod("clone", new Class<?>[]{}).invoke(new B(), new Object[]{});
  6.                 Method clone = A.class.getDeclaredMethod("clone", new Class<?>[]{});
  7.                 clone.setAccessible(true);
  8.                 clone.invoke(new A(), new Object[]{});
  9.         }        
  10.         
  11.         public static class A{}
  12.         public static class B implements Cloneable
  13.         {
  14.                 @Override
  15.                 //Overrides: clone() in Object
  16.                 public Object clone()
  17.                 {
  18.                         return this;
  19.                 }
  20.         }
  21. }
复制代码
6.暴力反射是不推荐的,而且在程序中大量应用反射也不对,暴力反射会影响程序稳定性,导致不可预期的问题,比如我声明了一个私有成员,我以为你可以按照我的封装方式来安全访问,结果就是可能你的暴力反射会导致我的代码崩溃。反射的带价还是很高,在无必要的情况一般不推荐使用反射,效率只是一方面。

7.访问权限,这个实际上很好理解,你必须保证你的反射过程中,所有路径都是可行的,仅目的地可以到达是不行的。

8.底层名字,这个我也不知道应该怎么说,或许sun本身就很蛋疼,举例来说像数组,你如果使用getName方法,会拿到一个[L开头的名字,所以getCanonicalName才是正确的做法。

9.反射可以帮助我们来解开很多神秘的面纱,比如匿名内部类为什么不能访问非final的局部变量这个问题,只要是百度的,99%的人的答案都是错的,那1%中99%虽然知道是什么原理,但是他们也没有办法证明。但这并不证明我们要在程序中大量应用反射。
下边这段代码就是这个问题的根本
  1. /* 定义一个局部内部类的上层接口,用以允许返回局部内部类实例 */
  2. interface SuperInner
  3. {
  4.         void inner();
  5. }
  6. /* 定义一个外部类,包括一个outer方法,用以返回局部内部类实例 */
  7. class Outer
  8. {
  9.         int y = 0;
  10.         //out方法将返回inner实例
  11.         SuperInner outer()
  12.         {
  13.                 //假设我们这里x可以被inner访问,那么outer函数返回之后
  14.                 //x已经被销毁,inner对象就无法访问x
  15.                
  16.                 //换句话说就是Inner对象的生存周期超出了x的边界,而能保证
  17.                 //生存周期一致的情况只有inner对象自己的成员,所以通过在
  18.                 //inner里边复制一个x来保存这个局部变量x的值以达到目的
  19.                 //就像val$x一样
  20.                
  21.                 //但这种办法有个致命点,如果在inner里对val$x进行赋值操作
  22.                 //外边的x并不会被改变,所以为了保证两个对象的一致性,该变量
  23.                 //x以及其复制变量val$x必须为终态的,也就是final,不能再次
  24.                 //赋值
  25.                
  26.                 //反编译inner可以证明以上结论,在字节码里
  27. //                question6.Outer$1Inner(question6.Outer, java.lang.String)
  28.                 //这是构造方法,传outer不奇怪,内部类都需要外部类的引用指针
  29.                 //这个String字符串就值得思考了
  30.                
  31.                 //4 getfield val$y
  32.                 //在inner方法中可以看到这个字节码,是val$y 而不是y
  33.                 int x = 4;
  34.                 final String y = new String("asd");
  35.                 class Inner implements SuperInner
  36.                 {
  37. //                        int val$x = 4;
  38.                         public void inner()
  39.                         {
  40.                                 try
  41.                                 {
  42.                                         //这里通过反射,可以发现证明此y非彼y,因为我们并没有
  43.                                         //在inner里定义val$y变量,而我们却可以拿得到,没异常
  44.                                         //而我们改的是val$y的值为qqq,下边的syso(y)居然会输出
  45.                                         //qqq,结论可以证实
  46.                                        
  47.                                         //通过代码也可以看出来,这个val$y肯定是private的,并且
  48.                                         //他也是final的
  49.                                         Field field = getClass().getDeclaredField("val$y");
  50.                                         field.setAccessible(true);
  51.                                         System.out.println(field.get(Inner.this));
  52.                                         field.set(Inner.this, "qqq");
  53.                 }
  54.                 catch (Exception e)
  55.                 {
  56.                         e.printStackTrace();
  57.                 }
  58. //                                System.out.println(x);本行编译报错,提示局部内部类访问必须是终态的
  59.                                 System.out.println(y);//输出y的值
  60.                         }
  61.                 }
  62.                 //当inner实例被返回时,x将被销毁
  63.                 return new Inner();
  64.         }
  65. }
复制代码
10.反射与范型是不可分割的,至少目前来看是这样的,典型的例子就是Class<?> clazz = ...;这样的声明方式,如果你单写Class clazz 这样的声明是不安全的,至少编译器是这么认为的,黄线真的很难看。在实际使用代码时,范型要清晰的写出来,避免一些类型不确定的问题,而在反射时就要把这种假范型完全扔出去,在所有范型的地方,本质仍然是Object。

11.多接口的实现类的方法反射,这个在动态代理会出现,代理是根据你声明接口带反射方法的,如果单一接口实现不会出问题,但是多接口的情况就会很复杂,你需要知道他依赖于哪一个接口。

评分

参与人数 1技术分 +1 收起 理由
黄奕豪 + 1 牛~~~~

查看全部评分

回复 使用道具 举报
就想起来这么多,凑合看吧
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马