Generics, type inference и casting в java

Written by elwood

Написал я тут обобщённый метод, который выглядит так:

private <T> T getParamValue(ProceedingJoinPoint joinPoint, String name) {
    if (! (joinPoint.getSignature() instanceof CodeSignature))
        throw new RuntimeException( "JoinPoint is not a method." );
    String[] parameterNames = (( CodeSignature ) joinPoint.getSignature()).getParameterNames();
    int index = -1;
    for (int i = 0; i < parameterNames.length; i++) {
        if (parameterNames[i].equals( name )) {
            index = i;
            break;
        }
    }
    if (-1 == index)
        throw new RuntimeException( String.format( "Parameter '%s' not found.", name ) );
    return ( T ) joinPoint.getArgs()[index];
}

В принципе, тело метода мало чем интересно. Интересен оператор return, который выполняет приведение типа к T. Мне захотелось разобраться, как работает этот cast и почему эта конструкция работоспособна с любым типом T. Известно, что в java оператор приведения типа может привести к ClassCastException во время выполнения, однако с другой стороны мы знаем, что джавские дженерики компилируются таким образом, что в class-файлах информации о типах не остаётся, и компилятор не может добавить в код метода инструкцию приведения типа к типу T. Значит, скорее всего, приведение типа к типу-аргументу не порождает байткод, в отличие от обычного приведения к конкретному типу, известному на момент компиляции. Эту гипотезу легко проверить, берём декомпилятор, и – да, действительно, он декомпилирует последнюю строчку как

return joinPoint.getArgs()[index];

Как мы видим, компилятор сгенерировал метод, возвращающий Object, а для того, чтобы что-то превратить в Object, не нужно выполнять checkcast (байткод операции проверки приведения типа). Так и работают обобщения в Java. Компилятор превращает переменные типов T в переменные типа Object, и при присвоении

T var = someObject;

компилятор генерирует код

Object var = someObject;

А вот когда наоборот объект обобщённого типа мы присваиваем переменной конкретного типа, то компилятор добавляет байткод checkcast:

String str = this.<String>getParamValue();

превращается в

String str = (String) this.getParamValue(); // this.getParamValue() возвращает Object

Так всё и работает. Поэтому в принципе не так уж и важно, правильно ли компилятор выведет тип при вызове getParamValue(). В любом случае будет вызов метода, возвращающего Object, и последующий каст к конкретному типу переменной (поля, аргумента функции).

Примитивные типы обрабатываются несколько особым образом. Допустим, мы вызываем наш метод и пытаемся присводить результат переменной-примитиву:

long id = getParamValue();

Кажется, что здесь может потребоваться явное указание типа T, так как long не может выступать в качестве типа-аргумента, однако компилятор достаточно умён и выполнит вывод ссылочного типа Long, соответствующего примитивному типу long, автоматически, и ещё добавит анбоксинг:

long id = ((Long) getParamValue()).longValue();

Осталось рассмотреть предупреждение компилятора “Unchecked cast: X to Y”, которое выдается при компиляции кода, преобразующего Object в обобщённый тип, либо в тип, зависящий от обобщённого типа:

List<String> list = (List<String>) map.get("list");

Теперь нам понятно, почему компилятор выдаёт это предупреждение. Инструкция checkcast добавляется в сгенерированный байткод, но она проверяет только то, что объект является списком List. Но то, что этот список был создан с типом-аргументом String, эта инструкция проверить не может. Аналогичное предупреждение мы получаем в строчке

return ( T ) joinPoint.getArgs()[index];

– и тут компилятор вообще ничем не может нам помочь, поскольку информация о T будет недоступна во время выполнения, переменная будет иметь тип Object, и мы не сможем быть уверены, что там хранится объект типа T, а не что-либо другое.

  • AtHeaven

    if (-1 == index)
    С/С++-шный рудимент, как защита от присвоения (lvalue = rvalue), т.к. rvalue = lvalue – даст ошибку компиляции при пропущенном втором =. Java же запрещает присвоение в if, поэтому смело пишу if (lvalue == rvalue). Семантически это естественнее, т.к. проверяешь значение индекса, а не -1.
    Для поиска строки в массиве просто враппер: Arrays.asList(arr).contains().

  • elwood blues

    Привычка 🙂 На самом деле мне так больше нравится, потому что мозг сам догадывается, что -1 будет сравниваться именно с index. Аналогично со строками:
    if (“my_str_const”.equals(str)) для меня выглядит удобнее по сравнению с
    if (str.equals(“my_str_const”)), потому что значимая часть выражения – сама константа – видна сразу. Особенно это заметно в старом коде, когда ещё не было switch по строкам.
    А так вы абсолютно правы, в Java это не нужно.

  • AtHeaven

    if (“my_str_const”.equals(str))

    в случае строк в яве польза от такого подхода есть, и она в том, что если str == null, то equals вернет false. в случае же if (str.equals(“my_str_const”)) будет выброшено NPE. дело не только в удобстве “для глаза”. случай if (str.equals(“my_str_const”)) описан как известный java антипаттерн. http://www.odi.ch/prog/design/newbies.php#2

  • elwood blues

    Кстати, да, и это тоже ! Я и забыл про такой прикол.