views:

153

answers:

5

How do I call a method of a class dynamically + conditionally?
(Class is eventually not in classpath)

Let's say, I need the class NimbusLookAndFeel, but on some systems it's not available (i.e. OpenJDK-6).

So I must be able to:

  • Get to know it that class is available (at runtime),
  • If it's not the case, skip the whole thing.
  • How do I manage to override a method of a dynamically-loaded class
    (thus creating an anonymous inner sub-class of it)?

Code example

public static void setNimbusUI(final IMethod<UIDefaults> method)
    throws UnsupportedLookAndFeelException {

  // NimbusLookAndFeel may be now available
  UIManager.setLookAndFeel(new NimbusLookAndFeel() {

    @Override
    public UIDefaults getDefaults() {
      UIDefaults ret = super.getDefaults();
      method.perform(ret);
      return ret;
    }

  });
}

EDIT:
Now I edited my code, as it was suggested, to intercept NoClassDefFoundError using try-catch. It fails. I don't know, if it's OpenJDK's fault. I get InvocationTargetException, caused by NoClassDefFoundError. Funny, that I can't catch InvocationTargetException: It's thrown anyway.

EDIT2::
Cause found: I was wrapping SwingUtilities.invokeAndWait(...) around the tested method, and that very invokeAndWait call throws NoClassDefFoundError when loading Nimbus fails.

EDIT3::
Can anyone please clarify where NoClassDefFoundError can occur at all? Because it seems that it's always the calling method, not the actual method which uses the non-existing class.

A: 

You can use Class class to do that.

I.E.:

Class c = Class.forName("your.package.YourClass");

The sentence above will throw a ClassNotFoundException if not found on current classpath. If the exception is not thrown, then you can use newInstance() method in c to create objects of your.package.YourClass class. If you need to call a specific constructor, you can use getConstructors method to get one and use it to create a new instance.

Pablo Santa Cruz
I was thinking about that, but how do I manage overriding a method of a dynamically-loaded class (thus creating an anonymous inner sub-class of it)?
java.is.for.desktop
I see... Good question. Let me think...
Pablo Santa Cruz
+1  A: 

Use BCEL to generate your dynamic subclass on the fly.

http://jakarta.apache.org/bcel/manual.html

Kirk Woll
Good idea, but isn't it possible in a more simple way?
java.is.for.desktop
I don't think so. The closest thing built into the JDK is the Proxy class, but that won't be a subclass so doesn't help. Ultimately, a new Class has to be created on the fly, which means generating the bytecode of that class, and loading it through a ClassLoader. Java doesn't provide many options for this without a third party library such as BCEL.
Kirk Woll
Yes, proxies are sadly only there for implementing interfaces.
java.is.for.desktop
A: 

Erm, can't you put the class you want to extend into the compile time class path, write your subclass as usual, and at runtime, explicitly trigger loading the subclass, and handle any exception thrown by the linker that indicates that the superclass is missing?

meriton
+1  A: 

Get to know it that class is available (at runtime)
Put the usage in a try block ...

If it's not the case, skip the whole thing
... and leave the catch block empty (code smell?!).

How do I manage to override a method of a dynamically-loaded class
Just do it and make sure the compile-time dependency is satisfied. You are mixing things up here. Overriding takes place at compile time while class loading is a runtime thing.

For completeness, every class you write is dynamically loaded by the runtime environment when it is required.

So your code may look like:

public static void setNimbusUI(final IMethod<UIDefaults> method)
    throws UnsupportedLookAndFeelException {

    try {
        // NimbusLookAndFeel may be now available
        UIManager.setLookAndFeel(new NimbusLookAndFeel() {

            @Override
            public UIDefaults getDefaults() {
                final UIDefaults defaults = super.getDefaults();
                method.perform(defaults);
                return defaults;
            }

        });
   } catch (NoClassDefFoundError e) {
       throw new UnsupportedLookAndFeelException(e);
   }
}
Willi
Good idea! I was not only executing, but also **compiling** the code under `OpenJDK 6`, so I got confused by compile-time errors.
java.is.for.desktop
... of course, the optimal solution would enable me to compile the eventually non-existing class optionally.
java.is.for.desktop
Assuming this code has been compiled, it won't throw a `ClassNotFoundException` but a `NoClassDefFoundError` if `NimbusLookAndFeel` is not there at runtime.
Pascal Thivent
@`Pascal Thivent`: Right! "exception ClassNotFoundException is never thrown in body of corresponding try statement".But are you sure that `NoClassDefFoundError` is thrown **inside that method**? (Not, for instance, at class' ctor, static ctor, ... or what ever...)
java.is.for.desktop
@Pascal thanks for pointing this out, fixed it
Willi
@java.is.for.desktop Did u try it?
Willi
Yes, as mentioned in the edited question, something seems to be wrong with OpenJDK-6: I get InvocationTargetException (caused by NoClassDefFoundError), which, strangely, can't be caught.
java.is.for.desktop
Ah, didn't see the new part in the question. Does the stacktrace indicate where the exception is coming from?
Willi
Now, found the cause, added to question (**EDIT2**).
java.is.for.desktop
+1  A: 

The follow code should solve your problem. The Main class simulates your main class. Class A simulates the base class you want to extend (and you have no control of). Class B is the derived class of class A. Interface C simulates "function pointer" functionality that Java does not have. Let's see the code first...

The following is class A, the class you want to extend, but have no control of:


/* src/packageA/A.java */

package packageA;

public class A {
    public A() {
    }

    public void doSomething(String s) {
        System.out.println("This is from packageA.A: " + s);
    }
}

The following is class B, the dummy derived class. Notice that, since it extends A, it must import packageA.A and class A must be available at the compile time of class B. A constructor with parameter C is essential, but implementing interface C is optional. If B implements C, you gain the convenience to call the method(s) on an instance of B directly (without reflection). In B.doSomething(), calling super.doSomething() is optional and depends on whether you want so, but calling c.doSomething() is essential (explained below):


/* src/packageB/B.java */

package packageB;

import packageA.A;
import packageC.C;

public class B extends A implements C {
    private C c;

    public B(C c) {
        super();
        this.c = c;
    }

    @Override
    public void doSomething(String s) {
        super.doSomething(s);
        c.doSomething(s);
    }
}

The following is the tricky interface C. Just put all the methods you want to override into this interface:


/* src/packageC/C.java */

package packageC;

public interface C {
    public void doSomething(String s);
}

The following is the main class:


/* src/Main.java */

import packageC.C;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) {
        doSomethingWithB("Hello");
    }

    public static void doSomethingWithB(final String t) {
        Class classB = null;
        try {
            Class classA = Class.forName("packageA.A");
            classB = Class.forName("packageB.B");
        } catch (ClassNotFoundException e) {
            System.out.println("packageA.A not found. Go without it!");
        }

        Constructor constructorB = null;
        if (classB != null) {
            try {
                constructorB = classB.getConstructor(C.class);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }

        C objectB = null;
        if (constructorB != null) {
            try {
                objectB = (C) constructorB.newInstance(new C() {
                    public void doSomething(String s) {
                        System.out.println("This is from anonymous inner class: " + t);
                    }
                });
            } catch (ClassCastException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

        if (objectB != null) {
            objectB.doSomething("World");
        }
    }
}

Why does it compile and run?
You can see that in the Main class, only packageC.C is imported, and there is no reference to packageA.A or packageB.B. If there is any, the class loader will throw an exception on platforms that don't have packageA.A when it tries to load one of them.

How does it work?
In the first Class.forName(), it checks whether class A is available on the platform. If it is, ask the class loader to load class B, and store the resulting Class object in classB. Otherwise, ClassNotFoundException is thrown by Class.forName(), and the program goes without class A.

Then, if classB is not null, get the constructor of class B that accepts a single C object as parameter. Store the Constructor object in constructorB.

Then, if constructorB is not null, invoke constructorB.newInstance() to create a B object. Since there is a C object as parameter, you can create an anonymous class that implements interface C and pass the instance as the parameter value. This is just like what you do when you create an anonymous MouseListener.

(In fact, you don't have to separate the above try blocks. It is done so to make it clear what I am doing.)

If you made B implements C, you can cast the B object as a C reference at this time, and then you can call the overridden methods directly (without reflection).

What if class A does not have a "no parameter constructor"?
Just add the required parameters to class B, like public B(int extraParam, C c), and call super(extraParam) instead of super(). When creating the constructorB, also add the extra parameter, like classB.getConstructor(Integer.TYPE, C.class).

What happens to String s and String t?
t is used by the anonymous class directly. When objectB.doSomething("World"); is called, "World" is the s supplied to class B. Since super can't be used in the anonymous class (for obvious reasons), all the code that use super are placed in class B.

What if I want to refer to super multiple times?
Just write a template in B.doSomething() like this:


    @Override
    public void doSomething(String s) {
        super.doSomething1(s);
        c.doSomethingAfter1(s);
        super.doSomething2(s);
        c.doSomethingAfter2(s);
    }

Of course, you have to modify interface C to include doSomethingAfter1() and doSomethingAfter2().

How to compile and run the code?

$ mkdir classes
$
$
$
$ javac -cp src -d classes src/Main.java
$ java -cp classes Main
packageA.A not found. Go without it!
$
$
$
$ javac -cp src -d classes src/packageB/B.java
$ java -cp classes Main
This is from packageA.A: World
This is from anonymous inner class: Hello

In the first run, the class packageB.B is not compiled (since Main.java does not have any reference to it). In the second run, the class is explicitly compiled, and thus you get the result you expected.

To help you fitting my solution to your problem, here is a link to the correct way to set the Nimbus Look and Feel:

Nimbus Look and Feel

Siu Ching Pong - Asuka Kenji
Wow! Many thanks for such a detailed answer!
java.is.for.desktop
Would you please accept my answer then? It could be done by putting a tick (check mark) on the left hand side of my answer. Thank you!
Siu Ching Pong - Asuka Kenji