Java Tip 113: Identify subclasses at runtime

Retrieve all the classes that implement a given interface

Java Reflection APIs and Java interfaces provide great tools for writing reusable code. Take, for example, the case of a generic command launcher: Suppose you had a set of classes performing various tasks -- such as switching on or off the light; opening, closing, or locking the door; and so forth. The name of those classes would be LightOn, LightOff, DoorOpen, DoorClose, and DoorLock, respectively. All these classes conveniently implement the Command interface defined below:

public interface Command {
    public void process();
}

You can write a simple generic launcher in the following way:

public class Launcher{
    public static void main(String[] args){
        if (args.length>0) {
            try {
                Command command = (Command)Class.forName(args[0]).newInstance();
                command.process();
            } catch (Exception ex) {
                System.out.println("Invalid command");
            } 
        } else {
            System.out.println("Usage: Launcher <command>");
        }
    }
} // Launcher

You use the Class.forName() method to get a Class object for the class given in a parameter. Then you create an instance of this class using the newInstance() method. Since the class is expected to implement the Command interface, you cast the object into Command, and then call the process() method that will perform the task. If an exception is launched -- due to a misspelling of the class, a wrong name, or a security exception -- you display an Invalid command message.

You can use the command launcher in the following way:

%java Launcher LightOn 

If new tasks are implemented, you don't need to change anything to the launcher. From a programmer point of view, that's great. But what about the user? Suppose a user enters:

%java Launcher OpenDoor
Invalid command

Does the Invalid command message mean the user cannot open the door? No, it is simply a name problem (OpenDoor instead of DoorOpen). The user should be able to see a list of available commands. To keep a generic launcher, he or she must be able to find those commands at runtime.

Java Reflection provides a lot of information about a given class at runtime: you can easily know all its super classes, implemented interfaces, methods, constructors, fields, and so on. But in this case, we are interested in all the classes that implement a given interface. No such information is available via Java Reflection. Before reading Mike Clark's "Java Tip 105: Mastering the Classpath with JWhich," I had no clue how to obtain that information. The remainder of this tip will show you how to apply a process for retrieving information on classes that implement a given interface.

A user-friendly generic command launcher

JWhich provides a way to obtain a File object from a package name. Since packages in Java are directories, it is easy to retrieve all the classes contained in a package using the File object's list() method.

The idea is to check, for each class file in the package, whether or not the corresponding class implements the Command interface using the instanceof statement. This means you can only check each class file's public class, and that the interface and its implementations must be located in a package.

Here is the code:

    public static void find(String pckgname) {
        // Code from JWhich
        // ======
        // Translate the package name into an absolute path
        String name = new String(pckgname);
        if (!name.startsWith("/")) {
            name = "/" + name;
        }        
        name = name.replace('.','/');
        
        // Get a File object for the package
        URL url = Launcher.class.getResource(name);
        File directory = new File(url.getFile());
        // New code
        // ======
        if (directory.exists()) {
            // Get the list of the files contained in the package
            String [] files = directory.list();
            for (int i=0;I<files.length;i++) {
                 
                // we are only interested in .class files
                if (files[i].endsWith(".class")) {
                    // removes the .class extension
                    String classname = files[i].substring(0,files[i].length()-6);
                    try {
                        // Try to create an instance of the object
                        Object o = Class.forName(pckgname+"."+classname).newInstance();
                        if (o instanceof Command) {
                            System.out.println(classname);
                        }
                    } catch (ClassNotFoundException cnfex) {
                        System.err.println(cnfex);
                    } catch (InstantiationException iex) {
                        // We try to instantiate an interface
                        // or an object that does not have a 
                        // default constructor
                    } catch (IllegalAccessException iaex) {
                        // The class is not public
                    }
                }
            }
        }
    }

To perform the task at hand, you just need to modify the original launcher a bit. You can assume now that the interface and its implementations are in the package commands:

    public static void main(String[] args){
        if (args.length>0) {
            try {
                Command command = (Command)Class.forName("commands."+args[0]).newInstance();
                command.process();
            } catch (Exception ex) {
                System.out.println("Invalid command");
                System.out.println("Available commands:");
                find("commands");        
            } 
        } else {
            System.out.println("Usage: Launcher <command>");
        }
    }

Here is the new result of a wrong command:

%java Launcher OpenDoor
Invalid command
Available commands:
LightOn
LightOff
DoorOpen
DoorClose
DoorLock

Runtime subclass identification

You can refine the find() method to find any subclass of a given class. To do so, you use the dynamic version of instanceof, isInstance(). You replace the (o instanceof Command) with (tosubclass.isInstance(o)), where tosubclass is the class given in the parameter of the find() method.

Now you have a method that can find any subclass of a given class in a given package. You can improve the method by letting it look for the subclasses in the currently loaded packages. To do that, you use the Package.getPackages() method, which returns the exact packages loaded by the current class loader. Then you just need to call the find() method for each package:

    public static void find(String tosubclassname) {
        try {
            Class tosubclass = Class.forName(tosubclassname);
            Package [] pcks = Package.getPackages();
            for (int i=0;I<pcks.length;i++) {
                find(pcks[i].getName(),tosubclass);
            }
        } catch (ClassNotFoundException ex) {
            System.err.println("Class "+tosubclassname+" not found!");
        }
    }

The result of this method mainly depends on when it is called. In the case of the generic command launcher, few packages will be loaded when the find() method is called. For instance, here are the packages loaded on my NT box before calling the find() method:

package java.util.zip, Java Platform API Specification, version 1.3
package java.security, Java Platform API Specification, version 1.3
package java.io, Java Platform API Specification, version 1.3
package sun.net.www.protocol.file, Java Platform API Specification, version 1.3
package sun.net.www.protocol.jar, Java Platform API Specification, version 1.3
package sun.net.www, Java Platform API Specification, version 1.3
package java.util.jar, Java Platform API Specification, version 1.3
package sun.security.action, Java Platform API Specification, version 1.3
package java.lang, Java Platform API Specification, version 1.3
package sun.io, Java Platform API Specification, version 1.3
package java.util, Java Platform API Specification, version 1.3
package sun.misc, Java Platform API Specification, version 1.3
package java.security.cert, Java Platform API Specification, version 1.3
package java.lang.reflect, Java Platform API Specification, version 1.3
package java.net, Java Platform API Specification, version 1.3
package sun.security.util, Java Platform API Specification, version 1.3
package java.lang.ref, Java Platform API Specification, version 1.3
package sun.security.provider, Java Platform API Specification, version 1.3
package com.sun.rsajca

Since, in the command launcher, the interface and all its implementations are in the same package, you get the loaded packages after loading the class, thus allowing the search for subclasses in that package. This is the only way to find a relevant package. The complete code for the RTSI class can be found in Resources. Once you unpack the zip file, you can test the code with the following:

% java -cp classes RTSI commands.Command

Working with jar files

The code I have described works fine when the class files are present on the file system, but no longer works when the class files are in a (or several) jar file(s). In the source code, you will find a solution to that problem. You can test that capability with the following:

% java -jar RTSI.jar commands.Command

Lessons learned

You have seen how to dynamically retrieve all the subclasses of a given class contained in a given package, or available on the loaded packages. This feature is useful for the design of generic programs. As shown by the command launcher example, it also helps the user.

Note from author: Some readers pointed out that this tip only detects subclasses that have a default constructor. They proposed that I use the isAssignableFrom() method from Class instead of the isInstance(). You can obtain an updated version of RTSI.java here or in the updated source code below.

Daniel Le Berre is a researcher in artificial intelligence in the Business and Technology Research Laboratory at the University of Newcastle, Australia. He has been playing with Java since late 1997. He is currently maintaining a JavaServer Pages site, SAT Live!, and an open source Java library, JSAT, dedicated to one of his research interests.

Learn more about this topic

Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more