Geneeriset tyyppiparametrit
Olemme listoihin tutustumisesta lähtien kertoneet tietorakenteille niiden sisältämän arvon tyypin. Esimerkiksi merkkijono-olioita sisältävä lista on esitelty muodossa ArrayList<String>
ja merkkijonoja avaimina ja arvoina sisältävä hajautustaulu on esitelty muodossa HashMap<String, String>
. Miten ihmeessä luokan voi toteuttaa niin, että luokka voi sisältää annetun tyyppisiä olioita?
Geneerisyys (generics) liittyy olioita säilövien luokkien tapaan säilöä vapaavalintaisen tyyppisiä olioita. Vapaavalintaisuus perustuu luokkien määrittelyssä käytettyyn geneeriseen tyyppiparametriin, jonka avulla voidaan määritellä olion luontivaiheessa valittavia tyyppejä. Luokan geneerisyys määritellään antamalla luokan nimen jälkeen haluttu määrä luokan tyyppiparametreja luokan nimen jälkeen tulevien pienempi kuin ja suurempi kuin -merkkien väliin public class Luokka<Tyyppiparametri1, Tyyppiparametri2, ...>
. Tyyppiparametrit määritellään tyypillisesti yhdellä kirjaimella.
Toteutetaan oma geneerinen luokka Lokero
, johon voi asettaa yhden minkälaisen tahansa olion.
public class Lokero<T> {
private T alkio;
public void asetaArvo(T alkio) {
this.alkio = alkio;
}
public T haeArvo() {
return alkio;
}
}
Määrittely public class Lokero<T>
kertoo että luokalle Lokero
tulee antaa konstruktorissa tyyppiparametri. Konstruktorikutsun jälkeen kaikki olion sisäiset muuttujat tulevat olemaan kutsun yhteydessä annettua tyyppiä. Luodaan merkkijonon tallentava lokero.
Lokero<String> merkkijono = new Lokero<>();
merkkijono.asetaArvo(":)");
System.out.println(merkkijono.haeArvo());
:)
Yllä olevalla ohjelmalla merkkijono-nimisen Lokero
-olion ajonaikainen toteutus on seuraavanlainen.
public class Lokero<String> {
private String alkio;
public void asetaArvo(String alkio) {
this.alkio = alkio;
}
public String haeArvo() {
return alkio;
}
}
Tyyppiparametria vaihtamalla voidaan luoda myös muuntyyppisiä olioita tallentavia Lokero
-olioita. Esimerkiksi kokonaisluvun saa tallennettua seuraavasti.
Lokero<Integer> luku = new Lokero<>();
luku.asetaArvo(5);
System.out.println(luku.haeArvo());
5
Vastaavasti esimerkiksi Random
-olion sisältävän lokeron saa luotua seuraavalla tavalla.
Lokero<Random> luku = new Lokero<>();
luku.asetaArvo(new Random());
System.out.println(luku.haeArvo().nextDouble());
Tyyppiparametrien määrää ei ole rajattu, vaan määrä riippuu toteutuksesta. Ohjelmoija voisi halutessaan toteuttaa esimerkiksi Pari
-luokan, johon voi laittaa kaksi halutun tyyppistä oliota.
public class Pari<T, K> {
private T eka;
private K toka;
public void asetaArvot(T eka, K toka) {
this.eka = eka;
this.toka = toka;
}
public T haeEka() {
return this.eka;
}
public K haeToka() {
return this.toka;
}
}
Huomattava osa Javan tietorakenteista käyttää tyyppiparametreja ja mahdollistaa eri tyyppisten muuttujien lisäämisen niihin. Esimerkiksi ArrayList saa yhden tyyppiparametrin, HashMap kaksi.
List<String> merkkijonot = new ArrayList<>();
Map<String, String> avainArvoParit = new HashMap<>();
Jatkossa kun näet esimerkiksi tyypin ArrayList<String>
tiedät että sen sisäisessä rakenteessa on käytetty geneeristä tyyppiparametria. Sama periaate löytyy esimerkiksi rajapinnassa Comparable.
Geneeristen rajapintojen tekeminen onnistuu samalla tavalla kuin geneeristen luokkien tekeminen. Alla on esimerkki geneerisestä rajapinnasta Lista
.
public interface Lista<T> {
void lisaa(T arvo);
T hae(int indeksi);
T poista(int indeksi);
}
Luokka voi toteuttaa geneerisen rajapinnan kahdella tavalla. Luokka voi määritellä rajapinnan toteutuksen yhteydessä rajapinnalle tyyppiparametrin tai luokalle voidaan myös määritellä tyyppiparametri. Alla olevassa esimerkissä on luokka Elokuvalista
, joka määrittelee listan toteutuksen yhteydessä rajapinnalle tyyppiparametrin. Elokuvalista on vain elokuvien käsittelyyn tarkoitettu.
public class Elokuvalista implements Lista<Elokuva> {
// oliomuuttujat
@Override
public void lisaa(Elokuva arvo) {
// toteutus
}
@Override
public Elokuva hae(int indeksi) {
// toteutus
}
@Override
public Elokuva poista(int indeksi) {
// toteutus
}
}
Toinen vaihtoehto on määritellä rajapinnan toteuttavalle luokalle tyyppiparametri, joka ohjataan myös rajapinnalle. Tällöin myös konkreettinen toteutus on geneerinen.
public class Yleislista<T> implements Lista<T> {
// oliomuuttujat
@Override
public void lisaa(T arvo) {
// toteutus
}
@Override
public T hae(int indeksi) {
// toteutus
}
@Override
public T poista(int indeksi) {
// toteutus
}
}
Yleislistan voisi toteuttaa myös halutessaan esimerkiksi Javan valmiin ArrayList-luokan avulla. Tällöin toteutus olisi kutakuinkin seuraava.
import java.util.ArrayList;
public class Yleislista<T> implements Lista<T> {
ArrayList<T> arvot;
public Yleislista() {
this.arvot = new ArrayList<>();
}
@Override
public void lisaa(T arvo) {
this.arvot.add(arvo);
}
@Override
public T hae(int indeksi) {
retun this.arvot.get(indeksi);
}
@Override
public T poista(int indeksi) {
T arvo = this.arvot.get(indeksi);
this.arvot.remove(indeksi);
return arvo;
}
}
Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!