Недеља 9 — Генерички типови
Код: GitHub · преузми пројекат (ZIP) · задатак за самостални рад
До сада смо правили класе које раде са унапред познатим типовима.
На пример:
class PaketTelefona {
private Telefon sadrzaj;
}
Али шта ако је понашање класе потпуно исто без обзира на то да ли пакет садржи:
- телефон;
- књигу;
- патике;
- неки наш објекат?
Једна могућност је да користимо Object.
То ради, али губимо информацију о типу.
Генерици нам омогућавају да напишемо једну апстракцију која ради за различите типове, а да компајлер и даље проверава исправност типова.
Главна идеја
Генерици нису првенствено начин да пишемо необичне изразе са:
<T>
<K, V>
<? extends T>
Њихов главни циљ у овом курсу је:
поновна употреба кода без губитка type safety-ja.
Циљеви
После вежби студент треба да уме да:
- објасни проблем чувања вредности као
Object; - направи генеричку класу;
- користи генерички тип са различитим аргументима типа;
- објасни да
Paket<Telefon>иPaket<Knjiga>нису исти тип; - напише једноставну генеричку методу;
- направи генерички интерфејс;
- користи ограничење типа, нпр.
<T extends Procenjiv>; - разуме основну мотивацију за wildcard
?; - препозна генерике у стандардним колекцијама које ћемо радити следеће недеље.
Редослед примера
primer01_problem_sa_object
Правимо класу:
Paket
која чува:
private Object sadrzaj;
Можемо ставити било који објекат.
Проблем настаје приликом читања:
Telefon telefon = (Telefon) paket.getSadrzaj();
Потребно је експлицитно кастовање.
Још горе:
Knjiga knjiga = (Knjiga) paket.getSadrzaj();
компајлер то дозвољава, а грешку добијамо тек током извршавања.
primer02_genericka_klasa
Уводимо:
public class Paket<T>
Сада:
Paket<Telefon>
Paket<Knjiga>
користе исту имплементацију класе, али компајлер зна шта се налази у конкретном пакету.
Добијамо:
Telefon telefon = paketTelefona.getSadrzaj();
без кастовања.
А ово више не може да се компајлира:
Paket<Telefon> paket = new Paket<>(...);
paket.postaviSadrzaj(new Knjiga(...));
Грешка се открива при компајлирању, а не код корисника програма.
primer03_genericka_metoda
Није обавезно да цела класа буде генеричка.
Можемо направити само једну генеричку методу:
public static <T> void prebaci(
Paket<T> izvor,
Paket<T> odrediste
)
Тип T се закључује из аргумената.
Иста метода ради за Paket<String>, Paket<Integer> и пакет било ког другог типа, без дуплирања имплементације.
primer04_genericki_interfejs
Интерфејс такође може бити генерички:
public interface Skladiste<T>
Његов уговор каже:
void smesti(T predmet);
T preuzmi();
boolean jePrazno();
Имплементација:
JednomesnoSkladiste<T>
не мора унапред да зна који ће конкретан тип чувати.
Ово је важна припрема за:
List<T>
Set<T>
Map<K, V>
из стандардне библиотеке.
primer05_ogranicenje_tipa
Понекад није довољно рећи:
<T>
јер желимо да над T можемо да позовемо одређену операцију.
Дефинишемо:
public interface Procenjiv {
int proceniVrednost();
}
и затим:
public class OsiguraniPaket<T extends Procenjiv>
Сада компајлер зна да сваки T има:
proceniVrednost()
Зато OsiguraniPaket може без кастовања да израчуна цену осигурања.
primer06_wildcard
Овај пример је намерно краћи.
Генерички типови у Јави су инваријантни.
То значи да, иако:
Telefon IS-A Procenjiv
не следи:
Paket<Telefon> IS-A Paket<Procenjiv>
Када желимо методу која само чита различите пакете процењивих предмета, можемо користити:
Paket<? extends Procenjiv>
За ову недељу је довољно разумети мотивацију.
Детаљне комбинације extends и super нису циљ курса.
Зашто не само Object?
Без генерика:
Object sadrzaj = ...
компајлер губи прецизну информацију о типу.
Онда корисник мора да зна шта је стварно унутра и да кастује.
Са генериком:
Paket<Telefon>
информација о типу постаје део потписа.
Компајлер може да провери код пре извршавања.
Генерици и ООП
Генерици нису замена за полиморфизам.
На пример:
List<NacinPlacanja>
може да садржи различите имплементације интерфејса NacinPlacanja.
Овде истовремено користимо:
- генерике да кажемо који тип елемената колекција прихвата;
- полиморфизам да различити конкретни објекти могу бити посматрани кроз заједнички интерфејс.
Ове две идеје се веома често користе заједно.
Type erasure — само напомена
Java генерици су имплементирани помоћу механизма који се зове type erasure.
За овај курс није потребно детаљно проучавати његову имплементацију.
Важно је само знати да због тога постоје нека ограничења, на пример не можемо једноставно писати:
new T()
или:
new T[10]
Немојте памтити листу ограничења напамет. Када наиђемо на конкретан случај, објаснићемо га.
Питања за дискусију
- Шта губимо када вредност чувамо као
Object? - Када се грешка погрешног кастовања открива?
- Шта значи
TуPaket<T>? - Да ли је
Tпосебна Java кључна реч? - Да ли су
Paket<Telefon>иPaket<Knjiga>исти тип? - Зашто нам код генеричке класе више није потребан cast?
- Која је разлика између генеричке класе и генеричке методе?
- Зашто интерфејс
Skladiste<T>има смисла? - Зашто
OsiguraniPaket<T>није довољан ако жели да позиваproceniVrednost()? - Шта нам гарантује
<T extends Procenjiv>? - Зашто
Paket<Telefon>није аутоматскиPaket<Procenjiv>? - Где ћемо следеће недеље видети генерике у стандардној библиотеци?
Мини задатак
У ZADATAK.md налази се пример паметног ормарића.
Студент треба да направи генерички Ormaric<T> и затим ограничену генеричку класу за предмете који имају масу.
Уговор складишта (пример04)
null означава празно складиште, па га није дозвољено сместити као предмет.
Покушај смештања у заузето складиште баца IllegalStateException, без промене
постојећег садржаја. preuzmi() из празног складишта враћа null. Политика је
експлицитна да позивалац не нагађа да ли је операција успела.