Wstęp
Począwszy od JSE 5.0, do arsenału języków Java dodano języki generyczne.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 ”.
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:
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:<>
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.
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ą.
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.
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:
- Symbole wieloznaczne z górnym ograniczeniem - < ? rozszerza Numer >
- Nieograniczone symbole wieloznaczne - < ? >
- Dolne symbole wieloznaczne - < ? superliczba całkowita >
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:
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:- Yuri Tkach: Typy surowe – Generics #1 – Zaawansowana Java
- Dziedziczenie i przedłużacze generyczne — Generics #2 — Zaawansowana Java
- Rozszerzenie typu rekursywnego — Generics #3 — Zaawansowana Java
- Alexander Matorin - Nieoczywiste rodzaje generyczne
- Wprowadzenie do Javy. Genetyki. Symbole wieloznaczne | Technostream
- O'Reilly: Generics i kolekcje Java
GO TO FULL VERSION