Java Class.cast () a operator rzutowania

107

Podczas moich dni w C ++ uczono mnie o złu operatora rzutowania w stylu C, na początku z przyjemnością odkryłem, że w Javie 5 java.lang.Classnabyłem castmetodę.

Pomyślałem, że wreszcie mamy OO sposób radzenia sobie z castingami.

Okazuje się, że Class.castto nie to samo, co static_castw C ++. To jest bardziej jak reinterpret_cast. Nie wygeneruje błędu kompilacji tam, gdzie jest oczekiwany, a zamiast tego opóźni działanie. Oto prosty przypadek testowy pokazujący różne zachowania.

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 );
        }
    }
}

Więc to są moje pytania.

  1. Powinien Class.cast()zostać wygnany do ziemi Generics? Tam ma sporo legalnych zastosowań.
  2. Czy kompilatory powinny generować błędy kompilacji, gdy Class.cast()są używane i można określić niedozwolone warunki w czasie kompilacji?
  3. Czy Java powinna udostępniać operator rzutowania jako konstrukcję języka podobną do C ++?
Alexander Pogrebnyak
źródło
4
Prosta odpowiedź: (1) Gdzie jest „ziemia generyczna”? Czym się to różni od tego, jak obecnie używany jest operator rzutowania? (2) Prawdopodobnie. Ale w 99% całego kiedykolwiek napisanego kodu Java jest bardzo mało prawdopodobne, aby ktokolwiek użył, Class.cast()gdy w czasie kompilacji można określić niedozwolone warunki. W takim przypadku wszyscy oprócz Ciebie używają tylko standardowego operatora rzutowania. (3) Java ma operator rzutowania jako konstrukcję językową. Nie jest podobny do C ++. Dzieje się tak, ponieważ wiele konstrukcji języka Java nie jest podobnych do C ++. Pomimo powierzchownych podobieństw, Java i C ++ są zupełnie inne.
Daniel Pryden,

Odpowiedzi:

117

Zawsze starałem Class.cast(Object)się unikać ostrzeżeń w „krainie leków generycznych”. Często widzę metody, które robią takie rzeczy:

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

Często najlepiej jest go zastąpić:

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

To jedyny przypadek użycia, z Class.cast(Object)jakim kiedykolwiek się spotkałem.

Odnośnie ostrzeżeń kompilatora: Podejrzewam, że Class.cast(Object)nie jest to szczególne dla kompilatora. Można go zoptymalizować, gdy jest używany statycznie (tj. Foo.class.cast(o)Zamiast cls.cast(o)), ale nigdy nie widziałem nikogo, kto go używa - co sprawia, że ​​wysiłek związany z wbudowaniem tej optymalizacji w kompilator jest nieco bezwartościowy.

sfussenegger
źródło
8
Myślę, że w tym przypadku właściwym sposobem byłoby wykonanie najpierw sprawdzenia w ten sposób: if (cls.isInstance (o)) {return cls.cast (o); } Oczywiście z wyjątkiem sytuacji, gdy jesteś pewien, że typ będzie prawidłowy.
Puce
1
Dlaczego drugi wariant jest lepszy? Czy pierwszy wariant nie jest bardziej wydajny, ponieważ kod dzwoniącego i tak wykonywałby dynamiczne rzutowanie?
user1944408
1
@ user1944408, o ile mówisz ściśle o wydajności, może to być, chociaż prawie nieistotne. Nie spodobałbym się jednak pomysłowi uzyskania ClassCastExceptions tam, gdzie nie ma oczywistego przypadku. Dodatkowo, drugi działa lepiej gdzie kompilator nie można wywnioskować, T, np list.add (to <String> doSomething ().) Vs. list.add (doSomething (String.class))
sfussenegger
2
Ale nadal możesz uzyskać ClassCastExceptions, gdy wywołasz cls.cast (o), gdy nie otrzymujesz żadnych ostrzeżeń w czasie kompilacji. Wolę pierwszy wariant, ale użyłbym drugiego wariantu, gdy potrzebuję na przykład zwrócić wartość null, jeśli nie mogę wykonać rzutowania. W takim przypadku zawinąłbym cls.cast (o) w bloku try catch. Zgadzam się też z Tobą, że jest to również lepsze, gdy kompilator nie może wywnioskować T. Dzięki za odpowiedź.
user1944408
1
Używam również drugiego wariantu, gdy potrzebuję cls Class <T>, aby były dynamiczne, na przykład jeśli używam odbicia, ale we wszystkich innych przypadkach wolę pierwszy. Myślę, że to tylko osobisty gust.
user1944408
20

Po pierwsze, zdecydowanie odradza się wykonywanie prawie każdego rzutu, więc powinieneś go jak najbardziej ograniczyć! Tracisz korzyści płynące z silnie wpisywanych w czasie kompilacji funkcji języka Java.

W każdym razie Class.cast()powinno być używane głównie podczas pobierania Classtokena przez odbicie. Pisanie jest bardziej idiomatyczne

MyObject myObject = (MyObject) object

zamiast

MyObject myObject = MyObject.class.cast(object)

EDYCJA: błędy w czasie kompilacji

Ogólnie rzecz biorąc, Java sprawdza rzutowanie tylko w czasie wykonywania. Jednak kompilator może zgłosić błąd, jeśli może udowodnić, że takie rzutowanie nigdy się nie powiedzie (np. Rzutowanie klasy na inną klasę, która nie jest nadtypem i rzutowanie ostatecznego typu klasy na klasę / interfejs, który nie znajduje się w jego hierarchii typów). Tutaj od Fooi Barsą klasy, które nie są w każdej innej hierarchii, obsada nie może się udać.

notnoop
źródło
Całkowicie zgadzam się, że rzutowanie powinno być używane oszczędnie, dlatego posiadanie czegoś, co można łatwo przeszukiwać, byłoby wielką korzyścią dla refaktoryzacji / utrzymania kodu. Problem polega jednak na tym, że chociaż na pierwszy rzut oka Class.castwydaje się, że pasuje do ustawy, stwarza więcej problemów niż rozwiązuje.
Alexander Pogrebnyak
16

Próba tłumaczenia konstrukcji i pojęć między językami jest zawsze problematyczna i często wprowadza w błąd. Casting nie jest wyjątkiem. Szczególnie dlatego, że Java jest językiem dynamicznym, a C ++ jest nieco inny.

Całe przesyłanie w Javie, bez względu na to, jak to robisz, odbywa się w czasie wykonywania. Informacje o typie są przechowywane w czasie wykonywania. C ++ to trochę więcej mieszanka. Możesz rzutować strukturę w C ++ na inną i jest to po prostu reinterpretacja bajtów, które reprezentują te struktury. Java nie działa w ten sposób.

Również typy generyczne w Javie i C ++ są bardzo różne. Nie przejmuj się zbytnio tym, jak robisz rzeczy w C ++ w Javie. Musisz nauczyć się robić rzeczy na sposób Java.

cletus
źródło
Nie wszystkie informacje są przechowywane w czasie wykonywania. Jak widać na moim przykładzie (Bar) foo, generuje błąd w czasie kompilacji, ale Bar.class.cast(foo)tak nie jest. Moim zdaniem, jeśli jest używany w ten sposób, powinien.
Alexander Pogrebnyak
5
@Alexander Pogrebnyak: Nie rób tego! Bar.class.cast(foo)jawnie informuje kompilator, że chcesz wykonać rzutowanie w czasie wykonywania. Jeśli chcesz sprawdzić w czasie kompilacji poprawność rzutowania, jedynym wyborem jest wykonanie (Bar) foorzutowania stylu.
Daniel Pryden,
Jak myślisz, co można zrobić w ten sam sposób? ponieważ java nie obsługuje dziedziczenia wielu klas.
Yamur
13

Class.cast()jest rzadko używany w kodzie Java. Jeśli jest używany, to zwykle z typami, które są znane tylko w czasie wykonywania (tj. Przez ich odpowiednie Classobiekty i przez jakiś parametr typu). Jest naprawdę przydatny tylko w kodzie, który używa typów ogólnych (z tego powodu nie został wprowadzony wcześniej).

To nie podobne do reinterpret_cast, ponieważ będzie to nie pozwalają przełamać system typu w czasie wykonywania więcej niż zwykły odlew ma (czyli może pan złamać parametry typu rodzajowego, ale nie może złamać „prawdziwe” typy).

Zło operatora rzutowania w stylu C zazwyczaj nie dotyczy języka Java. Kod Java, który wygląda jak rzutowanie w stylu C, jest najbardziej podobny do kodu dynamic_cast<>()z typem referencyjnym w Javie (pamiętaj: Java ma informacje o typie środowiska wykonawczego).

Ogólnie porównywanie operatorów rzutowania w C ++ z rzutowaniem w Javie jest dość trudne, ponieważ w Javie można rzutować tylko referencje i nigdy nie zachodzi żadna konwersja obiektów (przy użyciu tej składni można konwertować tylko wartości pierwotne).

Joachim Sauer
źródło
dynamic_cast<>()z typem odniesienia.
Tom Hawtin - tackline
@Tom: czy ta edycja jest poprawna? Mój C ++ jest bardzo zardzewiały, musiałem sporo go ponownie wygooglować ;-)
Joachim Sauer
+1 za: „Zło operatora rzutowania w stylu C generalnie nie dotyczy języka Java”. Całkiem prawdziwe. Właśnie miałem zamieścić te dokładne słowa jako komentarz do pytania.
Daniel Pryden,
4

Ogólnie rzecz biorąc, operator rzutowania jest preferowany względem metody cast klasy #, ponieważ jest bardziej zwięzły i może zostać przeanalizowany przez kompilator w celu wyplucia rażących problemów z kodem.

Class # cast bierze odpowiedzialność za sprawdzanie typów w czasie wykonywania, a nie podczas kompilacji.

Z pewnością istnieją przypadki użycia rzutowania klasy #, szczególnie jeśli chodzi o operacje odblaskowe.

Odkąd lambda pojawiły się w Javie, osobiście lubię używać rzutowania klasy # z interfejsem API kolekcji / strumienia, jeśli pracuję na przykład z typami abstrakcyjnymi.

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);
}
Caleb
źródło
3

C ++ i Java to różne języki.

Operator rzutowania w stylu Java C jest znacznie bardziej ograniczony niż wersja C / C ++. W rzeczywistości rzutowanie Java jest podobne do C ++ dynamic_cast, jeśli obiekt, którego masz, nie może być rzutowany na nową klasę, otrzymasz wyjątek czasu wykonywania (lub jeśli jest wystarczająco dużo informacji w kodzie, czas kompilacji). Dlatego pomysł C ++, aby nie używać rzutów typu C, nie jest dobrym pomysłem w Javie

mmmmmm
źródło
0

Oprócz usunięcia brzydkich ostrzeżeń o rzutach, jak większość wspomniano, Class.cast jest rzutowaniem w czasie wykonywania, używanym głównie z rzutowaniem ogólnym, ponieważ ogólne informacje zostaną usunięte w czasie wykonywania i niektóre, jak każdy rodzaj będzie traktowany jako Obiekt, co prowadzi do tego, że nie zgłoś wczesny wyjątek ClassCastException.

na przykład serviceLoder używa tej sztuczki podczas tworzenia obiektów, sprawdź S p = service.cast (c.newInstance ()); spowoduje to zgłoszenie wyjątku rzutowania klasy, gdy SP = (S) c.newInstance (); nie będzie i może wyświetlać ostrzeżenie „Bezpieczeństwo typów: niezaznaczone rzutowanie z obiektu do S” `` . (tak samo jak Object P = (Object) c.newInstance ();)

- po prostu sprawdza, czy rzutowany obiekt jest instancją klasy rzutującej, a następnie użyje operatora rzutowania, aby rzucić i ukryć ostrzeżenie, pomijając je.

implementacja java dla dynamicznego przesyłania:

@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
    }
Amjad Abdul-Ghani
źródło
0

Osobiście użyłem tego wcześniej do zbudowania konwertera JSON na POJO. W przypadku, gdy obiekt JSONObject przetwarzany za pomocą funkcji zawiera tablicę lub zagnieżdżone obiekty JSONObject (co oznacza, że ​​dane tutaj nie są typu pierwotnego lub String), próbuję wywołać metodę ustawiającą, używając class.cast()w następujący sposób:

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))));
    }
    ...
}

Nie jestem pewien, czy jest to niezwykle pomocne, ale jak powiedziałem tutaj wcześniej, refleksja jest jednym z nielicznych uzasadnionych przypadków użycia, class.cast()które mogę wymyślić, przynajmniej masz teraz inny przykład.

Wep0n
źródło