Rajapinta
osaamistavoitteet
- Ymmärrät, mitä rajapinta (interface) tarkoittaa olio-ohjelmoinnissa.
- Osaat määritellä ja käyttää rajapintoja Javassa.
- Osaat käyttää rajapintaa aliohjelman parametrina ja muuttujan tyyppinä.
- Ymmärrät, milloin kannattaa käyttää rajapintaa perinnän sijaan.
- Ymmärrät, että luokka voi toteuttaa monta rajapintaa, mutta periä vain yhden luokan
Rajapinta toimii sitovana sopimuksena: Se määrittelee, mitä metodeja luokan on tarjottava, ottamatta kantaa siihen, miten ne on teknisesti toteutettu. Toisin kuin abstrakti luokka, joka luo pohjan luokan metodeille ja attribuuteille, rajapinta keskittyy kuvailemaan olion kyvykkyyksiä. Rajapinta mahdollistaa yhtenevän kyvykkyyksien määrittelyn, vaikka luokat olisivat täysin erilaisia tai periytyisivät eri paikoista luokkahierarkiassa. Kun ohjelmoija sitten käsittelee oliota rajapinnan kautta, hän voi luottaa siihen, että olio tarjoaa sovitun kyvykkyyden riippumatta siitä, mitä luokkaa olio edustaa.
Älykoti: säädettävät laitteet
Jatketaan Osassa 3
aloittamaamme
älykoti-esimerkkiä. Jotkin älykotimme laitteet voisivat olla säädettäviä, eli
niihin voisi asettaa suoraan arvon, kuten kirkkauden, lämpötilan tai
äänenvoimakkuuden. Näinhän periaatteessa toimimmekin jo esimerkkimme
Valo-luokassa, jossa kirkkaus vaihtelee kolmen arvon välillä. Olion käyttäjän
kannalta olisi kuitenkin kätevämpää, jos voisi asettaa kirkkauden suoraan
haluttuun arvoon (esim. 33%), sen sijaan, että pitäisi kutsua
vaihdaTilaa()-metodia useita kertoja ja toivoa, että arvo osuu kohdalleen.
Loppukäyttäjän kannalta tätä voisi verrata tilanteeseen, jossa käyttäjä voisi
asettaa vaikkapa mobiilisovelluksesta suoraan haluamansa kirkkauden sen sijaan,
että pitäisi klikkailla Lisää kirkkautta- tai Vähennä kirkkautta
-painikkeita useita kertoja.
Määritellään rajapinta Saadettava, jossa on metodi asetaArvo(int arvo).
Tiedosto tallennetaan nimellä Saadettava.java, eli samaan tapaan kuin luokat.
/**
* Laite, jonka voi säätää suoraan haluttuun arvoon.
*/
public interface Saadettava {
void asetaArvo(int arvo);
}
Tämän voi lukea seuraavasti: Jokaisella Saadettava-rajapinnan toteuttavalla
luokalla tulee olla asetaArvo-metodi.
Nyt voimme muokata Valo-luokkaa toteuttamaan Saadettava-rajapinnan:
Lisätään Valo-luokkaan rajapinnan toteutus (klikkaa Valo.java-tiedostoa).
Jätämme Kahvinkeitin- ja Turvakamera-luokat tässä vaiheessa esimerkistä
pois, koska päätämme yksinkertaisuuden vuoksi, että ne eivät ole säädettäviä
laitteita.
public class Main {
public static void main() {
Valo valo = new Valo("PhilipsHue");
valo.asetaArvo(33);
valo.raportoiTila();
valo.vaihdaTilaa();
valo.raportoiTila();
}
}
Luokkakaaviona esimerkkimme näyttäisi tältä. I-kirjain ilmaisee, että kyseessä on rajapinta. Abstraktin luokan tapaan rajapinta on merkitty kursiivilla. Rajapinnan toteuttaminen esitetään katkoviivalla, jossa on avoin nuoli kohti rajapintaa.
Usean rajapinnan toteuttaminen
Luokka voi toteuttaa useita rajapintoja. Esimerkiksi Javan sisäänrakennettu
ArrayList-luokka toteuttaa rajapintoja: List, RandomAccess, Cloneable ja
Serializable (ks. ArrayList-luokan
dokumentaatio).
List-rajapinta määrittelee listan perustoiminnot, kuten elementtien lisäämisen, poistamisen ja hakemisen.RandomAccess-rajapinta määrittelee, että listan alkioihin tulee päästä käsiksi nopeasti indeksien avulla.Cloneable-rajapinta sallii olion kloonauksen eli kopioinnin.Serializable-rajapinta sallii olion tallentamisen tiedostoon tai lähettämiseen verkon yli.
Toisaalta myös Javan
Date-luokka
toteuttaa muun muassa Cloneable-rajapinnan, joka mahdollistaa päivämääräolion
kloonaamisen. Huomaa, että Date-luokka ei liity mitenkään
ArrayList-luokkaan, mutta molemmat toteuttavat saman rajapinnan.
Luodaan nyt itse kaksi rajapintaa ja luokkia, jotka toteuttavat molemmat rajapinnat.
Otetaan esimerkki käyttöliittymäkomponenteista, joita voi piirtää näytölle ja
joita voi klikata hiirellä. Määritellään kaksi rajapintaa: Piirrettava ja
Klikattava. Näiden rajapintojen avulla voitaisiin määritellä, millaisia
komponentteja käyttöliittymässä on. Sovitaan niin, että piirrettävä komponentti
osaa piirtää itsensä, ja klikattava komponentti osaa käsitellä klikkauksia ja
korostaa itsensä, kun hiiri on sen päällä.
/**
* Käyttöliittymään piirrettävä komponentti.
*/
public interface Piirrettava {
public void piirra();
}
Huomaa, että emme tiedä emmekä välitä siitä, miten nämä metodit aikanaan
toteutetaan. Piirto voi tapahtua graafisella käyttöliittymällä,
tekstipohjaisella käyttöliittymällä tai vaikkapa tulostamalla tiedostoon. Meille
riittää, että tiedämme, että jokaisella Pirrettava-rajapinnan toteuttavalla
luokalla on piirra()-metodi, ja jokaisella Klikattava-rajapinnan
toteuttavalla luokalla on klikattu()- ja asetaKorostus(boolean korostus)-metodit.
Mennään eteenpäin. Toteutetaan Teksti, joka on pelkkää tekstiä näyttävä
käyttöliittymäkomponentti.
/**
* Pelkkää tekstiä esittävä piirrettävä komponentti.
*/
public class Teksti implements Piirrettava {
private String sisalto;
public Teksti(String sisalto)
{
this.sisalto = sisalto;
}
@Override
public void piirra() {
// Piirretään vain pelkkä tekstisisältö ilman kehyksiä
IO.println(sisalto);
}
}
Rajapintojen hyöty ei vielä kokonaisuudessaan välity, osittain siksi, että
piirra()-metodi on ainoa metodi, jota Piirrettava-rajapinta tarjoaa. Nyt
kuitenkin voimme luoda toisen komponentin, Painike, joka on laatikon näköinen
klikkattava painike, jossa on tekstiä. Painike-luokka toteuttaa molemmat
rajapinnat: Pirrettava ja Klikattava.
/**
* Laatikon näköinen klikkattava painike,
* jossa on tekstiä.
*/
public class Painike implements Piirrettava, Klikattava {
private String sisalto;
private boolean korostettu;
public Painike(String sisalto)
{
this.sisalto = sisalto;
this.korostettu = false;
}
@Override
public void piirra() {
// Piirretään suorakulmio ja teksti
if (!korostettu) {
IO.println("[ " + sisalto + " ]");
} else {
IO.println("[*" + sisalto + "*]");
}
}
/**
* Käsitellään klikkaustapahtuma
*/
@Override
public void klikattu() {
IO.println("(Klikattiin painiketta, jossa lukee \"" + sisalto + "\")");
}
/**
* Asetetaan korostustila. Jos tila muuttuu, piirretään komponentti uudestaan.
*/
@Override
public void asetaKorostus(boolean korostus) {
if (this.korostettu == korostus) {
return;
}
this.korostettu = korostus;
this.piirra();
}
}
Nyt meillä on kaksi erilaista käyttöliittymäkomponenttia, jotka molemmat voidaan
piirtää näytölle. Painike-komponentti on lisäksi klikattava. Käytetään näitä
komponentteja pääohjelmassa.
/**
* Käyttöliittymään piirrettävä komponentti.
*/
public interface Piirrettava {
public void piirra();
}
Jos haluat testata tätä koodia omalla koneellasi, voit ladata tämänkin esimerkin GitHubista.
Valinnaista lisätietoa: Piirtämisvastuun siirtäminen pois komponenteista
Yllä oleva esimerkkimme on siinä mielessä aavistuksen epätodellinen, että käyttöliittymäkomponentit eivät yleensä huolehdi itse itsensä piirtämisestä, vaan piirtämisvastuu on usein erotettu muuhun osaan järjestelmää. Tällöin komponentit vain tarjoavat tiedot, jotka tarvitaan piirtämiseen, ja joku muu osa järjestelmää huolehtii siitä, että komponentit piirretään oikein näytölle (tai muuhun esitystapaan).
Muokataan esimerkkiämme tämän ajatuksen mukaisesti. Tehdään Naytto-luokka,
joka pitää kirjaa kaikista näytöllä näkyvistä käyttöliittymäkomponenteista.
/**
* Naytto-luokka hallinnoi piirrettäviä komponentteja.
*/
public class Naytto {
private ArrayList<Piirrettava> komponentit = new ArrayList<>();
public void lisaaKomponentti(Piirrettava p) {
komponentit.add(p);
}
public void poistaKomponentti(Piirrettava p) {
komponentit.remove(p);
}
}
Tehdään myös Piirturi-luokka, joka toimii välikerroksena Naytto-luokan ja
käyttöliittymäkomponenttien välillä. Piirturi-luokka huolehtii siitä, että
komponentit piirretään oikein näytölle. Tässä esimerkissä ne tulostetaan
konsolille, mutta oikeassa käyttöliittymässä ne piirrettäisiin graafiselle
näytölle.
/**
* Piirturi-luokka vastaa piirtoalueen piirtämisestä.
*/
public class Piirturi {
public void piirraPainike(String teksti, boolean korostettu) {
if (!korostettu) {
IO.println("[ " + teksti + " ]");
} else {
IO.println("[*" + teksti + "*]");
}
}
public void piirraTeksti(String teksti) {
IO.println(teksti);
}
public void tyhjaa() {
IO.println("Tyhjennetään piirtoalue");
// Jätetään tässä toteuttamatta
}
}
Nyt Naytto-luokka voi käyttää Piirturi-luokkaa piirtämään ne tarvittaessa.
Lisätään Naytto-luokkaan metodi paivita(), joka käy läpi kaikki näytöllä
olevat komponentit ja pyytää niitä piirtämään itsensä Piirturi-olion avulla.
import java.util.ArrayList;
/**
* Naytto-luokka hallinnoi piirrettäviä komponentteja.
*/
public class Naytto {
private ArrayList<Piirrettava> komponentit = new ArrayList<>();
// HIGHLIGHT_GREEN_BEGIN
private Piirturi piirturi = new Piirturi();
// HIGHLIGHT_GREEN_END
public void lisaaKomponentti(Piirrettava p) {
komponentit.add(p);
}
public void poistaKomponentti(Piirrettava p) {
komponentit.remove(p);
}
// HIGHLIGHT_GREEN_BEGIN
public void paivita() {
piirturi.tyhjaa();
for (Piirrettava p : komponentit) {
p.piirra(piirturi);
}
}
// HIGHLIGHT_GREEN_END
}
Huomaa, että Piirrettava-rajapinnan piirra()-metodin tulee nyt ottaa
parametrina Piirturi-olio. Tämän avulla komponentit voivat käyttää
Piirturi-oliota piirtämiseen.
public interface Piirrettava {
// HIGHLIGHT_GREEN_BEGIN
public void piirra(Piirturi piirturi);
// HIGHLIGHT_GREEN_END
}
Ja nyt se oleellinen kohta: Tämän seurauksena Teksti- ja Painike-luokkien
piirra()-metodit eivät enää itse tulosta mitään, vaan ne kutsuvat
Piirturi-olion metodeja.
/**
* Pelkkää tekstiä esittävä piirrettävä komponentti.
*/
public class Teksti implements Piirrettava {
private String sisalto;
public Teksti(String sisalto)
{
this.sisalto = sisalto;
}
/**
* Piirrä komponentti
* @param piirturi Piirturi
*/
@Override
// HIGHLIGHT_GREEN_BEGIN
public void piirra(Piirturi piirturi) {
piirturi.piirraTeksti(sisalto);
}
// HIGHLIGHT_GREEN_END
}
Vastaava muutos tulee tehdä Painike-luokkaan.
Tässä meidän yksinkertaisessa esimerkissämme kaikki tietysti tapahtuu konsolille
tulostamalla, mutta oikeassa graafisessa käyttöliittymässä Piirturi-luokka
voisi käyttää jotain graafista kirjastoa, kuten JavaFX:ää tai Swingiä.
Esimerkki on pitkähkö, ja jos haluat ajaa sen omalla tietokoneellasi, lataa se GitHubista.
Rajapinnan periminen
Rajapinta voi myös laajentaa (periä) toista rajapintaa. Syntaktisesti tämä
tapahtuu käyttämällä extends-avainsanaa, kuten luokkien perinnässä. Luokkien
perinnästä poiketen rajapinta voi periä useita rajapintoja. Alirajapinta saa
kaikki ylirajapinnan metodit. Alla synteettinen esimerkki.
Esimerkit
Löydät kaikki tällä sivulla esitellyt esimerkit GitHubista (E34-alkuiset kansiot).
Huomautuksia
Valinnaista lisätietoa: Javan versiosta 8 alkaen rajapinnat voivat sisältää myös metodien oletustoteutuksia. Ominaisuus saattaa olla hyödyllinen esimerkiksi tilanteissa, jossa halutaan lisätä uusi metodi olemassa olevaan rajapintaan rikkomatta vanhoja toteutuksia. Lue aiheesta lisää Javan dokumentaatiosta.
Tehtävät
-
Luo rajapinta nimeltään
Muunnin. Määrittele rajapintaan yksi metodi:String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita{}). -
Tee luokat
PienetKirjaimet,IsotKirjaimetjaIsoAlkukirjain, jotka toteuttavatMuunnin-rajapinnan.
PienetKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi.muunna("Hei Maa")-->"hei maa".IsotKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon suuraakkosiksi.muunna("Hei Maa")-->"HEI MAA".IsoAlkukirjain-luokanmuunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä.muunna("HEI MAA")-->"Hei maa".
- Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Vakoojat lähettävät viestejä toisilleen, mutta salausmenetelmä vaihtuu päivittäin, jotta vihollinen ei pääse perille logiikasta. Tarvitsemme rajapinnan, jonka avulla voimme vaihtaa salausalgoritmia lennosta.
- Luo rajapinta
Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
- Toteuta kolme erilaista luokkaa:
Kaantaja,HakkerijaSeuraavaKirjain, jotka toteuttavatSalaaja-rajapinnan seuraavilla logiikoilla:
-
Kaantaja(Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttääStringBuilder-luokanreverse()-komentoa tai silmukkaa, joka käy sanan läpi lopusta alkuun. -
Hakkeri("Leet-speak"). Korvaa tietyt kirjaimet numeroilla tai merkeillä. Esimerkki: "Agentti" -> "@g3ntt!"
Korvaa 'a' -> '@'
Korvaa 'e' -> '3'
Korvaa 'i' -> '!'
Korvaa 'o' -> '0'
SeuraavaKirjain(Caesar-siirros). Jokaista kirjainta siirretään aakkosissa yksi eteenpäin. Esimerkki: abc -> bcd. Vihje: Javassacharon luku. Voit tehdämerkki + 1.
'a' -> 'b'
'b' -> 'c'
'k' -> 'l'
jne.
Tässä harjoituksessa ei tarvitse huolehtia ö-kirjaimen pyörähtämisestä ympäri,
ellei halua. Tehtävässä ei myöskään tarvitse huolehtia siitä, että salauksen ja
purkamisen jälkeen saatu viesti ei välttämättä ole samanlainen kuin alkuperäinen
viesti. Esimerkiksi jos Hakkeri-muuntajaa käytettäessä alkuperäisessä
viestissä on oikeasti merkki @, pura-metodi antaa tulokseksi tuohon paikalle
merkin a. Tämä ei haittaa tässä, mutta tietenkin oikeassa salauksessa pitäisi
varmistaa, ettei tietoa katoa tai muutu vahingossa.
Saat TIMissä valmiina pääohjelman, jonka avulla voit testata luokkarakennettasi.