- COMP.CS.140
- 6. Luokkahierarkiat
- 6.5 Rajapinnat Javassa, osa 2
Rajapinnat Javassa, osa 2¶
Nyt kun Javan geneerisyys on hieman tutumpi käsite, voimme tarkastella muutamia keskeisiä Javan luokkakirjastossa määritettyjä rajapintoja.
Rajapinta Comparable<T>
¶
Aloitetaan geneerisestä rajapinnasta Comparable
, jonka määritys esiteltiin jo aiemminkin:
public interface Comparable<T> {
public int compareTo(T o);
}
Tämän rajapinnan funktion compareTo
rooli on määrittää luokan olioille luonnollinen järjestys.
Esimerkiksi Javan lajittelufunktioita Collections.sort(List<T> list)
ja
Arrays.sort(Object[] a)
tai järjestettyä joukkoa TreeSet<T>
voi käyttää antamatta erillistä
vertailufunktiota ainoastaan, jos käsiteltävien alkioiden luokka toteuttaa rajapinnan
Comparable
. Tällöin alkioiden vertailu tehdään rajapinnan Comparable
edellyttämällä
funktiolla compareTo
.
Alla on esimerkkinä yksinkertaistettu versio aiemmasta koordinaattipisteen esittämiseen sopivasta
luokasta Point2D
, joka toteuttaa rajapinnan Comparable<Point2D>
. Luokkaan on toteutettu
funktio compareTo(Point2D other)
, joka vertaa Point2D
-oliota itseään toiseen
Point2D
-olioon other
. Tässä esimerkissä toteutettu vertailu pohjautuu ensisijaisesti x
-
ja toissijaisesti y
-koordinaattiin, ja näiden arvojen vertailussa hyödynnetään
Double
-luokan tarjoamaa valmista vertailufunktiota Double.compare
.
public class Point2D implements Comparable<Point2D> { // Voi verrata toiseen Point2D-olioon.
private double x;
private double y;
public Point2D(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
// Toteutetaan rajapinnan Comparable<Point2D> vaatima compareTo-funktio.
@Override
public int compareTo(Point2D other) {
int cmp = Double.compare(x, other.x); // Ensin x-koordinaattien vertailu.
if(cmp == 0) {
cmp = Double.compare(y, other.y); // Tarvittaessa y-koordinaattien vertailu.
}
return cmp;
}
}
Seuraava koodinpätkä havainnollistaa, kuinka Point2D
-olioita voisi nyt esim. lajitella ilman
erillistä vertailufunktiota.
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Arrays.sort(ps); // Ei tarvitse antaa erillistä vertailufunktiota.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Koodinpätkän suoritus tulostaisi jokseenkin:
(-3.5, 4.5) (0.5, 0.5) (2.5, -1.5)
Rajapinta Comparator<T>
¶
Toinen alkioden järjestämisessä usein hyödynnetty Javan luokkakirjaston rajapinta on
Comparator<T>
. Alla on esitetty sen määritys, josta on jätetty pois oletus- ja
staattiset funktiot.
public interface Comparator<T> {
int compare(T a, T b);
boolean equals(Object obj);
}
Rajapinnan oleellisin osa on funktio int compare(T a, T b)
. Rajapinta Comparator<T>
määrittää efektiivisesti vertailuolion tyypin: tyyppiä Comparator<T>
olevan (eli sen
toteuttavan luokan) olion funktiolla compare
voidaan vertailla kahta tyypin``T`` oliota a
ja b
keskenään. Erillisen vertailufunktion hyväksyvät Javan lajittelufunktiot sekä järjestystä
ylläpitävät säiliöt ottavat vertailufunktion nimenomaan rajapinnan Comparator
toteuttavan
luokan oliona.
Vertailuolio vs. lambda-funktiona annettu vertailufunktio?
Lambda-funktiolla määritetyn vertailufunktion käyttö ei ole poikkeus edellämainittuun sääntöön:
myös lambda-funktiota käytettäessä lajittelufunktio saa parametrikseen rajapinnan Comparator
toteuttavan vertailuolion. Kun annamme lajittelufunktiolle parametriksi lambda-funktion, luo
Java sen pohjalta automaattisesti rajapinnan Comparator
toteuttavan olion, jolla on
lambda-funktion kuvaamat askeleet suorittava jäsenfunktio compare
. Tähän palataan tarkemmin
funktionaalisten rajapintojen yhteydessä.
Alla on esimerkkinä rajapinnan Comparator<Point2D>
toteuttava luokka CmpPoint2D
, joka
toteuttaa samanlaisen vertailulogiikan kuin ylempänä määritetty jäsenfunktio compareTo
.
public class CmpPoint2D implements Comparator<Point2D> { // Voi verrata kahta Point2D-oliota.
// Toteutetaan rajapinnan Comparator<Point2D> vaatima compare-funktio.
@Override
public int compare(Point2D a, Point2D b) {
int cmp = Double.compare(a.getX(), b.getX());
if(cmp == 0) {
cmp = Double.compare(a.getY(), b.getY());
}
return cmp;
}
}
Ylempänä esitetty Point2D
-olioiden lajiteluesimerkki muuttuisi luokkaa CmpPoint2D
käytettäessä esimerkiksi seuraavaan muotoon:
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Arrays.sort(ps, new CmpPoint2D()); // Annetaan erillinen vertailuolio.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Rajapinnan Comparator<T>
toteuttava luokka on sinänsä aivan tavallinen luokka, jolle voi
halutessaan määrittää muitakin jäseniä. Alla on esimerkkinä sellainen muunnelma luokasta
CmpPoint2D
, jonka rakentimelle voi vertailuolion luonnin yhteydessä antaa vertailulogiikkaa
mukauttavan parametrin: tehdäänkö vertailu tavallisessa vai käänteisessä järjestyksessä.
public class CmpPoint2D implements Comparator<Point2D> { // Voi verrata kahta Point2D-oliota.
// Yksityinen jäsenmuuttuja, joka kertoo vertailujärjestyksen: käänteinen vai ei?
private final boolean reversed;
public CmpPoint2D(boolean reversed) { // Rakennin, joka ottaa jäsenen reversed alustusarvon.
this.reversed = reversed;
}
public CmpPoint2D() { // Parametriton rakennin: oletusjärjestys (reversed = false).
this.reversed = false;
}
@Override
public int compare(Point2D a, Point2D b) {
int cmp = Double.compare(a.getX(), b.getX());
if(cmp == 0) {
cmp = Double.compare(a.getY(), b.getY());
}
return reversed ? -cmp : cmp; // Käännetään tulos päinvastaiseksi, jos reversed on true.
}
}
Tätä muunnelmaa voisi käyttää seuraavan koodinpätkän esittämään tapaan:
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Arrays.sort(ps, new CmpPoint2D(true)); // CmpPoint2D:lle true: saadaan käänteinen järjestys.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Rajapinnat Iterable<E>
ja Iterator<E>
¶
Tarkastellaan seuraavaksi vielä Javan luokkakirjaston rajapintoja Iterable<E>
ja
Iterator<E>
, joihin runsaasti käyttämämme muotoa for(V item : vals)
oleva iteroiva
silmukkakin nojautuu: tällaista silmukkaa voi käyttää jos ja vain jos vals
on joko taulukko tai
rajapinnan Iterable<E>
toteuttava säiliö, jonka alkiot ovat yhteensopivia muuttujan item
tyypin V
kanssa.
Rajapinnan Iterable<E>
määritys, ilman paria sen lisäksi määrittämää oletusfunktiota, näyttää
seuraavalta. Javan luokkakirjastossa tosin käytetään tämän rajapinnan yhteydessä tyyppiparametrin
nimeä T
eikä E
, mutta tämä on vain puhtaasti kosmeettinen seikka.
public interface Iterable<E> {
Iterator<E> iterator();
}
Iterable<E>
määrittää abstraktin jäsenfunktion iterator
, jonka tulee palauttaa rajapinnan
toteuttavan säiliön alkioita iteroiva tyyppiä Iterator<E>
oleva iteraattoriolio. Rajapinnan
Iterator<E>
määritys, jälleen ilman oletusfunktioita, näyttää puolestaan seuraavalta:
public interface Iterator<E> {
boolean hasNext();
E next();
}
Iteraattorin funktion boolean hasNext()
tulee palauttaa tieto siitä, onko iteroitavassa
säiliössä olemassa seuraava vielä iteroimaton alkio (vai onko säiliö jo iteroitu läpi). Funktion
next()
tulee palauttaa seuraava vielä iteroimaton alkio (Javan dokumentaatio lisäksi määrittää,
että se heittää NoSuchElementException
-poikkeuksen, ellei seuraavaa alkiota enää ole). Muotoa
for(V item : vals)
oleva Iterable<E>
-säiliötä iteroiva for-silmukka näyttää pinnan alla
(Java-kääntäjän automaattisesti generoimana) karkeasti seuraavalta:
for(Iterator<E> iterator = vals.iterator(); iterator.hasNext(); ) {
V item = iterator.next();
// Tämän jälkeen silmukan rungon askeleet.
}
Yhteenveto edellisistä on, että jos itse toteutettua säiliötä halutaan käyttää iteroivassa
for-silmukassa, tulee säiliön toteuttaa rajapinta Iterable<E>
, ja sen oheen (yleensä
sisään) tulee lisäksi toteuttaa sopiva iteraattoriluokka. Jotta iteraattoriolio voisi
lukea säiliön alkioita, tulee sen yleensä ottaen sisältää jonkinlainen viite iteroitavaan
säiliöön (linkitetyn rakenteen kohdalla säiliön solmuun) sekä mahdollisesti lisätietoa siitä,
missä kohtaa iterointi etenee. Iteraattoriluokka on tämän vuoksi luontevaa toteuttaa
ei-staattisena sisäisenä luokkana: ei-staattisen sisäisen luokan oliot voivat viitata suoraan
(ilman erillistä viitemekanismia) ympäröivän luokan sen olion (eli tässä kontekstissa säiliön)
jäseniin, jonka puitteissa sisäisen luokan olio on luotu.
Alla on hahmoteltu, miten geneeriseen esimerkkiluokkaamme LinkedStack<E>
voitaisiin lisätä
tuki iteroinnille. Tässä on toiston välttämisen vuoksi näytetty vain ne osat, jotka tulee lisätä
aiemmin esitettyyn toteutukseen. Jos haluat kokeilla tätä koodia käytännössä, copy-pasteta koodit
yhteen. Tässä toteutettu iteraattori askeltaa pinon alkiot läpi muuttamatta pinoa.
public class LinkedStack<E> implements Iterable<E> { // Nyt toteutetaan Iterable<E>
// Sisäinen iteraattoriluokka. Voi olla yksityinenkin.
private class LSIterator implements Iterator<E> {
// Viite seuraavaksi iterointivuorossa olevaan solmuun. Alussa top.
private Node nextNode = top; // Voi suoraan viitata isäntäsäiliön jäseneen top.
// Rajapinnan Iterator<E> vaatimat funktiot.
@Override
public boolean hasNext() {
return nextNode != null; // Seuraava on jäljellä, ellei nextNode ole null.
}
@Override
public E next() {
if(nextNode == null) { // Heitetään poikkeus, ellei seuraavaa alkiota ole.
throw new NoSuchElementException("No more values in the stack!");
}
E item = nextNode.getItem(); // Palautettava alkio talteen.
nextNode = nextNode.getNext(); // Siirretään nextNode askel eteenpäin.
return item;
}
}
// Rajapinnan Iterable<E> vaatima funktio.
@Override
public Iterator<E> iterator() {
return new LSIterator(); // Palautetaan uusi LSIterator-iteraattoriolio.
}
// Muut jäsenet kuin aiemmin, niitä ei näytetä tässä.
}
Seuraava koodinpätkä testaa edellä toteutettua iteroitavaa pinoa:
LinkedStack<Integer> intStack = new LinkedStack<>();
intStack.push(2022);
intStack.push(12);
intStack.push(6);
for(Integer i : intStack) {
System.out.print(i + " ");
}
Koodinpätkän suoritus tulostaisi:
6 12 2022
Iteraattorin mitätöityminen
Edellisessä iteraattorin toteutuksessa on jätetty huomioimatta sellainen pikantti yksityiskohta, että yleensä iteraattori pitäisi mitätöidä, jos sen iteroimaa säiliötä muutetaan sen jälkeen, kun iteraattori on luotu. Tällainen tilanne voisi syntyä esimerkiksi pinomme tapauksessa seuraavasti:
LinkedStack<Integer> intStack = new LinkedStack<>();
intStack.push(2022);
Iterator<Integer> iterator = intStack.iterator(); // Luodaan pinon iteraattori.
intStack.pop(); // Muutetaan pinoa: nyt edellinen iteraattori mitätöityy.
System.out.format("Next value: %d%n", iterator.next()); // Ei saisi onnistua!
Jos iteraattori on mitätöitynyt, ei sen pitäisi enää suostua lukemaan uusia arvoja. Tämän
tarkoituksena on välttää esimerkiksi yllä kuvattu tilanne, jossa iteraattori yrittäisi
lukea säiliöstä jo poistettuja arvoja. Esimerkiksi Javan ArrayList
-luokan iteraattori heittää
ConcurrentModificationException
-poikkeksen, jos iteraattori on mitätöitynyt.
Iteraattorin mitätöitymisen tarkistuksen voi toteuttaa esimerkiksi ylläpitämällä säiliössä
juoksevaa laskuria, jota kasvatetaan jokaisen muutoksen jälkeen, ja tallettamalla kuhunkin
iteraattoriolioon laskurin arvon kyseisen iteraattorin luontihetkellä. Iteraattori voi havaita
mitätöitymisen vertaamalla omaa laskuriarvoaan laskurin nykyiseen arvoon: jos arvot poikkeavat
toisistaan, on säiliötä muutettu iteraattorin luonnin jälkeen ja iteraattori on mitätöitynyt.
Esimerkiksi ArrayList
-luokan iteraattori on toteutettu tällä tavalla.
Nimettömät luokkamääritykset¶
Javassa on mahdollista määrittää jonkin ylityypin perivä tai toteuttava luokka tavallisen
määrityksen lisäksi myös ns. nimettömänä luokkamäärityksenä suoraan kyseisen luokan olion
luovan new
-operaation yhteydessä. Tämä tapahtuu aivan kuin loisimme kyseisen ylityypin
olion, mutta luokan rakentimelle välitettävän parametrilistan loppusulun perässä annetaankin
luokkaamäärityksen runko (eikä esim. heti puolipistettä). Nimettömän luokkamäärityksen runko voi
olla pitkälti samanlainen kuin tavallisenkin luokkamäärityksen runko, mutta esimerkiksi rakenninta
siihen ei saa määrittää. Jäsenmuuttujat, jotka alustetaan suoraan niiden määritysten yhteydessä,
ovat sallittuja. Nimetön luokkamääritys tyypillisesti määrittää korkeintaan muutaman jäsenfunktion,
jotka esim. toteuttavat rajapinnan vaatimat jäsenfunktiot. Jos nimetön luokkamääritys alkaisi käydä
monimutkaisemmaksi, voisi koodin selkeyden kannalta olla parempi määrittää se tavallisena luokkana.
Nimettömiä luokkamäärityksiä on tyypillisesti käytetty tilanteissa, joissa halutaan luoda jonkin rajapinnan tai abstraktin yliluokan toteuttava olio kertaluontoiseen käyttöön. Aiemmissa Javan versioissa, jotka eivät vielä tukeneet lambda-funktioita, nimettömiä luokkamäärityksiä käytettiin usein lajittelun vertailuolion määrittämiseen suoraan lajittelufunktion kutsussa.
Alla on esimerkki, kuinka Point2D
-olioita voitaisiin lajitella antamalla lajittelufunktiolle
nimettömällä luokkamäärityksellä luotu Comparator<Point2D>
-olio. Huomannet, että luokan runko
on identtinen ensimmäisen aiemmin annetun Comparator<Point2D>
-luokan määrityksen kanssa.
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Arrays.sort(ps, new Comparator<Point2D>(){ // Nimetön Comparator<Double>:n toteuttava luokka.
// Toteutetaan rajapinnan Comparator<Point2D> vaatima compare-funktio.
@Override
public int compare(Point2D a, Point2D b) {
int cmp = Double.compare(a.getX(), b.getX());
if(cmp == 0) {
cmp = Double.compare(a.getY(), b.getY());
}
return cmp;
}
}); // Luokkamäärityksen loppusulku ja sort-kutsun loppusulku.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Nimettömällä luokkamäärityksellä luodun olion ei tarvitse olla kertakäyttöinen: olion voi asettaa
muuttujaan. Alla on esimerkki, joka on muuten samanlainen kuin edellä, mutta nyt vertailuolio
asetetaan muuttujaan comparator
. Tällöin samaa vertailuoliota voisi käyttää monessa eri
lajittelukutsussa.
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Comparator<Point2D> comparator = new Comparator<>(){ // Vertailuolio muuttujaan comparator.
@Override
public int compare(Point2D a, Point2D b) {
int cmp = Double.compare(a.getX(), b.getX());
if(cmp == 0) {
cmp = Double.compare(a.getY(), b.getY());
}
return cmp;
}
};
Arrays.sort(ps, comparator); // Annetaan vertailuolio comparator parametrina.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Vaikka edellä suoritetut lajittelut tehtäneen nykyisin useimmiten lambda-funktion avulla, on hyvä huomata, että nimettömät luokkamääritykset ovat lambda-funktioita selvästi monipuolisempia: lambda-funktio määrittää aina täsmälleen yhden funktion (joka riittääkin esim. lajitteluun), mutta nimetön luokkamääritys voi sisältää monen jäsenfunktion toteutukset sekä uusia jäsenmuuttujia. Näin ollen lambda-funktiot eivät täysin korvaa nimettomiä luokkamäärityksiä.