在尝试使用默认访问者(例如: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()
(输出中的“错误”)。思考工作正常。
什么错了?或者是预期的行为? 我能以某种方式解决这个问题吗?
答案 0 :(得分:5)
来自Java Virtual Machine Specification:
5.3创建和加载
<小时/> 5.4.4访问控制
...
在运行时,类或接口是 不是单靠它的名字决定的,而是 一对:它的完全限定名称 及其定义的类加载器。每 这样的类或接口属于a 单个运行时包。运行时 类或接口的包是 由包名和 定义类的类加载器或 接口。
...
字段或方法R 可访问到类 或者界面D当且仅当有任何一个 满足以下条件:
- ...
- R是
protected
或包私有(即,public
也不是protected
也private
),是 由一个类在同一个声明 运行时包为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
。