Java Class.cast () против оператора приведения

107

Когда я учился на C ++ о пороках оператора приведения в стиле C, я сначала был рад обнаружить, что в Java 5 java.lang.Classесть castметод.

Я подумал, что наконец-то у нас появился ОО-подход к кастингу.

Оказывается, Class.castэто не то же самое, что static_castв C ++. Это больше похоже reinterpret_cast. Он не будет генерировать ошибку компиляции там, где она ожидается, а вместо этого будет отложена до времени выполнения. Вот простой тестовый пример, чтобы продемонстрировать различное поведение.

package test;

import static org.junit.Assert.assertTrue;

import org.junit.Test;


public class TestCast
{
    static final class Foo
    {
    }

    static class Bar
    {
    }

    static final class BarSubclass
        extends Bar
    {
    }

    @Test
    public void test ( )
    {
        final Foo foo = new Foo( );
        final Bar bar = new Bar( );
        final BarSubclass bar_subclass = new BarSubclass( );

        {
            final Bar bar_ref = bar;
        }

        {
            // Compilation error
            final Bar bar_ref = foo;
        }
        {
            // Compilation error
            final Bar bar_ref = (Bar) foo;
        }

        try
        {
            // !!! Compiles fine, runtime exception
            Bar.class.cast( foo );
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }

        {
            final Bar bar_ref = bar_subclass;
        }

        try
        {
            // Compiles fine, runtime exception, equivalent of C++ dynamic_cast
            final BarSubclass bar_subclass_ref = (BarSubclass) bar;
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }
    }
}

Итак, это мои вопросы.

  1. Должно Class.cast()быть сослан в дженерик землю? Там у него довольно много законных применений.
  2. Должны ли компиляторы генерировать ошибки компиляции при Class.cast()использовании, а недопустимые условия могут быть определены во время компиляции?
  3. Должна ли Java предоставлять оператор приведения в качестве языковой конструкции, подобной C ++?
Александр Погребняк
источник
4
Простой ответ: (1) Где находится «Земля дженериков»? Чем это отличается от того, как сейчас используется оператор приведения? (2) Возможно. Но в 99% всего кода Java, когда-либо написанного, крайне маловероятно, что кто-либо будет использовать его, Class.cast()если во время компиляции можно определить недопустимые условия. В этом случае все, кроме вас, используют стандартный оператор приведения. (3) В Java есть оператор приведения в качестве языковой конструкции. Он не похож на C ++. Это потому, что многие языковые конструкции Java не похожи на C ++. Несмотря на внешнее сходство, Java и C ++ совершенно разные.
Дэниел Прайден,

Ответы:

117

Я только когда-либо использовал, Class.cast(Object)чтобы избежать предупреждений в "стране дженериков". Я часто вижу такие методы:

@SuppressWarnings("unchecked")
<T> T doSomething() {
    Object o;
    // snip
    return (T) o;
}

Часто лучше заменить его:

<T> T doSomething(Class<T> cls) {
    Object o;
    // snip
    return cls.cast(o);
}

Это единственный случай использования, с которым Class.cast(Object)я когда-либо сталкивался.

Что касается предупреждений компилятора: я подозреваю, что Class.cast(Object)это не особенность компилятора. Его можно было бы оптимизировать при статическом использовании (то есть, Foo.class.cast(o)а не cls.cast(o)), но я никогда не видел, чтобы кто-нибудь его использовал, что делает попытки встроить эту оптимизацию в компилятор несколько бесполезной.

сфуссенеггер
источник
8
Я думаю, что в этом случае правильным способом было бы сначала выполнить экземпляр проверки следующим образом: if (cls.isInstance (o)) {return cls.cast (o); } Если, конечно, вы не уверены, что тип будет правильным.
Puce
1
Почему второй вариант лучше? Разве первый вариант не более эффективен, потому что код вызывающего абонента все равно будет выполнять динамическое приведение?
user1944408 04
1
@ user1944408, если вы строго говорите о производительности, это может быть, хотя в значительной степени незначительно. Я бы не хотел получить ClassCastExceptions там, где нет очевидного случая. Кроме того, второй работает лучше, когда компилятор не может вывести T, например list.add (this. <String> doSomething ()) vs. list.add (doSomething (String.class))
sfussenegger
2
Но вы все равно можете получить ClassCastExceptions при вызове cls.cast (o), пока вы не получаете никаких предупреждений во время компиляции. Я предпочитаю первый вариант, но я бы использовал второй вариант, когда мне нужно, например, вернуть null, если я не могу выполнить кастинг. В этом случае я бы заключил cls.cast (o) в блок try catch. Я также согласен с вами, что это также лучше, когда компилятор не может вывести T. Спасибо за ответ.
user1944408 04
1
Я также использую второй вариант, когда мне нужно, чтобы Class <T> cls был динамическим, например, если я использую отражение, но во всех остальных случаях я предпочитаю первый. Думаю, это просто личный вкус.
user1944408 04
20

Во-первых, вам категорически не рекомендуется использовать практически любые заклинания, поэтому вам следует максимально ограничить их! Вы теряете преимущества строго типизированных функций Java во время компиляции.

В любом случае его Class.cast()следует использовать в основном при получении Classтокена через отражение. Писать более идиоматично

MyObject myObject = (MyObject) object

скорее, чем

MyObject myObject = MyObject.class.cast(object)

EDIT: ошибки во время компиляции

В целом Java выполняет проверку приведения только во время выполнения. Однако компилятор может выдать ошибку, если сможет доказать, что такое приведение не может быть успешным (например, приведение класса к другому классу, не являющемуся супертипом, и приведение окончательного типа класса к классу / интерфейсу, не входящему в его иерархию типов). Здесь, поскольку Fooи Barявляются классами, не входящими в иерархию друг друга, приведение не может быть успешным.

notnoop
источник
Я полностью согласен с тем, что приведения следует использовать экономно, поэтому наличие чего-то, что можно легко найти, будет большим преимуществом для рефакторинга / поддержки кода. Но проблема в том, что, хотя на первый взгляд Class.castкажется, что оно отвечает всем требованиям, оно создает больше проблем, чем решает.
Александр Погребняк,
16

Всегда проблематично и часто вводить в заблуждение пытаться переводить конструкции и концепции между языками. Кастинг - не исключение. В частности, потому что Java - это динамический язык, а C ++ несколько отличается.

Все приведение типов в Java, независимо от того, как вы это делаете, выполняется во время выполнения. Информация о типе хранится во время выполнения. C ++ - это немного больше смесь. Вы можете преобразовать структуру в C ++ к другой, и это просто переинтерпретация байтов, которые представляют эти структуры. Java так не работает.

Также дженерики в Java и C ++ сильно отличаются. Не беспокойтесь слишком о том, как вы делаете вещи C ++ в Java. Вам нужно научиться делать что-то на языке Java.

Cletus
источник
Не вся информация хранится во время выполнения. Как вы можете видеть из моего примера (Bar) foo, во время компиляции возникает ошибка, но Bar.class.cast(foo)этого не происходит. На мой взгляд, если он используется таким образом, он должен.
Александр Погребняк
5
@ Александр Погребняк: Тогда не делай этого! Bar.class.cast(foo)явно сообщает компилятору, что вы хотите выполнить приведение во время выполнения. Если вы хотите проверить правильность преобразования во время компиляции, единственный выбор - выполнить приведение (Bar) fooстиля.
Дэниел Прайден,
Как вы думаете, как поступить так же? поскольку java не поддерживает множественное наследование классов.
Ямур
13

Class.cast()редко когда-либо используется в коде Java. Если он используется, то обычно с типами, которые известны только во время выполнения (т. Е. Через их соответствующие Classобъекты и какой-либо параметр типа). Это действительно полезно только в коде, который использует дженерики (это также причина, по которой он не был представлен ранее).

Это не похоже на reinterpret_cast, потому что это не позволит вам нарушить систему типов во время выполнения, как и обычное приведение (т.е. вы можете нарушить параметры универсального типа, но не можете сломать «настоящие» типы).

Пороки оператора приведения в стиле C обычно не относятся к Java. Код Java, который выглядит как приведение в стиле C, больше всего похож на код dynamic_cast<>()со ссылочным типом в Java (помните: Java имеет информацию о типе времени выполнения).

Обычно сравнивать операторы приведения C ++ с приведением Java довольно сложно, поскольку в Java вы можете приводить только ссылку, и никакое преобразование никогда не происходит с объектами (с использованием этого синтаксиса можно преобразовать только примитивные значения).

Иоахим Зауэр
источник
dynamic_cast<>()со ссылочным типом.
Том Хотин - tackline
@Tom: это правильно? Мой C ++ очень ржавый, мне пришлось его повторно гуглить ;-)
Иоахим Зауэр
+1 за: «Зло оператора приведения в стиле C обычно неприменимо к Java». Совершенно верно. Я как раз собирался опубликовать именно эти слова в качестве комментария к вопросу.
Дэниел Прайден,
4

Обычно оператор приведения предпочтительнее метода приведения Class #, поскольку он более краток и может быть проанализирован компилятором, чтобы выявить явные проблемы с кодом.

Class # cast берет на себя ответственность за проверку типов во время выполнения, а не во время компиляции.

Несомненно, существуют варианты использования приведения Class #, особенно когда дело доходит до отражающих операций.

Поскольку лямбда пришла в java, мне лично нравится использовать приведение Class # с API коллекций / потоков, например, если я работаю с абстрактными типами.

Dog findMyDog(String name, Breed breed) {
    return lostAnimals.stream()
                      .filter(Dog.class::isInstance)
                      .map(Dog.class::cast)
                      .filter(dog -> dog.getName().equalsIgnoreCase(name))
                      .filter(dog -> dog.getBreed() == breed)
                      .findFirst()
                      .orElse(null);
}
Калеб
источник
3

C ++ и Java - разные языки.

Оператор приведения типа Java в C намного более ограничен, чем версия C / C ++. Фактически приведение Java похоже на C ++ dynamic_cast, если имеющийся у вас объект не может быть приведен к новому классу, вы получите исключение времени выполнения (или, если в коде достаточно информации, время компиляции). Таким образом, идея C ++ об отказе от приведений типов C не является хорошей идеей в Java.

мммммм
источник
0

В дополнение к удалению уродливых предупреждений о приведении, как уже упоминалось, Class.cast - это приведение во время выполнения, в основном используемое с универсальным приведением, из-за того, что общая информация будет удалена во время выполнения, и некоторым образом каждый общий будет считаться объектом, это приводит к тому, что выбросить раннее исключение ClassCastException.

например serviceLoder использует этот трюк при создании объектов, проверьте S p = service.cast (c.newInstance ()); это вызовет исключение приведения класса, когда SP = (S) c.newInstance (); не будет и может отображать предупреждение «Типовая безопасность: отключенное приведение от объекта к S» . (то же, что и Object P = (Object) c.newInstance ();)

-просто он проверяет, является ли приведенный объект экземпляром класса приведения, затем он будет использовать оператор приведения для приведения и скрытия предупреждения, подавляя его.

Реализация java для динамического приведения:

@SuppressWarnings("unchecked")
public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
        throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
}




    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
Амджад Абдул-Гани
источник
0

Лично я использовал это раньше для создания конвертера JSON в POJO. В случае, если JSONObject, обработанный с помощью функции, содержит массив или вложенные JSONObject (подразумевая, что данные здесь не примитивного типа или String), я пытаюсь вызвать метод установки, используя class.cast()следующим образом:

public static Object convertResponse(Class<?> clazz, JSONObject readResultObject) {
    ...
    for(Method m : clazz.getMethods()) {
        if(!m.isAnnotationPresent(convertResultIgnore.class) && 
            m.getName().toLowerCase().startsWith("set")) {
        ...
        m.invoke(returnObject,  m.getParameters()[0].getClass().cast(convertResponse(m.getParameters()[0].getType(), readResultObject.getJSONObject(key))));
    }
    ...
}

Не уверен, что это чрезвычайно полезно, но, как было сказано здесь ранее, отражение - один из очень немногих законных вариантов использования, которые class.cast()я могу придумать, по крайней мере, у вас есть еще один пример.

Wep0n
источник