跨不同类加载器覆盖默认访问器方法会破坏多态性

时间:2010-10-30 21:54:17

标签: java inheritance polymorphism classloader

在尝试使用默认访问者(例如:void run())覆盖方法时,我遇到了一种奇怪的行为。 根据Java规范,如果类属于同一个包,则类可以使用或覆盖基类的默认成员。 当所有类从同一个类加载器加载时,一切正常。 但是,如果我尝试从单独的类加载器加载子类,则多态性不起作用。

以下是样本:

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:应该在单独的文件夹中。否则classloader将是相同的

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

输出:

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

然后我调用Outside#run()我得到Base#run()(输出中的“错误”)。思考工作正常。

什么错了?或者是预期的行为? 我能以某种方式解决这个问题吗?

3 个答案:

答案 0 :(得分:5)

来自Java Virtual Machine Specification

  

5.3创建和加载
  ...
  在运行时,类或接口是   不是单靠它的名字决定的,而是   一对:它的完全限定名称   及其定义的类加载器。每   这样的类或接口属于a   单个运行时包。运行时   类或接口的包是   由包名和   定义类的类加载器或   接口。   

<小时/>    5.4.4访问控制
  ...
  字段或方法R 可访问到类   或者界面D当且仅当有任何一个   满足以下条件:

     
      
  • ...
  •   
  • R是protected或包私有(即,public也不是   protectedprivate),是   由一个类在同一个声明   运行时包为D。
  •   

答案 1 :(得分:2)

Java语言规范要求类只能覆盖它可以访问的方法。如果超类方法不可访问,则会被遮蔽而不是覆盖。

反射“有效”,因为你问Outside.class的run方法。如果你问Base.class,你会得到超级实现:

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

您可以通过执行以下操作验证该方法是否无法访问:

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

那么,为什么该方法无法访问?我并不完全确定,但我怀疑,就像由不同类加载器加载的同名类导致不同的运行时类一样,由不同类加载器加载的同名命令包会导致不同的运行时包。

修改:实际上,反射API说它是相同的包:

    Base.class.getPackage() == p.getClass().getPackage() // true

答案 2 :(得分:1)

我找到了(hack)方法在主类加载器中加载外部类,所以这个问题就消失了。

以字节形式读取一个类并调用受保护的ClassLoader#defineClass方法。

代码:

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();

按预期输出ok. outside