Java: How do I override a method of a class dynamically (class is eventually NOT in classpath)?
Solution 1
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);
}
}
Solution 2
Use BCEL to generate your dynamic subclass on the fly.
http://jakarta.apache.org/bcel/manual.html
Solution 3
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:
java.is.for.desktop
Updated on July 20, 2022Comments
-
java.is.for.desktop almost 2 years
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 interceptNoClassDefFoundError
using try-catch. It fails. I don't know, if it's OpenJDK's fault. I getInvocationTargetException
, caused byNoClassDefFoundError
. Funny, that I can't catchInvocationTargetException
: It's thrown anyway.EDIT2::
Cause found: I was wrappingSwingUtilities.invokeAndWait(...)
around the tested method, and that veryinvokeAndWait
call throwsNoClassDefFoundError
when loading Nimbus fails.EDIT3::
Can anyone please clarify whereNoClassDefFoundError
can occur at all? Because it seems that it's always the calling method, not the actual method which uses the non-existing class. -
java.is.for.desktop almost 14 yearsI 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 almost 14 yearsGood idea, but isn't it possible in a more simple way?
-
Kirk Woll almost 14 yearsI 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.
-
java.is.for.desktop almost 14 yearsGood 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 almost 14 yearsYes, proxies are sadly only there for implementing interfaces.
-
java.is.for.desktop almost 14 years... of course, the optimal solution would enable me to compile the eventually non-existing class optionally.
-
Devanshu Mevada almost 14 yearsAssuming this code has been compiled, it won't throw a
ClassNotFoundException
but aNoClassDefFoundError
ifNimbusLookAndFeel
is not there at runtime. -
java.is.for.desktop almost 14 years@
Pascal Thivent
: Right! "exception ClassNotFoundException is never thrown in body of corresponding try statement". But are you sure thatNoClassDefFoundError
is thrown inside that method? (Not, for instance, at class' ctor, static ctor, ... or what ever...) -
whiskeysierra almost 14 years@java.is.for.desktop Did u try it?
-
java.is.for.desktop almost 14 yearsYes, 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.
-
whiskeysierra almost 14 yearsAh, didn't see the new part in the question. Does the stacktrace indicate where the exception is coming from?
-
java.is.for.desktop almost 14 yearsNow, found the cause, added to question (EDIT2).