Java-luokan perusrakenne

Tarkastellaan seuraavaksi tarkemmin tavanomaisen Java-luokan rakennetta. Tässäkään ei kuitenkaan vielä oteta kantaa luokkien hienostuneempiin ominaisuuksiin.

Java-luokkien korkean tason rakenne muistuttaa varsin pitkälti C++-luokkia. Luokkamääritys voi alkaa määreillä, jotka määrittävät esim. luokan näkyvyyttä tai tiettyjä erityisominaisuuksia. Päätason luokan näkyvyysmääre voi olla:

  • public: luokkaan voi viitata kaikista luokista.

  • Tyhjä (ei näkyvyysmäärettä): luokkaan voi viitata vain sen kanssa samassa pakkauksessa määritetyistä luokista. Pakkauksia käsitellään kohta tarkemmin.

Luokan määreiden jälkeen tulee avainsana class ja luokan nimi, ja tätä seuraa aaltosulkujen rajaama luokan runko, jonka sisällä määritetään luokan jäsenet. Javassa näkyvyysmääre annetaan jokaisen jäsenen kohdalla erikseen (vrt. C++:n näkyvyyslohkot). Jäsenen (esim. myös jäsenluokan!) näkyvyysmääre voi olla:

  • public: jäseneen voi viitata kaikkialta.

  • protected: jäseneen voi viitata saman pakkauksen sisältä tai perivästä/toteuttavasta aliluokasta.

    • Perintään ja aliluokkiin palataan myöhemmin.

  • private: jäseneen voi viitata luokasta itsestään, ja jos kyseessä on sisäinen luokka, myös kaikista sen sisältävistä ulommista luokista.

  • Tyhjä (ei näkyvyysmäärettä): jäseneen voi viitata saman pakkauksen sisältä.

Luokalla voi olla seuraavanlaisia osia:

  • Jäsenmuuttujia, joita on kahta tyyppiä:

    • Oliokohtainen jäsenmuuttuja (≈ “tavallinen” jäsenmuuttuja):

      • Tallettaa oliokohtaista dataa (jokaisella oliolla on muuttujasta oma, muista olioista riippumaton ilmentymä).

    • Luokkakohtainen jäsenmuuttuja (ns. staattinen jäsenmuuttuja tai luokkamuuttuja):

      • Määritetään antamalla jäsenmuuttujamäärityksen edessä lisämääre static. Eli samalla tavalla kuin C++-kielessäkin.

      • Tallettaa luokkakohtaista dataa (muuttujasta on olemassa vain yksi, luokkaan itseensä sidottu, ilmentymä).

      • Käytetään usein esimerkiksi yleiskäyttöisten vakioarvojen määrittämiseen. Esimerkiksi kullakin primitiivilukutyypin kääreluokalla on mm. julkiset vakioluokkamuuttujat MIN_VALUE ja MAX_VALUE, jotka kertovat kyseisen lukutyypin pienimmän ja suurimman mahdollisen arvon.

      • Luokkamuuttujaan tulisi viitata luokan kautta (esim. tapaan Integer.MIN_VALUE). Olionkin kautta voi viitata, mutta sitä pidetään huonona ohjelmointityylinä.

  • Rakentimia.

    • Erityisiä funktioita, joille ei määritetä palautusarvon tyyppiä ja joiden nimi on sama kuin luokan nimi.

    • Suorittuvat, kun uusi luokan olio luodaan new-operaattorilla. Rakenninta ei voi kutsua tavallisen funktion tapaan.

    • Tehtävänä alustaa luokan olio (esim. asettaa jäsenmuuttujiin sopivia arvoja tms.).

  • Jäsenfunktioita, joita on kahta tyyppiä:

    • Oliokohtainen jäsenfunktio (≈ “tavallinen” jäsenfunktio):

      • Funktion kutsu on aina sidottu johonkin olemassaolevaan olioon. Funktiolla on käytettävissä kyseiseen olioon viittaava this-viite.

      • Voi viitata saman luokan sekä olio- että luokkakohtaisiin jäseniin.

      • Yleensä jollain tavalla lukee tai muokkaa oliokohtaisia jäseniä (muuten funktion ei tarvitsisi olla oliokohtainen jäsenfunktio?).

    • Luokkakohtainen jäsenfunktio (ns. staattinen jäsenfunktio tai luokkafunktio):

      • Määritetään antamalla jäsenfunktiomäärityksen edessä lisämääre static.

      • Voi viitata saman luokan luokkakohtaisiin jäseniin.

      • Ei ole sidottu mihinkään olioon. Ei esim. ole this-viitettä eikä voi tehdä suoria viittauksia luokan oliokohtaisiin jäseniin.

      • Käytetään varsinkin yleiskäyttöisten apufunktioiden toteuttamiseen. Esimerkiksi Javan luokka java.util.Arrays ei sisällä oikeastaan mitään muuta, kuin luokkafunktioina toteutettuja apufunktioita taulukoiden käsittelyyn (mm. lajittelufunktio Arrays.sort).

      • Luokkafunktioon tulisi viitata luokan kautta (esim. tapaan Integer.parseInt).

  • Jäsenluokkia (= sisäisiä luokkia).

    • Luokan sisällä annettu luokkamääritys. Sisäisen luokan määritys tapahtuu pitkälti samalla tavalla kuin päätason luokankin, mutta määreitä voidaan käyttää hieman eri tavoin (päätason luokka ei esim. voi olla yksityinen, mutta sisäinen luokka voi).

    • Yleensä jonkinlaisia isäntäluokkansa hyödyntämiä apuluokkia, joita ei ole tarkoitettu yleiskäyttöisiksi luokiksi vaan nimenomaan isäntäluokkansa yhteydessä käytettäviksi.

      • Yleiskäyttöiset luokat on yleensä tarkoituksenmukaista määrittää päätason luokkina.

    • Ulkoisesta luokasta voi viitata suoraan kaikkiin (myös yksityisiin) sen sisäisen luokan jäseniin.

    • Sisäisen luokan oikeudet ulkoiseen luokkaan nähden riippuvat siitä, onko sisäinen luokka staattinen (eli määritetty määreellä static) vai ei.

      • Staattinen sisäinen luokka ei saa mitään erityisoikeuksia ulkoiseen luokkaan: sen sisältä ei voi viitata ulkoisen luokan yksityisiin jäseniin.

      • Ei-staattinen sisäinen luokka saa viitata kaikkiin (myös yksityisiin) ulkoisen luokan jäseniin.

  • Jäsenrajapintoja (= sisäisiä rajapintoja).

    • Luokan sisällä annettu rajapintamääritys. Olemus pitkälti analoginen sisäisen luokan kanssa.

    • Rajapinnoista lisää myöhemmin.

Ensimmäinen esimerkkiluokka Point2D on yksinkertainen kaksiulotteisen avaruuden pistettä kuvaava luokka. Se tallettaa datan yksityisiin jäsenmuuttujiin ja tarjoaa rakentimen sekä jäsenfunktiot datan lukemiseen ja asettamiseen. Javassa on vallitseva käytäntö, että yksityistä jäsenmuuttujaa member koskevat luku- ja asetusfunktiot nimetään muodossa getMember ja setMember. Tässä esimerkkiluokassa on lisäksi yksi staattinen jäsenfunktio, joka laskee kahden sille parametrina annetun pisteen välisen euklisiden etäisyyden.

public class Point2D {
  private double x;
  private double y;

  public Point2D(double x, double y) {
    this.x = x;
    this.y = y;
  }

  // Rinnalle myös toinen rakennin, joka ei vaadi parametreja. Toimii kuin Point2D(0, 0).
  public Point2D() {
    this(0, 0);  // Rakennin voi kutsua toista rakenninta this-viitteen kautta.
  }

  public double getX() {
    return x;
  }

  public void setX(double x) {
    this.x = x;
  }

  public double getY() {
    return y;
  }

  public void setY(double y) {
    this.y = y;
  }

  public static double distance(Point2D a, Point2D b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return Math.sqrt(dx*dx + dy*dy);
  }
}

Huomaa esimerkissä, kuinka usean rinnakkaisen (eli kuormitetun) rakentimen tapauksessa yksi rakennin voi kutsua toista rakenninta. Jos näin tekee, pitää toisen rakentimen kutsu tehdä heti rakentimen rungon alussa. Yllä kuvattu rinnakkaisten rakentimien toteutustapa on Javassa kohtalaisen yleinen, koska Javassa funktioparametreille ei voi määrittää oletusarvoja. Esimerkiksi C++:ssa vastaava toiminnallisuus voitaisiin toteuttaa oletusarvojen avulla tapaan Point2D(double x = 0, double y = 0).

Luokkaa Point2D voisi käyttää esimerkiksi seuraavasti:

Point2D a = new Point2D(1, 2);           // a = koordinaattipiste (1, 2)
Point2D b = new Point2D(2, 1);           // b = koordinaattipiste (2, 1)
Point2D c = new Point2D();               // c = koordinaattipiste (0, 0)
double distAB = Point2D.distance(a, b);  // distAB = 1.4142135623730951
double distAC = Point2D.distance(a, c);  // distAC = 2.23606797749979
a.setY(5);                               // a = koordinaattipiste (1, 5)
b.setX(4);                               // b = koordinaattipiste (4, 2)
distAB = Point2D.distance(a, b);         // distAB = 5.0

Alla on toisena esimerkkinä yksinkertaisen linkitetyn pinon toteuttava luokka LinkedStack, joka sisältää edellämainittuja elementtejä rajapintoja sekä staattisia jäseniä (paitsi main) lukuunottamatta. Pino on listamainen tietorakenne, jonka luku- ja kirjoitusoperaatiot kohdistuvat vain listan loppuun (“pinon päälle”). Pinon perusoperaatiot ovat (1) alkion lisääminen pinon päälle, (2) pinon päällimmäisen alkion arvon lukeminen ja (3) pinon päällimmäisen alkion poisto. Esimerkissä käsitellään alkioita Object-tyyppisten viitteiden kautta. Kuten myöhemmin perintää käsittelevässä osiossa tarkemmin todetaan, Javassa kaikkia viitetyyppejä voi käsitellä Object-viitteiden kautta.

// Päätason luokka LinkedStack.
public class LinkedStack {

  // Sisäinen luokka Node, jota pino hyödyntää yksittäisten alkioiden tallennukseen.
  public static class Node {
    // Node-luokan yksityiseksi määritetyt jäsenmuuttujat, joihin
    // tallennetaan seuraava solmu ja tämän solmun alkio.
    private Node next;
    private Object item;  // Object-viite voi viitata kaikentyyppisiin olioihin!

    // Node-luokan rakennin, jolle annetaan parametreina viitteet seuraavaan
    // solmuun sekä alkioon ja joka asettaa ne nyt luotavan Node-olion jäseniin.
    // Rakennin yksityiseksi, jos ulkopuolisten ei haluta voivan luoda solmuja.
    private Node(Node next, Object item) {
      this.next = next;
      this.item = item;
    }

    // Node-luokan jäsenfunktio, joka palauttaa yksityisen next-jäsenen arvon.
    // Tässä näkyvyysmääre on jätetty pois (sellaisen saisi antaa).
    Node getNext() {
      return this.next;
    }

    // Node-luokan jäsenfunktio, joka palauttaa yksityisen item-jäsenen arvon.
    // Tässä näkyvyysmääre on jätetty pois (sellaisen saisi antaa).
    Object getItem() {
      return this.item;
    }
  } // Sisäisen Node-luokan määritys päättyy tähän.

  // LinkedStack-luokan yksityiset jäsenmuuttujat, joihin tallennetaan
  // pinon päällimmäinen solmu sekä pinon koko. Java sallii alustuksen
  // suoraan määrittelyn yhteydessä, mutta ne voisi alustaa myös rakentimessa.
  private Node top = null;
  private int n = 0;

  // Tämä LinkedStack-luokka ei tarvitse rakenninta, koska tarvittavat alustukset
  // onnistuivat suoraan jäsenmuuttujien määritysten yhteydessä. Luokka toimii aivan
  // kuin sillä olisi muotoa public LinkedList() {} oleva mitään tekemätön rakennin.

  // LinkedStack-luokan julkinen jäsenfunktio, joka palauttaa pinon
  // päällimmäisen arvon.
  public Node peek() {
    return this.top;
  }

  // LinkedStack-luokan julkinen jäsenfunktio, joka poistaa ja palauttaa
  // pinon päällimmäisen solmun. Palautetaan null, jos pino oli jo tyhjä.
  public Node pop() {
    Node result = this.top;
    if(this.top != null) { // Poisto tehdään, ellei pino jo ole tyhjä.
      this.n -= 1;
      this.top = this.top.getNext();
    }
    return result;
  }

  // LinkedStack-luokan julkinen jäsenfunktio, joka lisää parametrina
  // saadun alkion pinon päälle.
  public void push(Object item) {
    this.top = new Node(this.top, item);
    this.n += 1;
  }

  // LinkedStack-luokan julkinen jäsenfunktio, joka palauttaa pinon koon.
  public int size() {
    return this.n;
  }
} // C++-ohjelmoijat huomio: Java-luokan loppusulun perään ei kuulu puolipistettä!
  // Java tosin sallii puolipisteen, koska tämä virhe on yleinen C++-taustaisilla.

Alla on yksinkertainen luokkaa LinkedStack käyttävä testiohjelma.

public class StackTest {
  // Testaa luokkaa LinkedStack asettamalla komentoriviparametrit pinoon ja
  // sen jälkeen tulostamalla ja poistamalla päällimmäisen alkion, kunnes
  // pino on tyhjä.
  public static void main(String[] args) {
    LinkedStack stack = new LinkedStack();
    for(String arg: args) {
      stack.push(arg);  // String-tyyppinen arg käy Object-tyyppiseksi parametriksi.
    }
    while(stack.size() > 0) {
      System.out.println("Current stack size: " + stack.size());
      System.out.println("  Top item: " + stack.peek().getItem());
      System.out.println("Now popping the top item.");
      stack.pop();
    }
    System.out.println("Current stack size: " + stack.size());
  }
}

Voit asettaa koodit tiedostoihin LinkedStack.java ja StackTest.java, ja sitten kääntää ne tapaan javac LinkedStack.java StackTest.java. Jos tämän jälkeen ajat ohjelman tapaan java StackTest one two three, tulostuu:

Current stack size: 3
  Top item: three
Now popping the top item.
Current stack size: 2
  Top item: two
Now popping the top item.
Current stack size: 1
  Top item: one
Now popping the top item.
Current stack size: 0

Aiemmin todettiin, että javac-kääntäjä tuottaa kutakin luokkaa kohden erillisen luokkatiedoston. Tämä koskee myös sisäisiä luokkia. Pelkästään tiedoston LinkedStack.java käännös tuottaa luokkatiedostot LinkedStack.class ja LinkedStack$Node.class. Javac-kääntäjä nimeää sisäisiä luokkia vastaavat luokkatiedostot muodossa PäätasonLuokka$SisäinenLuokka.class.