Статьи

Ковариантный тип возврата Abyssal

Несколько месяцев назад я работал в проекте для клиентов, где мы строили большую систему продаж 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, но недостающие аннотации все еще остаются проблема. И наверняка будут еще несколько забавных проблем в этой области.

Когда вы смотрите в пропасть,
пропасть также смотрит в вас.
Фридрих Ницше

Ресурсы