- COMP.CS.140
- 4. Suuren ohjelman toteuttaminen
- 4.2 Javan luokkien perusteet
- 4.2.1 Java-luokan perusrakenne
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
jaMAX_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. lajittelufunktioArrays.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 euklidisen 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, 1)
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
.