Wednesday, January 6, 2010

Puzzle 78: Reflection Infection











 < Day Day Up > 







Puzzle 78: Reflection Infection



This puzzle illustrates a simple application of reflection. What does this program print?





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));

}

}














Solution 78: Reflection Infection



The program creates a set with a single element in it, gets an iterator over the set, invokes the iterator's hasNext method reflectively, and prints the result of the method invocation. As the iterator hasn't yet returned the set's sole element, hasNext should return TRue. Running the program, however, tells a different story:





Exception in thread "main" 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)




How can this be? Of course the hasNext method is public, just as the exception tells us, and so can be accessed from anywhere. So why should the reflective method invocation be illegal?



The problem isn't the access level of the method; it's the access level of the type from which the method is selected. This type plays the same role as the qualifying type in an ordinary method invocation [JLS 13.1]. In this program, the method is selected from the class represented by the Class object that is returned by it.getClass. This is the dynamic type of the iterator, which happens to be the private nested class java.util.HashMap.KeyIterator. The reason for the IllegalAccessException is that this class is not public and comes from another package: You cannot legally access a member of a nonpublic type from another package [JLS 6.6.1].



This prohibition applies whether the access is normal or reflective. Here is a program that runs afoul of this rule without resorting to reflection:





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());

}

}




Attempting to compile the program results in this error:





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());

^




This diagnostic makes about as much sense as the runtime error generated by the original reflective program. The class Object and the method hashCode are both public. The problem is that the hashCode method is invoked with a qualifying type that is inaccessible to the client. The qualifying type of the method invocation is library.Api.PackagePrivate, which is a nonpublic class in a different package.



This does not imply that Client can't invoke hashCode on Api.member. To do this, it has merely to use an accessible qualifying type, which it can do by casting Api.member to Object. With this change, Client compiles and runs successfully:





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




As a practical matter, this problem doesn't arise in ordinary nonreflective access, because API writers use only public types in their public APIs. Even if the problem were to occur, it would manifest itself as a compile-time error, so it would be fixed quickly and easily. Reflective access is another matter. Although common, the idiom object.getClass().getMethod("methodName") is broken and should not be used. It can easily result in an IllegalAccessException at run time, as we saw in the original program.



When accessing a type reflectively, use a Class object that represents an accessible type. Going back to our original program, the hasNext method is declared in the public type java.util.Iterator, so its class object should be used for reflective access. With this change, the Reflector program prints TRue as expected:





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




You can avoid this whole category of problem if you use reflection only for instantiation and use interfaces to invoke methods [EJ Item 35]. This use of reflection isolates the class that invokes methods from the class that implements them and provides a high degree of type-safety. It is commonly used in Service Provider Frameworks. This pattern does not solve every problem that demands reflective access, but if it solves your problem, by all means use it.



In summary, it is illegal to access a member of a nonpublic type in a different package, even if the member is also declared public in a public type. This is true whether the member is accessed normally or reflectively. The problem is likely to manifest itself only in reflective access. For platform designers, the lesson, as in Puzzle 67, is to make diagnostics as clear as possible. Both the runtime exception and the compiler diagnostic leave something to be desired.

























     < Day Day Up > 



    No comments: