Geneerilised meetodid
Sissejuhatus
Tüübiparameetrid ei pea olema deklareeritud ainuüksi klassitasemel.
Ka üksikud meetodid võivad deklareerida oma tüübiparameetrid, olles seeläbi geneerilised sõltumata klassist, kuhu nad kuuluvad.
See on kasulik olukorras, kus tüüp on oluline ainult konkreetse meetodi sees, või siis, kui meetod on static ega ole seotud ühegi konkreetse isendi tüübiga.
Geneerilise meetodi deklareerimine
Geneerilise meetodi puhul kirjutatakse tüübiparameeter nurksulgudesse enne tagastustüüpi:
public static <T> T identity(T value) {
return value;
}
Siin deklareerib <T> tüübiparameetri ning T esineb nii parameetri kui ka tagastustüübina.
Kompilaator tuletab T tüübi automaatselt vastavalt argumendile.
Seda protsessi nimetatakse tüübituletuseks (type inference).
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as Integer
Enamasti töötab tüübituletus automaatselt ning täiendavat märkimist ei ole vaja. Soovi korral saab tüübi ka selgesõnaliselt määrata, kirjutades selle nurksulgudesse enne meetodi nime. Seda võtet nimetatakse tüübitunnistajaks (type witness):
// Instance method call with explicit type
String s = obj.<String>identity("hello");
// Static method call with explicit type
String s = Main.<String>identity("hello");
Selgesõnalist tüübitunnistajat on praktikas harva vaja, sest enamasti suudab kompilaator tüübi ise tuletada. See võib osutuda vajalikuks olukordades, kus kompilaatoril ei ole piisavalt infot tüübi määramiseks, näiteks kui tagastusväärtust ei kasutata või see omistatakse väga üldisele tüübile.
Praktiline näide
Geneeriliste meetodite klassikaline kasutusjuht on operatsioonide tegemine listiga ilma selle elemenditüüpi ette teadmata.
public static <T> void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
Meetod ei hooli sellest, millist tüüpi elemendid listis on.
Oluline on vaid, et kõik elemendid oleksid sama tüüpi T.
See töötab ükskõik millise listiga:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Carol"));
List<Integer> numbers = new ArrayList<>(List.of(10, 20, 30));
swap(names, 0, 2); // [Carol, Bob, Alice]
swap(numbers, 0, 1); // [20, 10, 30]
Üks ja sama meetod katab kõik elemenditüübid.
Ei ole vaja luua eraldi meetodeid, näiteks swapStrings, swapIntegers jne.
See on geneeriliste meetodite peamine eelis: käitumine on üldine, kuid tüübiohutus säilib.
Piiratud tüübiga geneerilised meetodid
Ka geneerilistel meetoditel võivad olla piiratud tüübiparameetrid, täpselt nagu geneerilistel klassidel. Antud piirang võimaldab meetodi sees kasutada neid meetodeid, mis on määratletud piiranguks olevas tüübis.
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
System.out.println(max(3, 7)); // 7
System.out.println(max("apple", "fig")); // fig
System.out.println(max(2.5, 1.8)); // 2.5
Piirang <T extends Comparable<T>> tagab, et T tüüpi objektid on omavahel võrreldavad.
Tänu sellele teab kompilaator, et compareTo-meetodi väljakutse on korrektne ja turvaline.
Geneerilised meetodid vs geneerilised klassid
Geneeriline klass on sobiv siis, kui tüübiparameeter mõjutab objekti tervikstruktuuri.
Näiteks välju, mitut meetodit või objekti identiteeti tervikuna.
Geneeriline meetod on sobiv siis, kui tüüp on oluline vaid konkreetse meetodi loogika jaoks.
Vaatame näiteks abiklassi kollektsioonidega töötamiseks:
public class CollectionUtils {
// Generic method — the type is only relevant within this method
public static <T> List<T> repeat(T element, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(element);
}
return result;
}
// Another generic method with its own independent type parameter
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException("List is empty");
return list.get(0);
}
}
CollectionUtils ise ei ole geneeriline, kuna klassil puudub klassitasemel tüübiparameeter.
Iga meetod on geneeriline iseseisvalt ning tuletab oma tüübi talle antud argumentide põhjal.
List<String> words = CollectionUtils.repeat("hello", 3); // [hello, hello, hello]
List<Integer> zeroes = CollectionUtils.repeat(0, 5); // [0, 0, 0, 0, 0]
String first = CollectionUtils.getFirst(words); // hello
Integer num = CollectionUtils.getFirst(zeroes); // 0
Erinevus seisneb seega ulatuses:
- kui tüüp on osa klassi olemusest, kasuta geneerilist klassi;
- kui tüüp on vajalik vaid konkreetse operatsiooni jaoks, piisab geneerilisest meetodist.
Tagastustüüp ja mitu tüübiparameetrit
Sarnaselt klassile võib geneerilisel meetodil olla mitu tüübiparameetrit ning neid saab vabalt kasutada nii parameetrites kui ka tagastustüübis.
public static <K, V> Map<V, K> invertMap(Map<K, V> original) {
Map<V, K> inverted = new HashMap<>();
for (Map.Entry<K, V> entry : original.entrySet()) {
inverted.put(entry.getValue(), entry.getKey());
}
return inverted;
}
Map<String, Integer> scores = Map.of("Alice", 90, "Bob", 85);
Map<Integer, String> inverted = invertMap(scores);
System.out.println(inverted.get(90)); // Alice
System.out.println(inverted.get(85)); // Bob
Antud juhul on tüübiparameetrid K ja V teineteisest sõltumatud.
Meetod tuletab mõlemad tüübid automaatselt argumendi põhjal ning kasutab neid seejärel vastupidises järjekorras tagastustüübis.
Eelista geneerilisi meetodeid geneerilistele klassidele siis, kui tüübiparameeter on oluline vaid ühe konkreetse operatsiooni jaoks. Nii püsib klass konkreetsena ning kasutajad ei pea määrama tüübiparameetreid klassi tasemel olukorras, kus neid on vaja ainult ühe meetodikutse jaoks.