Kiedy używać języka pierwotnego vs klasa w Javie?

54

Widzę, że Java ma Boolean (klasa) vs boolean (prymityw). Podobnie istnieje liczba całkowita (klasa) vs int (pierwotna). Jaka jest najlepsza praktyka, kiedy używać prymitywnej wersji w porównaniu z klasą? Czy zasadniczo powinienem zawsze używać wersji klasowej, chyba że mam konkretny (wydajność?) Powód, aby tego nie robić? Jaki jest najczęściej akceptowany sposób korzystania z każdego z nich?

Casey Patton
źródło
Moim zdaniem to nie są klasy, to są pudełka. Są tylko tam, więc możesz używać prymitywów tam, gdzie wymagane są obiekty, np. W kolekcjach. Nie możesz dodać dwóch liczb całkowitych (możesz udawać, ale tak naprawdę Java to auto boxing | rozpakowanie wartości dla ciebie).
stonemetal

Odpowiedzi:

47

Joshua Bloch mówi w punkcie 5 „Skuteczna Java”

Lekcja jest jasna: przedkładaj prymitywy nad prymitywy w pudełkach i uważaj na niezamierzone autoboxowanie .

Jednym z dobrych zastosowań klas jest użycie ich jako typów ogólnych (w tym klas Collection, takich jak listy i mapy) lub gdy chcesz je przekształcić w inny typ bez niejawnego rzutowania (na przykład Integerklasa ma metody doubleValue()lub byteValue().

Edycja: Powodem Joshua Blocha jest:

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
         sum += i;
    }
    System.out.println(sum);
}

Ten program otrzymuje prawidłową odpowiedź, ale jest znacznie wolniejszy niż powinien, ze względu na jednoznakowy błąd typograficzny. Zmienna sumjest zadeklarowana jako Longzamiast a long, co oznacza, że ​​program konstruuje około 2 ^ 31 niepotrzebnych Longinstancji (mniej więcej jedna za każdym razem, gdy long ijest dodawana do Long sum). Zmiana deklaracji sumy z Longna longskraca czas działania z 43 do 6,8 sekundy na moim komputerze.

m3th0dman
źródło
2
Byłoby pomocne, gdybyś wymienił powody Blocha, a nie tylko zacytował jego wnioski!
vaughandroid
@Baqueta Edytowałem post. Powodem jest wydajność.
m3th0dman
To trochę jaśniejsze, dzięki. Czuję się słuszny, publikując teraz własną odpowiedź. :)
vaughandroid
Architektura Lmax może być dla Ciebie interesująca - „Trzeba było nieco sprytniejszego, aby przejść o kolejny rząd wielkości. Zespół LMAX uznał, że jest kilka pomocnych sposobów. Jednym z nich było napisanie niestandardowych implementacji zaprojektowanych kolekcji java aby być przyjaznym dla pamięci podręcznej i zachować ostrożność w przypadku śmieci. Przykładem tego jest używanie prymitywnych java longs jako kluczy skrótów ze specjalnie napisaną implementacją mapy opartą na tablicy ”.
Wygląda na to, że JIT powinien sobie z tym poradzić
deFreitas
28

Standardową praktyką jest stosowanie prymitywów, chyba że masz do czynienia z lekami generycznymi (upewnij się, że wiesz o automatycznym pakowaniu i rozpakowywaniu !).

Istnieje wiele dobrych powodów, aby przestrzegać konwencji:

1. Unikasz prostych błędów:

Istnieją pewne subtelne, nieintuicyjne przypadki, które często łapią początkujących. Nawet doświadczeni koderzy czasami wpadają w błąd i popełniają te błędy (miejmy nadzieję, że po tym debugują kod i wykrywają błąd!).

Najbardziej powszechnym błędem jest użycie a == bzamiast a.equals(b). Ludzie są przyzwyczajeni do robienia a == bprymitywów, więc łatwo to zrobić, gdy używasz opakowań obiektów.

Integer a = new Integer(2);
Integer b = new Integer(2);
if (a == b) { // Should be a.equals(b)
    // This never gets executed.
}
Integer c = Integer.valueOf(2);
Integer d = Integer.valueOf(2);
if (c == d) { // Should be a.equals(b), but happens to work with these particular values!
    // This will get executed
}
Integer e = 1000;
Integer f = 1000;
if (e == f) { // Should be a.equals(b)
    // Whether this gets executed depends on which compiler you use!
}

2. Czytelność:

Rozważ następujące dwa przykłady. Większość ludzi powiedziałaby, że druga jest bardziej czytelna.

Integer a = 2;
Integer b = 2;
if (!a.equals(b)) {
    // ...
}
int c = 2;
int d = 2;
if (c != d) {
    // ...
}

3. Wydajność:

Faktem jest, że jest wolniej używać obwolut obiektu dla prymitywów niż tylko za pomocą prymitywów. Dodajesz koszty tworzenia instancji obiektów, wywołań metod itp. Do rzeczy, których używasz wszędzie .

Cytat Knutha „… mówią w 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła” tak naprawdę nie ma tutaj zastosowania. Mówił o optymalizacjach, które czynią kod (lub system) bardziej skomplikowanym - jeśli zgadzasz się z punktem # 2, jest to optymalizacja, która sprawia, że ​​kod jest mniej skomplikowany!

4. To konwencja:

Jeśli wybierzesz inne opcje stylistyczne niż 99% innych programistów Java, istnieją 2 wady:

  • Kod innych osób będzie trudniejszy do odczytania. 99% przykładów / samouczków / etc będzie używać prymitywów. Za każdym razem, gdy je czytasz, będziesz miał dodatkowe koszty poznawcze związane z myśleniem o tym, jak wyglądałby w stylu, do którego jesteś przyzwyczajony.
  • Dla innych osób twój kod będzie trudniejszy do odczytania. Ilekroć zadajesz pytania na temat przepełnienia stosu, będziesz musiał przeszukiwać odpowiedzi / komentarze, pytając „dlaczego nie używasz prymitywów?”. Jeśli mi nie wierzysz, spójrz na bitwy toczone przez ludzi, takie jak umieszczanie nawiasów, co nawet nie wpływa na generowany kod!

Zwykle wymieniam kilka kontrapunktów, ale szczerze mówiąc, nie mogę wymyślić żadnego dobrego powodu, aby nie iść tutaj na konwencję!

Vaughandroid
źródło
2
Nie sugeruj ludziom porównywania obiektów ==. Obiekty należy porównać equals().
Tulains Córdova
2
@ user61852 Nie sugerowałem tego jako rzeczy do zrobienia, ale jako powszechny błąd, który się popełnia! Czy powinienem to wyjaśnić?
vaughandroid
Tak, nie wspominasz, że obiekty powinny być porównywane z equals()... dajesz im obejście, aby porównanie obiektów z dało ==oczekiwane rezultaty.
Tulains Córdova
Słuszna uwaga. Zmieniłem to.
vaughandroid
Dodałem equals()do drugiego fragmentu kodu i zmieniłem swój głos.
Tulains Córdova
12

Zwykle idę z prymitywami. Jedną z osobliwości używania klas takich jak Integeri Booleanjest możliwość przypisania nulldo tych zmiennych. Oczywiście oznacza to, że musisz nullcały czas sprawdzać, ale lepiej uzyskać NullPointerException niż mieć błędy logiczne wynikające z użycia niektórych zmiennych intlub booleanzmiennych, które nie zostały poprawnie zainicjowane.

Oczywiście, ponieważ Java 8 możesz (i prawdopodobnie powinieneś) pójść o krok dalej i zamiast np. IntegerMożesz użyć Optional<Integer>dla zmiennych, które mogą mieć lub nie mieć wartości.

Dodatkowo wprowadza możliwość użycia nulldo przypisania tym zmiennym wartości „ nieznana ” lub „ symbol wieloznaczny ”. Może to być przydatne w niektórych sytuacjach, na przykład w Ternary Logic . Lub możesz sprawdzić, czy określony obiekt pasuje do jakiegoś szablonu; w takim przypadku możesz użyć nulltych zmiennych w szablonie, które mogą mieć dowolną wartość w obiekcie.

tobias_k
źródło
2
(Nie było to moje negatywne zdanie, ale ...) Java już wykrywa niezainicjowane zmienne i nie pozwala odczytać zmiennej, dopóki każda ścieżka kodu prowadząca do jej użycia ostatecznie nie przypisała wartości. Więc nie zyskujesz wiele z możliwości nulldomyślnego przypisania . Przeciwnie: lepiej byłoby w ogóle nie „inicjalizować” zmiennej. Nawet ustawienie dowolnej wartości domyślnej nullwyłącza kompilator ... ale także zapobiega wykrywaniu braku przydatnego przypisania na wszystkich ścieżkach kodu. Zatem błąd, który mógł wykryć kompilator, przechodzi do środowiska wykonawczego.
cHao
@ cHao Ale co, jeśli nie ma rozsądnej wartości domyślnej do zainicjowania zmiennej? Możesz ustawić na 0.0, lub -1, lub Integer.MAX_VALUE, lub False, ale ostatecznie nie wiesz, czy jest to wartość domyślna, czy rzeczywista wartość przypisana do tej zmiennej. W przypadkach, w których jest to ważne, posiadanie nullwartości może być wyraźniejsze.
tobias_k
Nie jest to jaśniejsze, po prostu łatwiej jest powiedzieć kompilatorowi, aby nie ostrzegał cię przed niejasną logiką, dopóki błąd się nie rozprzestrzeni. : P W przypadkach, gdy nie ma sensownego ustawienia domyślnego, nie inicjuj zmiennej. Ustaw go tylko wtedy, gdy będziesz miał rozsądną wartość, aby go tam umieścić. Pozwala to Java zatrzymać Cię w czasie kompilacji, jeśli twoja wartość może nie być ustawiona poprawnie.
cHao
@ cHao Miałem na myśli, że mogą istnieć przypadki, w których nie można zainicjować zmiennej i trzeba sobie z nią poradzić w czasie wykonywania. W takich przypadkach „domyślna”, taka jak „null”, którą można wyraźnie zidentyfikować jako domyślną lub „niezainicjowaną”, może być lepsza niż jakakolwiek inicjalizacja w czasie kompilacji, która może być również prawidłową wartością.
tobias_k
Czy masz na myśli taki przypadek? Przypadki, które mogę wymyślić, znajdują się na granicy interfejsu (np .: jako parametry lub typy zwracane) ... ale nawet tam byłyby znacznie lepsze, gdyby były opakowane. (Nadzy nullmają wiele problemów, w tym zerową paranoję). W obrębie funkcji zmienna, która może być faktycznie niezainicjowana w punkcie użycia, zazwyczaj wskazuje na nie wykryte przypadki. (Określona analiza przypisań jest uproszczona, więc możliwe są wyniki fałszywie dodatnie. Ale często można je rozwiązać, upraszczając logikę).
cHao
2

Słowami laika:

Z opakowań można korzystać, gdy trzeba dodawać elementy do kolekcji.

Kolekcje nie mogą zawierać prymitywów.

Tulains Córdova
źródło
0

Java wskazuje na autoboxing, jak wskazał m3th0dman. Pomyśl o tym na najniższym możliwym poziomie, a zobaczysz, że prowokacja (wejście lub wyjście) prymitywna wartość będzie oznaczać cykle zegara spędzane na niektórych zadaniach, których nie potrzebujesz, jeśli pracujesz z rodzimymi typami danych wokół aplikacji.

Zasadniczo należy próbować używać rodzimych typów danych, gdy tylko jest to możliwe.

Silvarion
źródło