- COMP.CS.140
- 6. Luokkahierarkiat
- 6.3 Rajapinnat Javassa, osa 1
Rajapinnat Javassa, osa 1¶
Rajapinta muistuttaa luokkaa, mutta sitä ei ole tarkoitettu määrittämään kokonaisvaltaista
toiminnallisuutta vaan lähinnä kuvailemaan tyypin ominaisuudet. Rajapinta määrittää tyypin, jota
voi käyttää muuttujien tyyppinä, mutta rajapinnasta itsestään ei voi suoraan luoda olioita vaan
rajapintaa vastaavat oliot ovat aina jonkin kyseisen rajapinnan toteuttavan luokan olioita. Edellä
käytetty ilmaisu “toteuttaminen” on rajapintojen vastine perinnälle. Esimerkiksi luokka
ArrayList
toteuttaa rajapinnan List
.
Rajapinta määritellään lähes täysin samaan tapaan kuin luokka, mutta määrityksessä käytetään
avainsanan class
sijaan avainsanaa interface
. Alla on esimerkkinä kahden Javan
luokkakirjaston tarjoaman varsin yksinkertaisen rajapinnan AutoCloseable
ja Readable
määritykset (joiden tulisi olla eri tiedostoissa AutoCloseable.java
ja Readable.java
ja lisäksi sisältää tarvittavat, tästä pois jätetyt, import
-lauseet):
public interface AutoCloseable {
void close() throws Exception;
}
public interface Readable {
int read(CharBuffer cb) throws IOException;
}
Edellä rajapinta AutoCloseable
määrittää tyypin, jolla on julkinen jäsenfunktio
void close()
, ja rajapinta Readable
tyypin, jolla on julkinen jäsenfunktio
int read(CharBuffer cb)
. Nämä funktiot on kuitenkin ainoastaan esitelty: ne ovat ns.
abstrakteja jäsenfunktioita eli jäsenfunktioita, joille ei ole määritetty toteutusta (funktion
runkoa) vaan ainoastaan funktion otsake. Kun luokka toteuttaa rajapinnan, on sen annettava
konkreettiset toteutukset kaikille toteutettavan rajapinnan määrittämille abstrakteille
jäsenfunktioille. Edelliset rajapintamääritykset eivät erikseen määrittäneet jäsenfunktioitaan
julkisiksi: rajapintojen jäsenfunktiot ovat automaattisesti julkisia, ellei erikseen
toisin määritetä.
Luokka voi toteuttaa yhtäaikaa monta eri rajapintaa. Rajapinnan toteuttaminen ilmaistaan lisäämällä
luokan nimen perään avainsana implements
, jonka perässä luetellaan pilkuilla eroteltuina kaikki
luokan toteuttamat rajapinnat. Jos luokka myös perii jonkin yliluokan, annetaan edellä mainittu
extends
-osuus vasta perityn luokan nimen perässä.
Alla on (sinänsä täysin keinotekoinen) esimerkkikoodi, jossa määritetään ylempänä esitellyt
rajapinnat AutoCloseable
ja/tai Readable
toteuttavat luokat A
, B
ja C
.
Näitä kolmea luokka ei ole tässä määritetty julkisiksi, jotta koodia voisi testata asettamatta
luokkia A
, B
ja C
erillisin tiedostoihin.
class A implements AutoCloseable { // A toteuttaa rajapinnan AutoCloseable.
// Ilmaistaan annotaatiolla @Override, että tässä toteutetaan rajapinnalta "peritty" funktio.
@Override
public void close() throws Exception { // A:n pitää toteuttaa AutoCloseable:n funktio close.
System.out.println("Closing A...");
}
}
class B implements AutoCloseable, Readable { // B toteuttaa AutoCloseable:n ja Readable:n.
@Override
public void close() throws Exception { // Toteutettaa AutoCloseable:n funktio close.
System.out.println("Closing A...");
}
@Override
public int read(CharBuffer cb) throws IOException { // Toteutettava Readable:n funktio read.
System.out.println("B is reading...");
return 0;
}
}
class C implements Readable { // C toteuttaa rajapinnan Readable.
@Override
public int read(CharBuffer cb) throws IOException { // Toteutettava Readable:n funktio read.
System.out.println("C is reading...");
return 0;
}
}
public class InterfaceTest {
public static void main(String[] args) throws Exception {
// A ja B toteuttavat rajapinnan AutoCloseable, joten niitä voidaan käsitellä
// AutoCloseable-olioina. Asetetaan A- ja B-oliot AutoCloseable-taulukkoon.
AutoCloseable[] acs = {new A(), new B()};
for(AutoCloseable ac : acs) {
ac.close(); // Tässä voidaan viitata funktioon close, koska AutoCloseable määrittää sen.
}
// B ja C toteuttavat rajapinnan Readable, joten niitä voidaan käsitellä
// Readable-olioina. Asetetaan B- ja C-oliot Readable-taulukkoon.
Readable[] rs = {new B(), new C()};
CharBuffer cb = CharBuffer.allocate(0); // Tyhjä CharBuffer ihan vain read-kutsua varten.
for(Readable r : rs) {
r.read(cb); // Tässä voidaan viitata funktioon read, koska Readable määrittää sen.
}
}
}
Tarkemmin ottaen Javassa rajapinnan määritys voi sisältää seuraavia osia:
Jäsenmuuttujia, joiden on pakko olla julkisia staattisia vakiojäsenmuuttujia, jotka alustetaan suoraan jäsenmuuttujan määrityksen yhteydessä.
Kääntäjä olettaa automaattisesti määreet
public
static
final
, mutta ne on laillista antaa erikseenkin.Rajapinnat eivät voi määrittää muunlaisia (esim. yksityisiä tai ei-staattisia) jäsenmuuttujia.
Julkisia jäsenfunktioita (rajapinnan jäsenfunktio on ilman erillistä määritystä
public
).Abstrakteja ei-staattisia jäsenfunktioita.
Abstrakti = ei anneta toteusta vaan ainoastaan esitellään funktio (sen otsake ilman runkoa).
Rajapinnan toteuttavan luokan on annettava funktiolle konkreettinen toteutus.
Nämä lienevät rajapintojen tärkein osa. Ne määrittävät, millaisia jäsenfunktioita ja siten millaisia toimintoja rajapinnan toteuttavan luokan tulee tarjota.
Ei-staattisia jäsenfunktioita, joille on annettu oletustoteutus.
Funktion otsakkeen edessä annetaan määre
default
ilmaisemaan, että kyseessä on oletustoteutus.Kokonainen funktiomääritys, joka toteutetaan samaan tapaan kuin tavallisen luokan jäsenfunktio. Huomaa kuitenkin, että koska rajapinta ei voi määrittää ei-staattisia jäsenmuuttujia, ei oletustoteutus voi viitata sellaisiin vaan sen on pitkälti nojauduttava muihin rajapinnan määrittämiin jäsenfunktioihin.
Rajapinnan toteuttava luokka perii oletustoteutuksen (eikä luokan siten ole pakko toteuttaa sitä itse).
Staattisia jäsenfunktioita (joille on annettu toteutus; eivät voi olla abstrakteja).
Toteutus pitkälti kuin tavallisen luokan staattinen jäsenfunktio.
Yksityisiä jäsenfunktioita (annettu eskplisiittisesti määre
private
).Rooli: rajapinnan sisäisiä apufunktioita (käytännössä tarvinnee todella harvoin).
Voivat olla ei-staattisia tai staattisia ja niille on pakko antaa toteutus.
Keskitymme etenkin rajapintojen tärkeimpiin osiin eli abstrakteihin jäsenfunktiohin.
Luokan ja sen toteuttaman rajapinnan suhde muistuttaa luokan ja sen perimän yliluokan suhdetta:
rajapinnan toteuttaminen tarkoittaa, että luokalla on kaikki rajapinnan määrittämät ominaisuudet
ja kyseisen luokan olioita voidaan siten käsitellä toteutetun rajapinnan tyyppisinä olioina. Kuten
alussa todettiin, ei rajapinnasta itsesään voi olla olemassa konkreettisia ilmentymiä. Rajapinnan
tyyppisinä olioina käsittely tarkoittaa käytännössä sitä, että viitemuuttujan tyyppi voi vastata
rajapintaa ja tällaisen viitemuuttujan kautta voidaan viitata kyseisen rajapinnan määrittämiin
jäseniin. Esimerkiksi koska rajapinta AutoCloseable
määrittää jäsenfunktion close
, on
AutoCloseable
-tyyppisen viitteen kautta sallittua kutsua jäsenfunktiota close
.
Rajapinta voi myös periä toisia rajapintoja. Tämä tapahtuu samaan tapaan kuin luokkien kohdallakin
käyttäen avainsanaa extends
, mutta erona vain mahdollisuus periä yhtäaikaa monta rajapintaa.
Perivä rajapinta perii perimiensä rajapintojen kaikki julkiset jäsenet. Tämä tarkoittaa
käytännössä, että useita rajapintoja perivän täyttävän luokan on tarjottava toteutukset kaikkien
perittyjen rajapintojen abstrakteille jäsenfunktioille.
Eräs yksinkertainen esimerkki rajapintojen välisestä perinnästä on rajapinnan AutoCloseable
perivä rajapinta Closeable
.
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
Rajapinta Closeable
määrittää, ja itse asiassa korvaa perimänsä, jäsenfunktion close
.
Funktioiden ainoa ero on, että rajapinta Closeable
tarkentaa funktion close
mahdollisesti
aiheuttaman poikkeuksen tyypiksi IOException
(eikä Exception
). Rajapinnan ei tarvitse antaa
toteutuksia myöskään perimilleen jäsenfunktioille, ja tässäkin close
säilyi abstraktina
jäsenfunktiona, jonka rajapinnan toteuttavan luokan tulee toteuttaa.
Abstraktit luokat¶
Rajapinnan kaltainen vaillinainen tyyppi voidaan määrittää Javassa myös ns. abstraktina luokkana.
Luokka määritetään abtraktiksi lisäämällä sen määrityksessä avainsanan class
eteen määre
abstract
. Abstrakti luokka muistuttaa rajapintaa: abstraktista luokasta ei voi luoda olioita
vaan sitä vastaavat olio ovat aina jonkin sen perivän luokan olioit, ja abstraktilla luokalla voi
olla abstrakteja eli ainoastaan esiteltyjä jäsenfunktioita. Abstrakti luokka on muuten täysin
tavallinen luokka. Rajapinnan ja abstraktin merkittävimmät erot ovat:
Rajapinnan abtraktia jäsenfunktiota ei tarvitse erikseen määrittää abstraktiksi, mutta abstraktin luokan kunkin abstraktin jäsenfunktion eteen pitää lisätä määre
abstract
.Rajapinta toteutetaan avainsanalla
implements
, abstrakti luokka peritään avainsanallaextends
.Luokka voi toteuttaa yhdellä kertaa monta eri rajapintaa mutta periä vain yhden abstraktin luokan.
Abstraktin luokan periminen vastaa säännöiltään tavallista luokkien perintää, joten moniperintä on kielletty. Oikeastaan ainoa ero tavallisen luokan ja abstraktin luokan perinnässä on, että abstraktin luokan perivän luokan tulee joko antaa toteutukset kaikille perityille abstrakteille jäsenfunktioille tai olla itsekin abstrakti luokka.
Abtrakti luokka voi määrittää kaikenlaisia jäseniä, mutta rajapinta on rajoitetumpi (ei voi esimerkiksi määrittää oliokohtaisia jäsenmuuttujia).
Alla on esimerkkinä taulukkosäiliön yliluokaksi tarkoitettu abstrakti luokka AbstractArray
sekä
sen perivä luokka Array
. AbstractArray
jättää perivän luokan vastuulle varsinaisen taulukon
varauksen sekä funktioiden get
, set
ja size
toteutuksen, ja tarjoaa itse esimerkkinä
yhden näiden kolmen funktion varassa toimivan funktion reverse
. Luokat käsittelevät alkoita
Object
-viitteiden kautta eli taulukkoon voi tallettaa kaikentyyppisiä olioita. Esimerkissä on
mukana myös poikkeusten heittäminen, jos koko tai indeksi on laiton. Tässä heitetyt poikkeustyypit
ovat Javassa valmiiksi käytettävissä (kuuluvat pakkaukseen java.lang
).
public abstract class AbstractArray {
public abstract Object get(int i); // Lukee indeksin i alkion.
public abstract void set(int i, Object item); // Asettaa indeksin i alkion.
public abstract int size(); // Palauttaa taulukon koon.
public void reverse() { // Kääntää taulukon alkiot päinvastaiseen järjestykseen.
for(int start = 0, end = size() - 1; start < end; start++, end--) {
Object tmp = get(start);
set(start, get(end));
set(end, tmp);
}
}
}
public class Array extends AbstractArray { // Peritään AbstractArray.
private Object[] items; // Alkiot tallettava Object-taulukko.
private int size; // Taulukon koko.
public Array(int size) { // Rakennin alustaa taulukon parametrin kokoiseksi.
if(size < 0) {
throw new NegativeArraySizeException(String.format("Illegal size: %d", size));
}
items = new Object[size];
this.size = size;
}
public Object get(int i) {
if(i < 0 || i >= size) {
throw new IndexOutOfBoundsException(String.format("Illegal index: %d", i));
}
return items[i];
}
public void set(int i, Object val) {
if(i < 0 || i >= size) {
throw new IndexOutOfBoundsException(String.format("Illegal index: %d", i));
}
items[i] = val;
}
public int size() {
return size;
}
}
Taulukkoa Array
voisi käyttää esimerkiksi seuraavasti:
Array arr = new Array(3);
arr.set(0, "One");
arr.set(1, "Two");
arr.set(2, "Three");
System.out.format("%s %s %s%n", arr.get(0), arr.get(1), arr.get(2));
arr.reverse();
System.out.format("%s %s %s%n", arr.get(0), arr.get(1), arr.get(2));
Esimerkki tulostaisi:
One Two Three
Three Two One
Abstrakteja luokkia käytetään lähinnä sellaisten toistensa kanssa samankaltaisten luokkien
yliluokkana, joille abstrakti yliluokka voi tarjota yhteistä toiminnallisuutta (esimerkiksi
valmiiksi toteutettuja jäsenfunktioita, joita aliluokat voivat hyödyntää). Javan luokkakirjasto
sisältää esimerkiksi abstraktin luokan AbstractList
, joka on tarkoitettu taulukkoon pohjautuvan
lista-säiliön yliluokaksi ja sisältää valmiit toteutukset monelle tällaisen listan jäsenfunktiolle.
Esimerkiksi ArrayList
on luokan AbstractList
aliluokka. Edellisen esimerkin
toimintaperiaate muistutti osin näitä luokkia. ArrayList
ja AbstractList
ovat tarkemmin
ottaen geneerisiä luokkia. Geneerisyyttä käsitellään kohta.
Tyyppimuunnos ja olion tyypin tutkiminen¶
Tässä vaiheessa lienee käynyt selväksi, että alityypin olioon on aina laillista viitata sen
ylityypin mukaisella viitteellä, koska alityypillä on myös ylityyppinsä ominaisuudet. Koska se,
mihin jäseniin viitemuuttujan kautta saa viitata, määräytyy viitteen tyypin perusteella, ei
alityypin oliosta voi tällöin viitata kuin sen ylityypin määrittämiin jäseniin. Esimerkiksi
seuraava ei onnistu, koska ylityypillä Object
ei ole jäsenfunktiota length
:
Object o = "I am a string";
o.length(); // Kääntäjän virheilmoitus: cannot find symbol o.length().
Jotta ylityypin kautta viitatun alityypin omia (ylityypiltä puuttuvia) jäsenfunktioita voisi kutsua, täytyy siihen viitata alityyppiä vastaavalla viitteellä. Tämä vaatii pakotetun tyyppimuunnoksen, koska muunnos ylityypistä alityyppiin ei ole itsestäänselvästi laillinen. Esimerkiksi alla ensimmäinen yritys tuottaisi käännösvirheen mutta toinen onnistuisi:
Object o = "I am a string";
String s = o; // Kääntäjän virheilmoitus: Object cannot be converted to String.
String s = (String) o; // Laillinen, koska o oikeasti on String.
Java tarkistaa viitetyyppien välisen tyyppimuunnoksen laillisuuden ajonaikana. Ellei muunnettu
olio ole oikeasti yhteensopiva kohdetyypin kanssa, aiheutuu ClassCastException
. Esimerkiksi
alla laiton yritys muuntaa String
-olio o
Integer
-tyyppiseksi aiheuttaa poikkeuksen.
Object o = "I am a string";
try {
Integer i = (Integer) o; // Laiton yritys muuntaa String -> Integer.
}
catch(ClassCastException e) {
System.out.println("An illegal cast brought me here...");
}
Tässä on hyvä painottaa, että tyyppimuunnoksen laillisuus riippuu vain olion omasta aidosta tyypistä; sillä ei ole merkitystä, minkätyyppisellä viitteellä olioon viitataan ennen tyyppimunnosta. Lisäksi oleellinen seikka on, ettei viitetyyppien välinen tyyppimuunnos muuta oliota: se muuntaa vain viittauksen tyypin. Olion oma aito luokka ja ominaisuudet säilyvät ennallaan.
Olion tyyppiä voi tarkastella ajonaikana esimerkiksi operaattorilla instanceof
. Muotoa
o instanceof T
olevan operaatio tulos on totuusarvo, joka kertoo, onko olio o
yhteensopiva
tyypin T
kanssa. Tämä ei siis rajoitu pelkästään olion omaan varsinaiseen tyyppiin vaan tulos
on true
myös jos T
on olion o
ylityyppi. Olion o
tyyppimuunnos tyyppiin T
on
laillinen jos ja vain jos o instanceof T
on true
.
Tarkastellaan esimerkiksi Float
-olion kanssa yhteensopivia tyyppejä. Luokalla Float
on
yliluokat Number
ja Object
ja se toteuttaa mm. rajapinnan Constable
(joka on
lähinnä Java-virtuaalikoneen vakioarvojen esittämiseen liittyvä rajapinta). Alla tutkitaan
Float
-olion yhteensopivuus näiden tyyppien sekä luokan Double
kanssa. Tulostuksessa
käytetty määre %b
vastaa totuusarvoa.
Object o = 3.14F;
System.out.format("o instanceof Object: %b%n", o instanceof Object);
System.out.format("o instanceof Number: %b%n", o instanceof Number);
System.out.format("o instanceof Float: %b%n", o instanceof Float);
System.out.format("o instanceof Constable: %b%n", o instanceof Constable);
System.out.format("o instanceof Double: %b%n", o instanceof Double);
Tämä koodinpätkä tulostaisi:
o instanceof Object: true
o instanceof Number: true
o instanceof Float: true
o instanceof Constable: true
o instanceof Double: false
Näistä viimeisin kuvastaa sitä, kuinka viitetyyppien väliset tyyppimuunnokset pohjautuvat
tyyppihierarkiaan eikä olioiden arvojen periaatteelliseen yhteensopivuuteen. Siksi esimerkiksi
alla ensimmäinen tyyppimuunnos epäonnistuu (f instanceof Double
on false
), mutta
jälkimmäinen automaattisten boxing-muunnosten kautta primitiiviarvojen välisenä tyyppimuunnoksena
tapahtuva muunnos onnistuu:
Float f = 3.14F;
Double d = (Double) f; // Käännösvirhe: Float cannot be converted to Double.
Double d = (double) f; // Ok: Float -> float -> double -> Double.
Koodausdemo (kesto 1:07:01)