views:

88

answers:

3

Hello.

I come across to a strange behavior while trying to override a method with default accessor (ex: void run()). According to Java spec, a class can use or override default members of base class if classes belongs to the same package. Everything works correctly while all classes loaded from the same classloader. But if I try to load a subclass from separate classloader then polymorphism don't work.

Here is sample:

App.java:

import java.net.*;
import java.lang.reflect.Method;

public class App {
    public static class Base {
        void run() {
            System.out.println("error");
        }
    }
    public static class Inside extends Base {
        @Override
        void run() {
            System.out.println("ok. inside");
        }
    }
    public static void main(String[] args) throws Exception {
        {
            Base p = (Base) Class.forName(Inside.class.getName()).newInstance();
            System.out.println(p.getClass());
            p.run();
        } {
            // path to Outside.class
            URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") };
            URLClassLoader ucl = URLClassLoader.newInstance(url);
            final Base p = (Base) ucl.loadClass("Outside").newInstance();
            System.out.println(p.getClass());
            p.run();
            // try reflection
            Method m = p.getClass().getDeclaredMethod("run");
            m.setAccessible(true);
            m.invoke(p);
        }
    }
}

Outside.java: should be in separate folder. otherwise classloader will be the same

public class Outside extends App.Base {
    @Override
    void run() {
        System.out.println("ok. outside");
    }
}

The output:

class App$Inside
ok. inside
class Outside
error
ok. outside

So then I call Outside#run() I got Base#run() ("error" in output). Reflections works correctly.

Whats wrong? Or is it expected behavior? Can I go around this problem somehow?

+2  A: 

The Java Language Specification mandates that a class can only override methods that it can access. If the super class method is not accessible, it is shadowed rather than overridden.

Reflection "works" because you ask Outside.class for its run method. If you ask Base.class instead, you'll get the super implementation:

        Method m = Base.class.getDeclaredMethod("run");
        m.setAccessible(true);
        m.invoke(p);

You can verify that the method is deemed inaccessible by doing:

public class Outside extends Base {
    @Override
    public void run() {
        System.out.println("Outside.");
        super.run(); // throws an IllegalAccessError
    }
}

So, why is the method not accessible? I am not totally sure, but I suspect that just like equally named classes loaded by different class loaders result in different runtime classes, equally named packages loaded by different class loaders result in different runtime packages.

Edit: Actually, the reflection API says that it's the same package:

    Base.class.getPackage() == p.getClass().getPackage() // true
meriton
Thank you for answer. Your suspection seems reasonably. Maybe JVM consider the packages are different for different classloaders.
mart
Is the way to avoid this? Maybe it is possible to add a class to the main classloader?
mart
+3  A: 

From Java Virtual Machine Specification:

5.3 Creation and Loading
...
At run time, a class or interface is determined not by its name alone, but by a pair: its fully qualified name and its defining class loader. Each such class or interface belongs to a single runtime package. The runtime package of a class or interface is determined by the package name and defining class loader of the class or interface.


5.4.4 Access Control
...
A field or method R is accessible to a class or interface D if and only if any of the following conditions is true:

  • ...
  • R is either protected or package private (that is, neither public nor protected nor private), and is declared by a class in the same runtime package as D.
axtavt
Thank you. Now the problem is clear.
mart
Is there any way to add external class to main classloader?
mart
@mart: Actually, you can call protected method `URLClassLoader.addURL()` via reflection (with `setAccessible(true)`), and it works as desired, but it's definitely a hack. However, you may write your own classloader where adding resources at runtime would be a legitimate behaviour, and use it to load all these classes.
axtavt
A: 

I found the (hack) way to load external class in main classloader so this problem is gone.

Read a class as bytes and invoke protected ClassLoader#defineClass method.

code:

URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") };
URLClassLoader ucl = URLClassLoader.newInstance(url);

InputStream is = ucl.getResourceAsStream("Outside.class");
byte[] bytes = new byte[is.available()];
is.read(bytes);
Method m = ClassLoader.class.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class, int.class, int.class });
m.setAccessible(true);
Class<Base> outsideClass = (Class<Base>) m.invoke(Base.class.getClassLoader(), "Outside", bytes, 0, bytes.length);

Base p = outsideClass.newInstance();
System.out.println(p.getClass());
p.run();

outputs ok. outside as expected.

mart