Несколько месяцев назад я работал в проекте для клиентов, где мы строили большую систему продаж smartclient для немецкой страховой компании. Мы использовали Riena в качестве среды приложения, затмение для разработки и Java 6 в качестве платформы. Помимо обычных проблем это было довольно забавно, но однажды мы наткнулись на проблему, связанную с ковариантными типами возвращаемых данных. А что это? Начиная с Java 1.5, вы можете использовать более конкретный тип возвращаемого значения при переопределении метода, поэтому вы можете избежать неприятных приведений в иерархиях классов. Например: у нас есть класс A
с методом, getValue()
который возвращает объект типа X
:
public class X {
...
}
public class A {
private X value;
public X getValue() {
return value;
}
}
Итак, если я наследую класс B
от A
, я могу переопределить getValue()
и использовать подкласс X в качестве возвращаемого типа:
public class Y extends X {
...
}
public class B {
...
@Override
public Y getValue() {
return value;
}
}
Это нормально, так как Y
«это» X
. Так какой в этом смысл? Проблема, о которой мы споткнулись, поразит вас, если вы будете использовать бин-самоанализ. Я не нуждаюсь в вонючем бобовом самоанализе! В самом деле? Вы когда-нибудь использовали привязку данных? Ах, так ты ?
Если я использую боб-самоанализ , чтобы получить свойство value
из объекта типа B
, и попросить его типа, я буду получать … X
. Чего-чего? Но .. это Y
?!? Да, но это то, что происходит. Есть некоторые открытые проблемы в этой области. У меня никогда не было проблем с дженериками, так когда это вас укусит? Скажем, Y extends X
со свойством name
:
public class Y extends X {
private String name;
public String getName() {...}
public void setName(String name) {...}
}
И теперь вы используете привязку данных вместе с этим свойством:
B b = ...
ridget.bind(b, "value.name")
Привязки данных видов применения боба-самоанализ , чтобы получить собственность value
от B
, и от типа value
он пытается получить свойство name
. И вот в этот момент ошибка вступает в стадию: из-за проблемы, описанной выше, bean-introspection получает X
тип value
. Но так как свойство name
не определено в X
(но в Y
), вы получите забавное исключение, говорящее, что такого свойства нет name
?
Но если я позвоню B.getValue()
, я получу Y
. Ну, по крайней мере, компилятор не будет жаловаться, если я отношусь к нему как к Y
… и я могу вызывать методы, определенные Y
как, например getName()
. Так это какая-то магия компилятора? Ну, там действительно есть методY getValue()
… но есть и метод X getValue()
! Если вы позвоните getMethods()
в класс B
, вы получите:
public X B.getValue()
public Y B.getValue()
Что за…? Откуда этот метод X getValue()
??? Это было сгенерировано компилятором. Ах …: — | Но почему? Целые тематические ковариантные типы возвращаемых данных придумали обобщения … и стирание типов, необходимое для совместимости. Да нет понятно. Итак, мы должны копать немного глубже здесь. Допустим, наш класс A
был бы определен с использованием обобщений:
public class A <T extends X> {
private T value;
public T getValue() {
return value;
}
}
public class B extends A<Y> {}
Если вы используете рефлексию для получения методов от A
и B
, у них обоих будет ОДИН метод X getValue()
. Ведь я ожидал, B
что у меня будет метод Y getValue()
? Так что же происходит, если я позвоню getValue()
и отнесусь к результату как Y
?!? Вот и весь сахар компилятора, значит: компилятор выполняет всю эту проверку типов, оценивая обобщенную информацию и вставляя приведения в ваш код, где это необходимо. Посмотрите на следующий фрагмент:
public static void main(String[] args) {
B b = new B();
Y y = b.getValue();
}
На уровне байт-кода X getValue()
вызывается (только) метод , и результат X
приводится к Y
:
public static void main(java.lang.String[] args);
0 new B [29]
3 dup
4 invokespecial B() [31]
7 astore_1 [b]
8 aload_1 [b]
9 invokevirtual B.getValue() : X [174] // X getValue() called
12 checkcast Y [133] // cast to Y
15 astore_2 [y]
16 return
Во время выполнения вся обобщенная информация стирается, и используется только необработанный тип X
. Это сделано для совместимости. То же самое, если вы используете коллекции: если вы определите List <String> в вашем источнике, компилятор преобразует это в нетипизированный список объектов. Так почему тип нашего универсального типа T
в классе был A
скомпилирован X
, а не в Object
? Это потому, что мы определили нижнюю границу нашего типа T
:
public class A <T extends X> {
...
Коллекции, такие как List, не определяют нижнюю границу, поэтому необработанным типом является Object:
public interface List<T> ...
До сих пор вы даже не замечаете все это; Вы просто используете это, и это кажется вполне естественным. Но все изменится, если мы переопределим getValue()
в B
:
public class B extends A<Y> {
@Override
public Y getValue() {
...
}
}
Теперь компилятор создает Y getValue()
ожидаемый метод , который переопределяет код суперкласса A
. Для языка Java все хорошо. Но с точки зрения JVM Y getValue()
это нечто иное, чем X getValue()
. Поэтому, если вы назначите объект типа B
для ссылки на тип A
и вызовете getValue()
, JVM будет искать метод, X getValue()
который определен в A
:
B b = new B();
A ba = b;
ba.getValue(); // -> JVM wants to invoke X getValue()
Так что здесь есть пробел. А что делает наш модный компилятор? Он исправляет этот недостаток, используя синтетический метод X getValue()
:
public class B extends A {
...
public Y getValue();
0 aload_0 [this]
...
public bridge synthetic X getValue();
0 aload_0 [this]
1 invokevirtual B.getValue() : Y [21] // delegate to Y getValue()
4 areturn
Line numbers:
[pc: 0, line: 1]
}
Это метод моста. В этом случае это всего лишь делегат нашего переопределенного метода Y getValue()
. Так что здесь у нас есть два метода getValue()
. (Между прочим: ‘bridge’ и ‘синтетический’ — оба атрибута метода, которые могут быть запрошены с помощью отражения.) Этот предмет становится более понятным, если вы подумаете о переопределении метода с общими параметрами:
public class A <T extends X> {
public void setValue(T value) {
....
}
...
}
public class B extends A<Y> {
@Override
public void setValue(Y value) {
...
}
}
Итак, снова компилятор правильно создает метод с подписью setValue(Y)
для метода. Опять же, это будет метод, отличный от того, который определен в A
, поэтому для заполнения пробела вставляется метод моста:
public class B extends A {
...
public void setValue(Y value);
0 aload_0 [this]
...
public bridge synthetic void setValue(X arg0);
0 aload_0 [this]
1 aload_1 [arg0]
2 checkcast Y [21] // cast to Y
5 invokevirtual B.setValue(Y) : void [23] // call setValue(Y)
8 return
Line numbers:
[pc: 0, line: 1]
}
Обычно вам не нужно даже думать об этом, потому что компилятор управляет всем этим для вас и выдает предупреждения или даже ошибки, если вы уходите с пути безопасности типов.
Но, несмотря на это, есть еще некоторые подводные камни, ожидающие. Одно замечание: в API отражения не существует определенного порядка (например getMethods()
) для доставки методов класса. Средства: в зависимости от того, когда и как запрашиваются методы, вы получите либо первый, X getValue()
либо Y getValue()
первый! И здесь у нас есть ошибка JDK, входящая в стадию: bean-introspection не обращает на это внимания, поэтому, если вы запросите тип свойства value
в классе, B
вы получите либо X
ИЛИY
! У нас была именно такая ситуация в проекте заказчика. Когда мы запустили приложение из нашей IDE, все было хорошо. Но в развернутом приложении мы получили ошибку привязки данных, описанную выше: — /
Но таких забавных проблем еще больше. И не в теории, мы получили следующую проблему в том же проекте. Следующий сценарий: мы хотели облегчить некоторые вещи, чтобы избавиться от кода котельной плиты. Например, вместо выполнения асинхронного выполнения программы с использованием потоков мы оценили решение на основе аннотаций. Значит: вы помечаете метод как@Async
и он будет выполняться асинхронно, пока цикл событий пользовательского интерфейса все еще работает. Мы использовали (Class-) прокси для перехвата, который анализировал аннотации и выполнял асинхронное выполнение при необходимости. Теперь давайте возьмем наш пример и отметим переопределенный метод getValue()
как @Async
:
public class B extends A<Y> {
@Override
@Async
public Y getValue() {
...
}
}
Теперь проблема. Иногда это выполнялось асинхронно, иногда нет: — Причина была в том, что аннотация иногда присутствовала, а иногда нет?!? Если вы думаете о причине мостовых методов, вы, возможно, уже знаете ответ. Это зависит от того, КАК вы звоните getValue()
. Посмотри:
7: B b = new B();
8: A ba = b;
9:
10: b.getValue(); // Executed asynchronously.
11: ba.getValue(); // In this case not 🙁
Опять же, мостовые методы являются причиной проблемы. Если вы установите точку останова в переопределенном методе Y getValue()
в классе B
, вы получите следующий стек выполнения в отладчике для вызова b.getValue()
:
Thread [main] (Suspended)
B.getValue() line: 7
BridgeMethodTest.main(String[]) line: 10
При втором вызове — ba.getValue () — это выглядит так:
Thread [main] (Suspended)
B.getValue() line: 7
B.getValue() line: 1 // <- bridge-method X getValue()
BridgeMethodTest.main(String[]) line: 11
Таким образом, в вызове ba.getValue()
мы попали в метод моста. Подумайте об этом: для компилятора объект ba
является A
, и в A
методе X getValue()
определен, так что именно это использует компилятор -> наш метод моста. И поскольку это синтетический метод, который не имеет никакого представления исходного кода, строка: 1 показана как информация о номере строки … что выглядит довольно любопытно, поскольку в нашей исходной строке ничего нет. Хорошо, а как насчет иногда отсутствующей аннотации? Опять же, это ошибка JDK: когда компилятор генерирует метод bridge, он не копирует аннотации из исходного метода, что означает: метод bridge не аннотируется. Таким образом, наш прокси не найдет никакой аннотации в этом случае и, следовательно, не выполнит вызов асинхронно: — /
Вывод
В конце концов, вы вряд ли попадете в эти ловушки, так что вам не нужно слишком о них заботиться. Но очень полезно знать эти странности … по крайней мере, чтобы понять, почему отладчик останавливается в строке 1 ? Кстати, ошибка в интроспекции бина была исправлена в Java 7, но недостающие аннотации все еще остаются проблема. И наверняка будут еще несколько забавных проблем в этой области.
Когда вы смотрите в пропасть,
пропасть также смотрит в вас.
Фридрих Ницше
Ресурсы