BetterMethodFinder.java
- By Paul Holser
- July 24, 2001
Power Java
Limitations of reflective method lookup
by Paul Holser
Listing 5. BetterMethodFinder.java.
package reflection;
import java.lang.reflect.*;
import java.util.*;
/**
* Finds methods and constructors that can be invoked reflectively.
* Attempts to address some of the limitations of the JDK's
* Class.getMethod() and Class.getConstructor(), and other JDK
* reflective facilities.
*/
public final class BetterMethodFinder {
/**
* The target class to look for methods and constructors in.
*/
private final Class clazz;
/**
* Mapping from method name to the Methods in the target
* class with that name.
*/
private final Map methodMap = new HashMap();
/**
* List of the Constructors in the target class.
*/
private final List ctorList = new ArrayList();
/**
* Mapping from a Constructor or Method object to the Class
* objects representing its formal parameters.
*/
private final Map paramMap = new HashMap();
/**
* @param clazz Class in which I will look for methods and
* constructors
* @exception IllegalArgumentException if clazz is null, or
* represents a primitive, or represents an array type
*/
public BetterMethodFinder(Class clazz) {
if (clazz == null) {
throw new IllegalArgumentException("null Class parameter");
}
if (clazz.isPrimitive()) {
throw new IllegalArgumentException(
"primitive Class parameter");
}
if (clazz.isArray()) {
throw new IllegalArgumentException(
"array Class parameter");
}
this.clazz = clazz;
loadMethods();
loadConstructors();
}
public boolean equals(Object o) {
if (this == o)
return true;
else if (o == null || getClass() != o.getClass())
return false;
else {
BetterMethodFinder other = (BetterMethodFinder) o;
return clazz.equals(other.clazz);
}
}
/**
* Returns the most specific public constructor in my target class
* that accepts the number and type of parameters in the given
* Class array in a reflective invocation.
* <p>
* A null value or Void.TYPE in parameterTypes matches a
* corresponding Object or array reference in a constructor's formal
* parameter list, but not a primitive formal parameter.
*
* @param parameterTypes array representing the number and
* types of parameters to look for in the constructor's signature. A
* null array is treated as a zero-length array.
* @return Constructor object satisfying the conditions
* @exception NoSuchMethodException if no constructors match
* the criteria, or if the reflective call is ambiguous based on the
* parameter types
*/
public Constructor findConstructor(Class[] parameterTypes)
throws NoSuchMethodException {
if (parameterTypes == null)
parameterTypes = new Class[0];
return (Constructor) findMemberIn(ctorList, parameterTypes);
}
/**
* Basis of findConstructor() and findMethod(). The member list
* fed to this method will be either all Constructor objects or all
* Method objects.
*/
private Member findMemberIn(List memberList, Class[] parameterTypes)
throws NoSuchMethodException {
List matchingMembers = new ArrayList();
for (Iterator it = memberList.iterator(); it.hasNext();) {
Member member = (Member) it.next();
Class[] methodParamTypes = (Class[]) paramMap.get(member);
if (Arrays.equals(methodParamTypes, parameterTypes))
return member;
if (ClassUtilities.compatibleClasses(methodParamTypes, parameterTypes))
matchingMembers.add(member);
}
if (matchingMembers.isEmpty())
throw new NoSuchMethodException(
"no member in " + clazz.getName() + " matching given args");
if (matchingMembers.size() == 1)
return (Member) matchingMembers.get(0);
return findMostSpecificMemberIn(matchingMembers);
}
/**
* Returns the most specific public method in my target class that
* has the given name and accepts the number and type of
* parameters in the given Class array in a reflective invocation.
* <p>
* A null value or Void.TYPE in parameterTypes will match a
* corresponding Object or array reference in a method's formal
* parameter list, but not a primitive formal parameter.
*
* @param methodName name of the method to search for
* @param parameterTypes array representing the number and
* types of parameters to look for in the method's signature. A
* null array is treated as a zero-length array.
* @return Method object satisfying the conditions
* @exception NoSuchMethodException if no methods match the
* criteria, or if the reflective call is ambiguous based on the
* parameter types, or if methodName is null
*/
public Method findMethod(String methodName, Class[] parameterTypes)
throws NoSuchMethodException {
List methodList = (List) methodMap.get(methodName);
if (methodList == null)
throw new NoSuchMethodException(
"no method named " + clazz.getName() + "." + methodName);
if (parameterTypes == null)
parameterTypes = new Class[0];
return (Method) findMemberIn(methodList, parameterTypes);
}
/**
* @param a List of Members (either all Constructors or all
* Methods)
* @return the most specific of all Members in the list
* @exception NoSuchMethodException if there is an ambiguity
* as to which is most specific
*/
private Member findMostSpecificMemberIn(List memberList)
throws NoSuchMethodException {
List mostSpecificMembers = new ArrayList();
for (Iterator memberIt = memberList.iterator(); memberIt.hasNext();)
{
Member member = (Member) memberIt.next();
if (mostSpecificMembers.isEmpty())
{
// First guy in is the most specific so far.
mostSpecificMembers.add(member);
}
else
{
boolean moreSpecific = true;
boolean lessSpecific = false;
// Is member more specific than everyone in the most-specific set?
for (Iterator specificIt = mostSpecificMembers.iterator(); specificIt.hasNext();)
{
Member moreSpecificMember = (Member) specificIt.next();
if (! memberIsMoreSpecific(member, moreSpecificMember))
{
/* Can't be more specific than the whole set. Bail out, and
mark whether member is less specific than the member
under consideration. If it is less specific, it need not be
added to the ambiguity set. This is no guarantee of not
getting added to the ambiguity set...we're just not clever
enough yet to make that assessment. */
moreSpecific = false;
lessSpecific = memberIsMoreSpecific(moreSpecificMember, member);
break;
}
}
if (moreSpecific)
{
// Member is the most specific now.
mostSpecificMembers.clear();
mostSpecificMembers.add(member);
}
else if (! lessSpecific)
{
// Add to ambiguity set if mutually unspecific.
mostSpecificMembers.add(member);
}
}
}
if (mostSpecificMembers.size() > 1)
{
throw new NoSuchMethodException(
"Ambiguous request for member in "
+ getTargetClass().getName()
+ " matching given args" );
}
return (Member) mostSpecificMembers.get(0);
}
/**
* @param args an Object array
* @return an array of Class objects representing the classes of the
* objects in the given Object array. If args is null, a zero-length
* Class array is returned. If an element in args is null, then
* Void.TYPE is the corresponding Class in the return array.
*/
public static Class[] getParameterTypesFrom(Object[] args)
{
Class[] argTypes = null;
if (args != null) {
argTypes = new Class[args.length];
for (int i = 0; i < args.length; ++i)
argTypes[i] = (args[i] == null) ? Void.TYPE : args[i].getClass();
}
else
argTypes = new Class[0];
return argTypes;
}
/**
* @param classNames String array of fully qualified names
* (FQNs) of classes or primitives. Represent an array type by using
* its JVM type descriptor, with dots instead of slashes (e.g.
* represent the type int[] with "[I", and Object[][] with
* "[[Ljava.lang.Object;").
* @return an array of Class objects representing the classes or
* primitives named by the FQNs in the given String array. If the
* String array is null, a zero-length Class array is returned. If an
* element in classNames is null, the empty string, "void", or "null",
* then Void.TYPE is the corresponding Class in the return array.
* If any classes require loading because of this operation, the
* loading is done by the ClassLoader that loaded this class. Such
* classes are not initialized, however.
* @exception ClassNotFoundException if any of the FQNs name
* an unknown class
*/
public static Class[] getParameterTypesFrom(String[] classNames)
throws ClassNotFoundException {
return getParameterTypesFrom(
classNames, BetterMethodFinder.class.getClassLoader());
}
/**
* @param classNames String array of fully qualified names
* (FQNs) of classes or primitives. Represent an array type by using
* its JVM type descriptor, with dots instead of slashes (e.g.
* represent the type int[] with "[I", and Object[][] with
* "[[Ljava.lang.Object;").
* @param loader a ClassLoader
* @return an array of Class objects representing the classes or
* primitives named by the FQNs in the given String array. If the
* String array is null, a zero-length Class array is returned. If an
* element in classNames is null, the empty string, "void", or "null",
* then Void.TYPE is the corresponding Class in the return array.
* If any classes require loading because of this operation, the
* loading is done by the given ClassLoader. Such classes are not
* initialized, however.
* @exception ClassNotFoundException if any of the FQNs name
* an unknown class
*/
public static Class[] getParameterTypesFrom(String[] classNames,
ClassLoader loader)
throws ClassNotFoundException {
Class[] types = null;
if (classNames != null) {
types = new Class[classNames.length];
for (int i = 0; i < classNames.length; ++i)
types[i] =
ClassUtilities.classForNameOrPrimitive(classNames[i], loader);
}
else
types = new Class[0];
return types;
}
public int hashCode() {
return clazz.hashCode();
}
/**
* Loads up the data structures for my target class's constructors.
*/
private void loadConstructors() {
Constructor[] ctors = clazz.getConstructors();
for (int i = 0; i < ctors.length; ++i) {
ctorList.add(ctors[i]);
paramMap.put(ctors[i], ctors[i].getParameterTypes());
}
}
/**
* Loads up the data structures for my target class's methods.
*/
private void loadMethods() {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; ++i) {
Method m = methods[i];
String methodName = m.getName();
Class[] paramTypes = m.getParameterTypes();
List list = (List) methodMap.get(methodName);
if (list == null) {
list = new ArrayList();
methodMap.put(methodName, list);
}
if (! ClassUtilities.classIsAccessible(clazz))
m = ClassUtilities.getAccessibleMethodFrom(clazz, methodName, paramTypes );
if (m != null) {
list.add(m);
paramMap.put(m, paramTypes);
}
}
}
/**
* @param first a Member
* @param second a Member
* @return true if the first Member is more specific than the second,
* false otherwise. Specificity is determined according to the
* procedure in the Java Language Specification, section 15.12.2.
*/
private boolean memberIsMoreSpecific(Member first, Member second) {
Class[] firstParamTypes = (Class[]) paramMap.get(first);
Class[] secondParamTypes = (Class[]) paramMap.get(second);
return ClassUtilities.compatibleClasses(secondParamTypes, firstParamTypes);
}
}
About the Author
Paul Holser is a senior consultant at Valtech, an international consulting group specializing in e-business technologies. He can be contacted at [email protected].