JavaRush /Blog Java /Random-PL /Teoria rodzajów generycznych w Javie, czyli jak zastosowa...
Viacheslav
Poziom 3

Teoria rodzajów generycznych w Javie, czyli jak zastosować nawiasy w praktyce

Opublikowano w grupie Random-PL

Wstęp

Począwszy od JSE 5.0, do arsenału języków Java dodano języki generyczne.
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 1

Co to są leki generyczne w Javie?

Generyki (uogólnienia) to specjalne środki języka Java służące do wdrażania programowania uogólnionego: specjalne podejście do opisu danych i algorytmów, które pozwala pracować z różnymi typami danych bez zmiany ich opisu. Na stronie internetowej Oracle oddzielny tutorial poświęcony jest lekom generycznym: „ Lekcja: Generics ”.

Po pierwsze, aby zrozumieć leki generyczne, musisz zrozumieć, dlaczego w ogóle są potrzebne i co zapewniają. W samouczku w sekcji „ Dlaczego warto używać typów ogólnych ?” Mówi się, że jednym z celów jest silniejsze sprawdzanie typów w czasie kompilacji i eliminowanie potrzeby jawnego rzutowania.
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 2
Przygotujmy nasz ulubiony kompilator Java tutorialspoint online do eksperymentów . Wyobraźmy sobie ten kod:
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Ten kod będzie działał poprawnie. Ale co by było, gdyby przyszli do nas i powiedzieli, że zdanie „Witaj, świecie!” pobity i możesz tylko wrócić Witam? Usuńmy z kodu konkatenację z ciągiem znaków ", world!". Wydawałoby się, że co może być bardziej nieszkodliwego? Ale tak naprawdę podczas kompilacji otrzymamy błąd : error: incompatible types: Object cannot be converted to String Rzecz w tym, że w naszym przypadku List przechowuje listę obiektów typu Object. Ponieważ String jest potomkiem Object (ponieważ wszystkie klasy są domyślnie dziedziczone z Object w Javie), wymaga jawnego rzutowania, czego nie zrobiliśmy. Podczas łączenia na obiekcie zostanie wywołana metoda statyczna String.valueOf(obj), która ostatecznie wywoła na obiekcie metodę toString. Oznacza to, że nasza lista zawiera obiekt. Okazuje się, że tam, gdzie potrzebujemy konkretnego typu, a nie obiektu, będziemy musieli sami wykonać rzutowanie typu:
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println((String)str);
		}
	}
}
Jednak w tym przypadku, ponieważ Lista akceptuje listę obiektów, przechowuje nie tylko ciąg znaków, ale także liczbę całkowitą. Ale najgorsze jest to, że w tym przypadku kompilator nie zobaczy niczego złego. I tutaj otrzymamy błąd PODCZAS WYKONANIA (mówią też, że błąd został odebrany „w czasie wykonywania”). Błąd będzie następujący: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Zgadzam się, nie najprzyjemniejszy. A wszystko dlatego, że kompilator nie jest sztuczną inteligencją i nie jest w stanie odgadnąć wszystkiego, co programista ma na myśli. Aby powiedzieć kompilatorowi więcej o typach, których będziemy używać, w Java SE 5 wprowadzono typy generyczne . Poprawmy naszą wersję, mówiąc kompilatorowi, czego chcemy:
import java.util.*;
public class HelloWorld {
	public static void main(String []args){
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println(str);
		}
	}
}
Jak widzimy, nie potrzebujemy już rzutowania na String. Ponadto mamy teraz nawiasy ostrokątne, które obramowują rodzaje ogólne. Teraz kompilator nie pozwoli na kompilację klasy, dopóki nie usuniemy dodania 123 do listy, ponieważ to jest liczba całkowita. On nam to powie. Wiele osób nazywa leki generyczne „cukrem syntaktycznym”. I mają rację, ponieważ typy generyczne rzeczywiście staną się tymi samymi kastami po skompilowaniu. Spójrzmy na kod bajtowy skompilowanych klas: z ręcznym rzutowaniem i użyciem typów generycznych:
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 3
Po kompilacji wszelkie informacje o lekach generycznych są usuwane. Nazywa się to „usuwaniem typu” lub „ kasowaniem typu ”. Usuwanie typów i typy generyczne zostały zaprojektowane tak, aby zapewnić kompatybilność wsteczną ze starszymi wersjami JDK, jednocześnie umożliwiając kompilatorowi pomoc w wnioskowaniu o typie w nowszych wersjach Java.
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 4

Typy surowe lub typy surowe

Mówiąc o rodzajach, zawsze mamy dwie kategorie: typy wpisane (typy ogólne) i typy „surowe” (typy surowe). Typy surowe to typy bez określenia „kwalifikacji” w nawiasach ostrych:
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 5
Typy wpisane są odwrotne, ze wskazaniem „wyjaśnienie”:
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 6
Jak widać zastosowaliśmy nietypowy projekt, oznaczony strzałką na zrzucie ekranu. Jest to specjalna składnia dodana w Java SE 7 i nazywa się ją „ diamentem ”, co oznacza diament. Dlaczego? Można narysować analogię pomiędzy kształtem rombu i kształtem nawiasów klamrowych: <> Składnia rombu jest również powiązana z koncepcją „ wnioskowania o typie ”, czyli wnioskowania o typie. Przecież kompilator widząc <> po prawej stronie, patrzy na lewą stronę, gdzie znajduje się deklaracja typu zmiennej, do której przypisana jest wartość. I z tej części rozumie, jakiego typu jest wpisana wartość po prawej stronie. W rzeczywistości, jeśli po lewej stronie podano typ generyczny, a po prawej nie, kompilator będzie mógł wywnioskować typ:
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Byłaby to jednak mieszanka nowego stylu z lekami generycznymi i starego stylu bez nich. A to jest wyjątkowo niepożądane. Podczas kompilacji powyższego kodu otrzymamy komunikat: Note: HelloWorld.java uses unchecked or unsafe operations. Tak naprawdę nie jest jasne, dlaczego w ogóle konieczne jest dodawanie tutaj diamentu. Ale oto przykład:
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Jak pamiętamy, ArrayList ma również drugi konstruktor, który jako dane wejściowe pobiera kolekcję. I tu właśnie kryje się oszustwo. Bez składni diamentu kompilator nie rozumie, że jest oszukiwany, ale w przypadku diamentu tak. Dlatego zasada nr 1 : zawsze używaj składni diamentowej, jeśli używamy typów typowanych. W przeciwnym razie ryzykujemy pominięcie miejsca, w którym używamy typu surowego. Aby uniknąć ostrzeżeń w dzienniku, które „wykorzystuje niesprawdzone lub niebezpieczne operacje”, możesz podać specjalną adnotację dotyczącą używanej metody lub klasy: @SuppressWarnings("unchecked") Pomijanie jest tłumaczone jako pomijanie, to znaczy dosłownie pomijanie ostrzeżeń. Ale zastanów się, dlaczego zdecydowałeś się to wskazać? Zapamiętaj zasadę numer jeden i być może będziesz musiał dodać pisanie.
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce - 7

Metody ogólne

Generics pozwalają na wpisywanie metod. W samouczku Oracle znajduje się osobna sekcja poświęcona tej funkcji: „ Metody ogólne ”. W tym samouczku ważne jest, aby zapamiętać składnię:
  • zawiera listę wpisanych parametrów w nawiasach ostrych;
  • lista wpisanych parametrów znajduje się przed zwróconą metodą.
Spójrzmy na przykład:
import java.util.*;
public class HelloWorld{

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Jeśli spojrzymy na klasę Util, zobaczymy w niej dwie metody typowane. Dzięki wnioskowaniu o typie możemy dostarczyć definicję typu bezpośrednio do kompilatora lub możemy go określić sami. Obie opcje przedstawiono w przykładzie. Nawiasem mówiąc, składnia jest całkiem logiczna, jeśli się nad tym zastanowić. Wpisując metodę, podajemy metodę generyczną PRZED metodą, ponieważ jeśli użyjemy metody generycznej po metodzie, Java nie będzie w stanie określić, jakiego typu użyć. Dlatego najpierw ogłaszamy, że będziemy używać generycznego T, a następnie mówimy, że zwrócimy to generyczne. Oczywiście Util.<Integer>getValue(element, String.class)zakończy się to niepowodzeniem z powodu błędu incompatible types: Class<String> cannot be converted to Class<Integer>. Stosując metody wpisane należy zawsze pamiętać o wymazaniu typu. Spójrzmy na przykład:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
To będzie działać świetnie. Ale tylko tak długo, jak kompilator rozumie, że wywoływana metoda ma typ Integer. Zastąpmy wyjście konsoli następującym wierszem: System.out.println(Util.getValue(element) + 1); I pojawia się błąd: złe typy operandów dla operatora binarnego „+”, pierwszy typ: Object , drugi typ: int Oznacza to, że typy zostały usunięte. Kompilator widzi, że nikt nie określił typu, typ jest określony jako Obiekt, a wykonanie kodu kończy się niepowodzeniem z powodu błędu.
Теория дженериков в Java Lub Jak на практике ставить скобки - 8

Typy ogólne

Można wpisywać nie tylko metody, ale także same klasy. Oracle ma sekcję „ Typy ogólne ” poświęconą temu w swoim przewodniku. Spójrzmy na przykład:
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Tutaj wszystko jest proste. Jeśli używamy klasy, jej nazwa rodzajowa jest wymieniona po nazwie klasy. Stwórzmy teraz instancję tej klasy w metodzie głównej:
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
To będzie działać dobrze. Kompilator widzi, że istnieje Lista liczb i Kolekcja typu String. Ale co, jeśli usuniemy typy generyczne i zrobimy to:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Otrzymamy błąd: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Wpisz ponownie usunięcie. Ponieważ klasa nie ma już klasy ogólnej, kompilator decyduje, że skoro przekazaliśmy Listę, bardziej odpowiednia będzie metoda z List<Integer>. I upadamy z błędem. Dlatego zasada nr 2: Jeśli wpisana jest klasa, zawsze określaj typ w pliku ogólnym .

Ograniczenia

Możemy zastosować ograniczenie do typów określonych w rodzajach. Na przykład chcemy, aby kontener akceptował tylko liczbę jako dane wejściowe. Ta funkcja jest opisana w samouczku Oracle w sekcji Parametry typu ograniczonego . Spójrzmy na przykład:
import java.util.*;
public class HelloWorld{

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number)  { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Jak widać, ograniczyliśmy typ ogólny do klasy/interfejsu Number i jego potomków. Co ciekawe, można określić nie tylko klasę, ale także interfejsy. Na przykład: public static class NumberContainer<T extends Number & Comparable> { Generics mają również koncepcję Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Te z kolei dzielą się na trzy typy: W przypadku symboli wieloznacznych obowiązuje tak zwana zasada Get Put . Można je wyrazić w następującej formie:
Теория дженериков в Java Lub Jak на практике ставить скобки - 9
Zasada ta nazywana jest także zasadą PECS (Producer Extends Consumer Super). Więcej o Habré możesz przeczytać w artykule „ Używanie ogólnych symboli wieloznacznych w celu poprawy użyteczności interfejsu API Java ”, a także w doskonałej dyskusji na temat przepełnienia stosu: „ Używanie symboli wieloznacznych w Generics Java ”. Oto mały przykład ze źródła Java - metoda Collections.copy:
Теория дженериков в Java Lub Jak на практике ставить скобки - 10
Cóż, mały przykład tego, jak to NIE będzie działać:
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Ale jeśli zastąpisz rozciąganie super, wszystko będzie dobrze. Ponieważ wypełniamy listę wartością przed jej wypisaniem, jest to dla nas konsument, to znaczy konsument. Dlatego używamy super.

Dziedzictwo

Generyki mają jeszcze jedną niezwykłą cechę – ich dziedziczenie. Dziedziczenie typów generycznych opisano w samouczku Oracle w sekcji „ Części ogólne, dziedziczenie i podtypy ”. Najważniejsze jest, aby pamiętać i uświadomić sobie, co następuje. Nie możemy tego zrobić:
List<CharSequence> list1 = new ArrayList<String>();
Ponieważ dziedziczenie działa inaczej w przypadku typów generycznych:
Теория дженериков в Java Lub Jak на практике ставить скобки - 11
A oto kolejny dobry przykład, który zakończy się niepowodzeniem z błędem:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Tutaj też wszystko jest proste. List<String> nie jest potomkiem List<Object>, chociaż String jest potomkiem Object.

Finał

Odświeżyliśmy więc naszą pamięć o lekach generycznych. Jeśli rzadko używa się ich w całej mocy, niektóre szczegóły wypadają z pamięci. Mam nadzieję, że ta krótka recenzja pomoże odświeżyć Twoją pamięć. Aby uzyskać lepsze wyniki, gorąco polecam przeczytanie następujących materiałów: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION