CodeGym /Java blog /Véletlen /Java Generics: hogyan használjuk a szögletes zárójeleket ...
John Squirrels
Szint
San Francisco

Java Generics: hogyan használjuk a szögletes zárójeleket a gyakorlatban

Megjelent a csoportban

Bevezetés

A JSE 5.0-tól kezdődően a Java nyelv arzenáljába a generikusok is bekerültek.

Mik azok a generikusok a java-ban?

Az általánosok a Java speciális mechanizmusa az általános programozás megvalósítására – az adatok és algoritmusok leírásának módja, amely lehetővé teszi, hogy különböző adattípusokkal dolgozzon anélkül, hogy megváltoztatná az algoritmusok leírását. Az Oracle webhelyen külön oktatóanyag található a generikus szerek számára: " Lecke ". A generikumok megértéséhez először ki kell derítenie, miért van rájuk szükség, és mit adnak. Az oktatóanyag „ Miért használjunk általánosokat? ” szakasza azt mondja, hogy néhány cél az erősebb típusellenőrzés a fordítási időben, és az explicit leadások szükségességének kiküszöbölése. Generics a Java-ban: a szögletes zárójelek használata a gyakorlatban - 1Készüljünk fel néhány tesztre szeretett Tutorialspoint online java fordítónkban. Tegyük fel, hogy a következő kóddal rendelkezik:

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);
	}
}
Ez a kód tökéletesen fog futni. De mi van akkor, ha odajön hozzánk a főnök, és azt mondja, hogy "Hello, world!" túl használt kifejezés, és csak a "Hello"-t kell visszaadnia? Eltávolítjuk a kódot, amely összefűzi a ", world!" Ez elég ártalmatlannak tűnik, igaz? De valójában egy hibát kapunk AZ ÖSSZEÁLLÍTÁSI IDŐBEN:

error: incompatible types: Object cannot be converted to String
A probléma az, hogy a listánkban az objektumokat tároljuk. A String az Object leszármazottja (mivel minden Java osztály implicit módon örökli az Object -et ), ami azt jelenti, hogy szükségünk van egy explicit leadásra, de nem adtunk hozzá. Az összefűzési művelet során a statikus String.valueOf(obj) metódus az objektum segítségével kerül meghívásra. Végül meghívja az Object osztály toString metódusát. Más szavakkal, a listánk egy objektumot tartalmaz . Ez azt jelenti, hogy ahol szükségünk van egy adott típusra (nem az objektumra ), magunknak kell elvégeznünk a típuskonverziót:

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);
		}
	}
}
Ebben az esetben azonban, mivel a List objektumokat vesz fel, nem csak String s-t, hanem Integer s-t is tárolhat . De a legrosszabb az, hogy a fordító nem lát itt semmi rosszat. És most hibaüzenetet kapunk AT RUN TIME ("futásidejű hiba"). A hiba a következő lesz:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
El kell fogadnod, hogy ez nem túl jó. És mindezt azért, mert a fordító nem egy mesterséges intelligencia, amely képes mindig helyesen kitalálni a programozó szándékát. A Java SE 5 bevezette az általánosokat, hogy elmondhassuk a fordítónak szándékainkat – arról, hogy mely típusokat fogjuk használni. A kódunkat úgy javítjuk, hogy elmondjuk a fordítónak, hogy mit akarunk:

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);
		}
	}
}
Amint látja, már nincs szükségünk egy karakterláncra leadásra . Ezenkívül szögletes zárójelek veszik körül a típus argumentumot. Most a fordító nem engedi lefordítani az osztályt, amíg el nem távolítjuk azt a sort, amely 123-at ad a listához, mivel ez egy egész szám . És ezt meg is fogja mondani nekünk. Sokan a generikus gyógyszereket „szintaktikai cukornak” nevezik. És igazuk van, mivel a generikumok összeállítása után valóban ugyanolyan típusú konverziókká válnak. Nézzük meg a lefordított osztályok bájtkódját: egy explicit cast-ot használó és egy generikust használó osztály: Generics a Java nyelven: hogyan kell használni a szögletes zárójeleket a gyakorlatban - 2A fordítás után minden általános törlődik. Ezt " típustörlésnek " nevezik". A típustörlést és az általánosokat úgy tervezték, hogy visszamenőleg kompatibilisek legyenek a JDK régebbi verzióival, miközben lehetővé teszik a fordító számára, hogy segítsen a típusdefiníciókban a Java új verzióiban.

Nyers típusok

Ha már a generikáról beszélünk, mindig két kategóriánk van: paraméterezett típusok és nyers típusok. A nyers típusok olyan típusok, amelyek elhagyják a szögletes zárójelben szereplő "típus-tisztázást": Generics a Java nyelven: hogyan kell használni a szögletes zárójeleket a gyakorlatban - 3A paraméterezett típusok viszont tartalmaznak egy "tisztázást": Generics a Java nyelven: hogyan kell használni a szögletes zárójeleket a gyakorlatban - 4Amint láthatja, egy szokatlan konstrukciót használtunk, amelyet a képernyőképen egy nyíl jelöl. Ez egy speciális szintaxis, amelyet a Java SE 7-hez adtak hozzá. Ezt " gyémántnak " hívják . Miért? A szögletes zárójelek gyémántot alkotnak: <> . Azt is tudnia kell, hogy a gyémánt szintaxis a " típuskövetkeztetés " fogalmához kapcsolódik . Hiszen a fordító, látva <>a jobb oldalon a hozzárendelési operátor bal oldalát nézi, ahol megtalálja annak a változónak a típusát, amelynek az értékét hozzárendeljük. Az ebben a részben találtak alapján megérti a jobb oldali érték típusát. Valójában, ha egy általános típus szerepel a bal oldalon, de a jobb oldalon nem, a fordító képes következtetni a típusra:

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);
	}
}
De ez keveri az új stílust a generikusokkal és a régi stílust ezek nélkül. Ez pedig nagyon nem kívánatos. A fenti kód összeállításakor a következő üzenetet kapjuk:

Note: HelloWorld.java uses unchecked or unsafe operations
Valójában érthetetlennek tűnik, hogy miért kell még egy gyémántot hozzáadni. De itt egy példa:

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);
	}
}
Emlékszel, hogy az ArrayListnek van egy második konstruktora, amely egy gyűjteményt vesz argumentumként. És ez az, ahol valami baljóslat rejtőzik. A gyémánt szintaxis nélkül a fordító nem érti, hogy megtévesztik. A gyémánt szintaxissal ez igen. Tehát az 1. szabály: mindig használja a gyémánt szintaxist a paraméterezett típusokkal. Ellenkező esetben fennáll annak a veszélye, hogy elveszítjük a nyers típusokat. A "nem bejelölt vagy nem biztonságos műveleteket használ" figyelmeztetések kiküszöbölésére használhatjuk a @SuppressWarnings("unchecked") megjegyzést egy metóduson vagy osztályon. De gondolja át, miért döntött úgy, hogy ezt használja. Emlékezz az első számú szabályra. Lehet, hogy típus argumentumot kell hozzáadnia.

Java általános módszerek

A Generics lehetővé teszi olyan metódusok létrehozását, amelyek paramétertípusai és visszatérési típusa paraméterezett. Az Oracle oktatóanyagának külön szakasza van ennek a képességnek: " Általános módszerek ". Fontos megjegyezni az oktatóanyagban tanított szintaxist:
  • szögletes zárójelben tartalmazza a típusparaméterek listáját;
  • a típusparaméterek listája a metódus visszatérési típusa elé kerül.
Nézzünk egy példát:

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));
		}
    }
}
Ha megnézi az Util osztályt, látni fogja, hogy két általános metódusa van. A típuskövetkeztetés lehetőségének köszönhetően vagy közvetlenül jelezhetjük a fordítónak a típust, vagy mi magunk is megadhatjuk. Mindkét lehetőséget bemutatjuk a példában. Egyébként a szintaxisnak sok értelme van, ha belegondolunk. Általános metódus deklarálásakor a type paramétert a metódus ELŐTT adjuk meg, mert ha a type paramétert a metódus után deklaráljuk, akkor a JVM nem tudná kitalálni, hogy melyik típust használja. Ennek megfelelően először deklaráljuk, hogy a T típusú paramétert használjuk , majd azt mondjuk, hogy ezt a típust fogjuk visszaadni. Természetesen az Util.<Integer>getValue(elem, String.class) hiba lép fel:nem kompatibilis típusok: A Class<String> nem konvertálható Class<Integer>-vé . Általános módszerek használatakor mindig emlékezzen a típustörlésre. Nézzünk egy példát:

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);
		}
    }
}
Ez jól fog működni. De csak addig, amíg a fordító megérti, hogy a meghívott metódus visszatérési típusa Integer . Cserélje ki a konzol kimeneti utasítását a következő sorra:

System.out.println(Util.getValue(element) + 1);
Hibát kapunk:

bad operand types for binary operator '+', first type: Object, second type: int.
Más szóval, típustörlés történt. A fordító azt látja, hogy senki nem adta meg a típust, ezért a típus Object- ként jelenik meg , és a metódus hibával meghiúsul.

Általános osztályok

Nem csak a módszerek paraméterezhetők. Osztályok is. Az Oracle oktatóanyagának „Általános típusok” című szakasza ennek szentelt. Nézzünk egy példát:

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);
		}
	}
}
Itt minden egyszerű. Ha az általános osztályt használjuk, akkor az osztály neve után megjelenik a típus paraméter. Most hozzuk létre ennek az osztálynak a példányát a metódusban:

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Ez a kód jól fog futni. A fordító látja, hogy létezik egy számlista és egy karakterlánc - gyűjtemény . De mi van, ha megszüntetjük a type paramétert, és ezt tesszük:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Hibát kapunk:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Ez megint a típustörlés. Mivel az osztály már nem használ típusparamétert, a fordító úgy dönt, hogy mivel egy Listát adtunk át, a List<Integer> metódus a legmegfelelőbb. És egy hibával elbukunk. Ezért van 2. szabályunk: Ha általános osztályod van, mindig adja meg a típusparamétereket.

Korlátozások

Korlátozhatjuk az általános metódusokban és osztályokban megadott típusokat. Tegyük fel például, hogy azt szeretnénk, hogy egy tároló csak egy számot fogadjon el típus argumentumként. Ezt a funkciót az Oracle oktatóanyagának Korlátozott típusparaméterek szakasza ismerteti . Nézzünk egy példát:

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");
    }
}
Amint látja, a type paramétert a Number osztályra/interfészre vagy annak leszármazottaira korlátoztuk . Ne feledje, hogy nemcsak osztályt, hanem interfészeket is megadhat. Például:

public static class NumberContainer<T extends Number & Comparable> {
Az általánosok a helyettesítő karaktereket is támogatják. Három típusra oszthatók:
  • Felső korlátos helyettesítő karakterek — < ? kiterjeszti Szám >
  • Korlátlan helyettesítő karakterek — < ? >
  • Alsó korlátos helyettesítő karakterek — < ? szuper egész >
A helyettesítő karakterek használatánál be kell tartani a Get-Put elvet . A következőképpen fejezhető ki:
  • Használjon kiterjesztett helyettesítő karaktert, ha csak értékeket kap ki egy struktúrából.
  • Használjon szuper helyettesítő karaktert, ha csak értékeket ad meg egy szerkezetben.
  • És ne használjon helyettesítő karaktert, ha mindketten egy szerkezetet akarsz elérni és elhelyezni.
Ezt az elvet a Producer Extends Consumer Super (PECS) elvnek is nevezik. Íme egy kis példa a Java Collections.copy metódusának forráskódjából : Generics a Java nyelven: hogyan kell használni a szögletes zárójeleket a gyakorlatban - 5És itt van egy kis példa arra, hogy mi NEM működik:

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);
}
De ha lecseréled a kiterjesztéseket szuperre , akkor minden rendben van. Mivel a listát a tartalmának megjelenítése előtt feltöltjük egy értékkel, ez fogyasztó . Ennek megfelelően szuper.

Öröklés

A generikának van még egy érdekes tulajdonsága: az öröklődés. Az öröklődés működésének általános leírása az Oracle oktatóanyagának „ Általános adatok, öröklődés és altípusok ” című részében található. A legfontosabb, hogy emlékezzen és ismerje fel a következőket. Ezt nem tudjuk megtenni:

List<CharSequence> list1 = new ArrayList<String>();
Mert az öröklődés másként működik a generikusoknál: Generics a Java nyelven: hogyan kell használni a szögletes zárójeleket a gyakorlatban - 6És itt van egy másik jó példa, amely hibával meghiúsul:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Itt megint minden egyszerű. A List<String> nem a List<Object> leszármazottja , bár a String az Object leszármazottja . A tanultak megerősítése érdekében javasoljuk, hogy nézzen meg egy videóleckét a Java-tanfolyamról

Következtetés

Tehát felfrissítettük emlékezetünket a generikus gyógyszerekkel kapcsolatban. Ha ritkán használja ki teljes mértékben a képességeiket, néhány részlet homályossá válik. Remélem, ez a rövid áttekintés segített felfrissíteni a memóriáját. A még jobb eredmények érdekében erősen ajánlom, hogy ismerkedjen meg a következő anyaggal:
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION