Переопределение метода доступа по умолчанию для разных загрузчиков классов нарушает полиморфизм - PullRequest
7 голосов
/ 31 октября 2010

Я сталкиваюсь со странным поведением при попытке переопределить метод с помощью стандартного доступа (например: 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: должен находиться в отдельной папке.в противном случае загрузчик классов будет таким же

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 ]

5 голосов
/ 31 октября 2010

С Спецификация виртуальной машины Java :

5.3 Создание и загрузка
...
Во время выполнения класс или интерфейс определяется не только его именем, но по паре: его полное имя и его определяющий загрузчик классов. каждый такой класс или интерфейс принадлежит один пакет времени выполнения . Время выполнения пакет класса или интерфейса определяется именем пакета и определение загрузчика класса или интерфейс.


5.4.4 Контроль доступа
...
Поле или метод R доступны для класса или интерфейс D, если и только если какой-либо из выполняются следующие условия:
  • ...
  • R является либо protected, либо пакетом private (то есть, ни public, ни protected или private), и является объявлено классом в том же пакет времени выполнения как D.
2 голосов
/ 31 октября 2010

Спецификация языка 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
1 голос
/ 31 октября 2010

Я нашел (взломать) способ загрузки внешнего класса в основной загрузчик классов, поэтому эта проблема исчезла.

Чтение класса в виде байтов и вызов защищенного 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 как положено.

...