Czy casting Java wprowadza narzuty? Czemu?

105

Czy jest jakiś narzut, gdy rzucamy obiekty jednego typu na inny? A może kompilator po prostu rozwiązuje wszystko i nie ma żadnych kosztów w czasie wykonywania?

Czy to sprawa ogólna, czy są różne przypadki?

Na przykład załóżmy, że mamy tablicę Object [], gdzie każdy element może mieć inny typ. Ale zawsze wiemy na pewno, że, powiedzmy, element 0 to Double, a element 1 to String. (Wiem, że to zły projekt, ale załóżmy, że musiałem to zrobić.)

Czy informacje o typach Javy są nadal dostępne w czasie wykonywania? Albo wszystko jest zapomniane po kompilacji, a jeśli zrobimy (Double) elementy [0], po prostu podążymy za wskaźnikiem i zinterpretujemy te 8 bajtów jako podwójne, cokolwiek to jest?

Nie mam pewności, jak są wykonywane typy w Javie. Jeśli masz jakieś rekomendacje dotyczące książek lub artykułu, to też dziękuję.

Phil
źródło
Wydajność instancji i castingu jest całkiem dobra. Opublikowałem trochę czasu w Java7 wokół różnych podejść do problemu tutaj: stackoverflow.com/questions/16320014/ ...
Wheezil
To drugie pytanie ma bardzo dobre odpowiedzi stackoverflow.com/questions/16741323/ ...
user454322

Odpowiedzi:

78

Istnieją 2 rodzaje odlewania:

Rzutowanie niejawne , gdy rzutujesz z typu na szerszy typ, co jest wykonywane automatycznie i nie ma narzutów:

String s = "Cast";
Object o = s; // implicit casting

Wyraźne rzucanie, gdy przechodzisz od szerszego typu do węższego. W tym przypadku musisz jawnie użyć rzutowania w ten sposób:

Object o = someObject;
String s = (String) o; // explicit casting

W tym drugim przypadku występuje obciążenie w czasie wykonywania, ponieważ oba typy muszą zostać sprawdzone, a jeśli rzutowanie nie jest możliwe, JVM musi zgłosić wyjątek ClassCastException.

Zaczerpnięte z JavaWorld: Koszt odlewania

Rzutowanie służy do konwersji między typami - w szczególności między typami referencyjnymi, dla typu operacji rzutowania, który nas tutaj interesuje.

Operacje upcast (zwane także konwersjami rozszerzającymi w specyfikacji języka Java) konwertują odwołanie do podklasy na odwołanie do klasy nadrzędnej. Ta operacja rzutowania jest zwykle automatyczna, ponieważ jest zawsze bezpieczna i może być zaimplementowana bezpośrednio przez kompilator.

Operacje downcast (zwane także konwersjami zawężającymi w specyfikacji języka Java) konwertują odwołanie do klasy nadrzędnej na odwołanie do podklasy. Ta operacja rzutowania powoduje obciążenie związane z wykonywaniem, ponieważ Java wymaga sprawdzenia rzutowania w czasie wykonywania, aby upewnić się, że jest poprawny. Jeśli obiekt, do którego istnieje odwołanie, nie jest instancją typu docelowego rzutowania ani podklasy tego typu, próba rzutowania jest niedozwolona i musi zgłosić wyjątek java.lang.ClassCastException.

Alex Ntousias
źródło
102
Ten artykuł JavaWorld ma ponad 10 lat, więc przyjmę wszelkie stwierdzenia dotyczące wydajności z bardzo dużym ziarnem twojej najlepszej soli.
skaffman
1
@skaffman, Właściwie to z przymrużeniem oka brałbym każde stwierdzenie (niezależnie od tego, czy chodzi o wykonanie, czy nie).
Pacerier,
Czy będzie tak samo, jeśli nie przypiszę rzutowanego obiektu do referencji i po prostu wywołam na nim metodę? jak((String)o).someMethodOfCastedClass()
Parth Vishvajit
4
Teraz artykuł ma prawie 20 lat. Odpowiedzi mają też wiele lat. To pytanie wymaga nowoczesnej odpowiedzi.
Raslanove
A co z typami prymitywnymi? Chodzi mi o to, czy na przykład - rzucanie z int na short powoduje podobne obciążenie?
luke 1985
44

Aby uzyskać rozsądną implementację języka Java:

Każdy obiekt ma nagłówek zawierający między innymi wskaźnik do typu środowiska wykonawczego (na przykład Doublelub String, ale nigdy nie może to być CharSequencelub AbstractList). Zakładając, że kompilator środowiska uruchomieniowego (ogólnie HotSpot w przypadku Sun'a) nie może statycznie określić typu, a wygenerowany kod maszynowy musi przeprowadzić pewne sprawdzenia.

Najpierw należy odczytać wskaźnik do typu środowiska wykonawczego. Jest to i tak konieczne do wywołania metody wirtualnej w podobnej sytuacji.

W przypadku rzutowania na typ klasy wiadomo dokładnie, ile jest superklas, dopóki nie trafisz java.lang.Object, więc typ można odczytać ze stałym przesunięciem od wskaźnika typu (właściwie pierwsza osiem w HotSpot). Ponownie jest to analogiczne do odczytu wskaźnika metody dla metody wirtualnej.

Wtedy odczytana wartość wymaga porównania z oczekiwanym statycznym typem rzutowania. W zależności od architektury zestawu instrukcji, inna instrukcja będzie musiała rozgałęzić się (lub spowodować błąd) w nieprawidłowej gałęzi. ISA, takie jak 32-bitowe ARM, mają instrukcje warunkowe i mogą mieć możliwość przejścia smutnej ścieżki przez szczęśliwą ścieżkę.

Interfejsy są trudniejsze z powodu wielokrotnego dziedziczenia interfejsu. Zwykle ostatnie dwa rzuty do interfejsów są buforowane w typie środowiska uruchomieniowego. W bardzo wczesnych dniach (ponad dziesięć lat temu) interfejsy były nieco powolne, ale to już nie ma znaczenia.

Miejmy nadzieję, że widzisz, że tego rodzaju rzeczy są w dużej mierze nieistotne dla wydajności. Twój kod źródłowy jest ważniejszy. Jeśli chodzi o wydajność, największym hitem w twoim scenariuszu mogą być chybienia w pamięci podręcznej spowodowane ściganiem wskaźników obiektów w całym miejscu (informacje o typie będą oczywiście powszechne).

Tom Hawtin - haczyk
źródło
1
interesujące - to znaczy, że dla klas nieinterfejsowych, jeśli napiszę podklasę Superclass sc = (Superclass); że kompilator (jit, tj .: czas ładowania) „statycznie” umieści przesunięcie względem obiektu w każdej z nadklasy i podklasy w ich nagłówkach „Klasa”, a następnie za pomocą prostego dodawania i porównywania będzie w stanie rozwiązać problem? - to fajnie i szybko :) Dla interfejsów nie założyłbym gorzej niż mały hashtable lub btree?
peterk
@peterk W przypadku rzutowania między klasami nie zmieniają się ani adres obiektu, ani „vtbl” (tabela wskaźników do metod, tabela hierarchii klas, pamięć podręczna interfejsu itp.). Więc rzutowanie [klasa] sprawdza typ i jeśli pasuje, nic więcej się nie dzieje.
Tom Hawtin - tackline
8

Na przykład załóżmy, że mamy tablicę Object [], gdzie każdy element może mieć inny typ. Ale zawsze wiemy na pewno, że, powiedzmy, element 0 to Double, a element 1 to String. (Wiem, że to zły projekt, ale załóżmy, że musiałem to zrobić.)

Kompilator nie zwraca uwagi na typy poszczególnych elementów tablicy. Po prostu sprawdza, czy typ każdego wyrażenia elementu można przypisać do typu elementu tablicy.

Czy informacje o typach Javy są nadal dostępne w czasie wykonywania? Albo wszystko jest zapomniane po kompilacji, a jeśli zrobimy (Double) elementy [0], po prostu podążymy za wskaźnikiem i zinterpretujemy te 8 bajtów jako podwójne, cokolwiek to jest?

Niektóre informacje są przechowywane w czasie wykonywania, ale nie statyczne typy poszczególnych elementów. Możesz to stwierdzić patrząc na format pliku klasy.

Teoretycznie jest możliwe, że kompilator JIT mógłby użyć „analizy ucieczki”, aby wyeliminować niepotrzebne sprawdzanie typu w niektórych przypisaniach. Jednak zrobienie tego w stopniu, który sugerujesz, wykraczałoby poza granice realistycznej optymalizacji. Korzyści z analizy typów poszczególnych elementów byłyby zbyt małe.

Poza tym ludzie i tak nie powinni pisać takiego kodu aplikacji.

Stephen C.
źródło
1
A co z prymitywami? (float) Math.toDegrees(theta)Czy również tutaj będzie znaczący narzut?
SD,
2
Niektóre prymitywne rzuty są obciążone kosztami. To, czy jest to istotne, zależy od kontekstu.
Stephen C
6

Wywoływana jest instrukcja kodu bajtowego do wykonywania rzutowania w czasie wykonywania checkcast. Możesz zdemontować kod Java za pomocą, javapaby zobaczyć, jakie instrukcje są generowane.

W przypadku tablic Java przechowuje informacje o typie w czasie wykonywania. W większości przypadków kompilator wyłapie za Ciebie błędy typu, ale zdarzają się przypadki, w których napotkasz a ArrayStoreExceptionpodczas próby zapisania obiektu w tablicy, ale typ nie pasuje (a kompilator go nie przechwycił) . Specyfikacja języka Java podaje następujący przykład:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpajest ważny, ponieważ ColoredPointjest podklasą Point, ale pa[0] = new Point()nie jest prawidłowy.

Jest to przeciwieństwo typów ogólnych, w przypadku których nie ma informacji o typie przechowywanych w czasie wykonywania. W checkcastrazie potrzeby kompilator wstawia instrukcje.

Ta różnica w wpisywaniu typów ogólnych i tablic powoduje, że często nie nadaje się do mieszania tablic i typów ogólnych.

JesperE
źródło
2

W teorii wprowadzono narzut. Jednak nowoczesne maszyny JVM są inteligentne. Każda implementacja jest inna, ale nie jest nierozsądne założenie, że może istnieć implementacja zoptymalizowana przez JIT w celu wykluczenia rzutowania sprawdzeń, kiedy mogłaby zagwarantować, że nigdy nie będzie konfliktu. Jeśli chodzi o konkretne maszyny JVM, które to oferują, nie mogę ci powiedzieć. Muszę przyznać, że sam chciałbym poznać specyfikę optymalizacji JIT, ale powinni się nimi martwić inżynierowie JVM.

Morał tej historii polega na napisaniu najpierw zrozumiałego kodu. Jeśli doświadczasz spowolnień, przedstaw profil i zidentyfikuj problem. Szanse są dobre, że nie będzie to spowodowane rzucaniem. Nigdy nie poświęcaj czystego, bezpiecznego kodu, próbując go zoptymalizować, AŻ NIE WIESZ, ŻE POTRZEBUJESZ.

HesNotTheStig
źródło