Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

main.java
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ä.

Piirrettava.java
/**
 * 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.

Piirrettava.java
/**
 * 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.

A.java
public interface A {
    void metodiA();
}

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

Tehtävä 4.1: Muunnin. 1 p.
  1. Luo rajapinta nimeltään Muunnin. Määrittele rajapintaan yksi metodi: String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita {}).

  2. Tee luokat PienetKirjaimet, IsotKirjaimet ja IsoAlkukirjain, jotka toteuttavat Muunnin-rajapinnan.

  • PienetKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi. muunna("Hei Maa") --> "hei maa".
  • IsotKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon suuraakkosiksi. muunna("Hei Maa") --> "HEI MAA".
  • IsoAlkukirjain-luokan muunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä. muunna("HEI MAA") --> "Hei maa".
  1. Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Tee tehtävä TIMissä
Tehtävä 4.2: Vakoojien viestijärjestelmä.1 p.

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.

  1. Luo rajapinta Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
  1. Toteuta kolme erilaista luokkaa: Kaantaja, Hakkeri ja SeuraavaKirjain, jotka toteuttavat Salaaja-rajapinnan seuraavilla logiikoilla:
  • Kaantaja (Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttää StringBuilder-luokan reverse()-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: Javassa char on 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.

Tee tehtävä TIMissä