Turha koodi muuttujitta,
ompi onneton ohjelmaksi.
Parametri kutsuun pistä
aliohjelmalle argumentti.
Tarjoappa käyttöön tuota
metodia mielekästä
rutiinia riittävätä
itse tarkoin testattua.
Mitä tässä luvussa käsitellään?
· muuttujat
· malliohjelma jossa tarvitaan välttämättä muuttujia
· oliomuuttujat eli viitemuuttujat
· aliohjelmat, eli funktiot (metodit)
· aliohjelman testaaminen
· erilaiset aliohjelmien kutsumekanismit
· parametrin välitys
· lokaalit muuttujat
· pöytätesti
Syntaksi:
Seuraavassa muut = muuttujan nimi, koostuu kirjaimista,0-9,_, ei ala 0-9
muut.esittely: tyyppi muut = alkuarvo; // 0-1 x =alkuarvo
sijoitus: muut = lauseke;
merkkijonon lukeminen, ks. Syotto-luokka
aliohj.esittely: tyyppi aliohj_nimi(tyypi muut, tyyppi muut); // 0-n x muut
aliohj.kutsu muut = aliohj_nimi(arvo, arvo); // 0-1 x muut=, 0-n x arvo
olion luonti Tyyppi olion_nimi = new Tyyppi(parametrit);
Luvun esimerkkikoodit:
Satunnainen matkaaja ajelee tällä kertaa kotimaassa. Autoillessa hänellä on käytössä Suomen tiekartan GT –karttalehtiä, joiden mittakaava on 1:200000. Viivoittimella hän mittaa kartalta milleinä matkan, jonka hän aikoo ajaa. Ilman matikkapäätä laskut eivät kuitenkaan suju. Siis hän tarvitsee ohjelman, jolla matkat saadaan muutettua kilometreiksi.
Millainen ohjelman toiminta voisi olla? Vaikkapa seuraavanlainen:
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>35[RET]
Matka on luonnossa 7.0 km.
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>352[RET]
Matka on luonnossa 70.4 km.
C:\OMA\MATKAAJA>
Edellisessä toteutuksessa on vielä runsaasti huonoja puolia. Mikäli samalla haluttaisiin laskea useita matkoja, niin olisi kätevämpää kysellä matkoja kunnes kyllästytään laskemaan. Lisäksi olisi ehkä kiva käyttää muitakin mittakaavoja kuin 1:200000. Muutettava matka voitaisiin tarvittaessa antaa jopa ohjelman kutsussa. Voimme lisätä nämä asiat ohjelmaan myöhemmin, kunhan kykymme siihen riittävät. Toteutamme nyt kuitenkin ensin mainitun ohjelman.
Ohjelmamme poikkeaa aikaisemmista esimerkeistä siinä, että nyt ohjelman sisällä tarvitaan muuttuvaa tietoa: matka millimetreinä. Tällaiset muuttuvat tiedot talletetaan ohjelmointikielissä muuttujiin. Muuttuja on koneen muistialueesta varattu tarvittavan kokoinen "muistimöhkäle", johon viitataan käytännössä muuttujan nimellä.
Kone viittaa muistipaikkaan muistipaikan osoitteella. Kääntäjäohjelman tehtävä on muuttaa muuttujien nimiä muistipaikkojen osoitteiksi. Kääntäjälle täytyy kuitenkin kertoa aluksi minkä kokoisia 'möhkäleitä' halutaan käyttää. Esimerkiksi kokonaisluku voidaan tallettaa pienempään tilaan kuin reaaliluku. Mikäli haluaisimme varata vaikkapa muuttujan, jonka nimi olisi matka_mm kokonaisluvuksi, kirjoittaisimme seuraavan Java-kielisen lauseen (muuttujan esittely):
int matka_mm; // yksinkertaisen tarkkuuden kokonaisluku
Pascal –kielen osaajille huomautettakoon, että Pascalissahan esittely oli päinvastoin:
var matka_mm: integer;
Tulos, eli matka kilometreinä voitaisiin laskea muuttujaan matka_km. Tämän muuttujan on kuitenkin oltava reaalilukutyyppinen (ks. esimerkkiajo), koska tulos voi sisältää myös desimaaliosan:
double matka_km; // kaksinkertaisen tarkkuuden reaaliluku
On olemassa myös yksinkertaisen tarkkuuden reaaliluku float, mutta emme tarvitse sitä tällä kurssilla. Samoin kokonaisluvusta voidaan tehdä "tosi lyhyt", "lyhyt" tai "kaksi kertaa isompi":
int matka_km;
short sormia; // max 32767
byte varpaita; // max 127
long valtion_velka_Mmk; // Tarvitaan ISO arvoalue
Muuttujan määritys voisi olla myös
volatile static long sadasosia;
Tulemme kuitenkin aluksi varsin pitkään toimeen pelkästään seuraavilla perustyypeillä:
short - kokonaisluvut –32 768 – 32 767, 16-bit
int – kokonaisluvut -2 147 483 648 - 2 147 483 647, 32-bit
double – reaaliluvut n. 15 desim. –> 1.7e308
char – kirjaimet 16 bit Unicode
boolean - true tai false
Katso lisää Javan tietotyypeistä linkistä:
Ohjelman käyttämä mittakaava kannattaa sijoittaa ehkä vakioksi, tällöin ainakin ohjelman muuttaminen on helpompaa. Samoin vakioksi kannattaa sijoittaa tieto siitä, paljonko yksi km on millimetreinä (1 km = 1000 m, 1 m = 1000 mm). Ohjelmastamme tulee tällöin esimerkiksi seuraavan näköinen:
java-muut\Matka.java - mittakaavamuunnos 1:200000 kartalta
import java.io.*;
/**
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
class Matka {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
public static void main(String[] args) {
int matka_mm;
double matka_km;
// Ohjeet
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
// Syöttöpyyntö ja vastauksen lukeminen
System.out.print("Anna matka millimetreinä>");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
if ( s == null ) return;
if ( s.equals("") ) return;
matka_mm = Integer.parseInt(s);
// Datan käsittely
matka_km = matka_mm*MITTAKAAVA/MM_KM;
// Tulostus
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
}
Lukija huomatkoon, että muuttujien ja vakioiden nimet on pyritty valitsemaan siten, ettei niitä tarvitse paljoa selitellä. Tästä huolimatta isommissa ohjelmissa on tapana kommentoida muuttujan esittelyn viereen muuttujan käyttötarkoitus. Mekin pyrimme tähän myöhemmin.
Valitettavasti Javan suoraan sanoen alkeiskäyttöön kelvottoman IO:n takia ohjelman kohta "Syöttöpyyntö ja vastauksen lukeminen" venähti toivottoman pitkäksi. No toisaalta näemme kohta sitäkin paremmin yleiskäyttöisten alirutiinien hyödyt.
Tehtävä 8.1 Vakion korvaaminen
Kokeile ottaa vakioiden edestä pois sana static. Mitä tällöin tapahtuu ja miksi? Onko final-sanan poistamisella sama vaikutus (palauta ensin static)?
Muuttujien nimissä on sallittuja kaikki kirjaimet (myös skandit, itse asiassa kaikki Unicode-kirjaimet) sekä numerot (0–9) sekä alleviivausviiva (_). Muuttujan nimi ei kuitenkaan saa alkaa numerolla. Muuttujia saa esitellä (declare) useita samalla kertaa, kunhan muuttujien nimet erotetaan toisistaan pilkulla. Yleinen Java-tapa on että muuttujan nimi alkaa pienellä kirjaimella ja sen jälkeen jokainen muuttujan nimessä oleva alkava sana alkaa isolla kirjaimella (parasTulos).
Muuttujan nimi ei myöskään saa olla mikään vakioista (literal):
true false null
eikä mikään seuraavista avainsanoista (keyword):
abstract assert boolean break byte case catch char class const * continue default do |
double else extends final finally float for goto * if implements import instanceof |
int interface long native new package private protected public return short static |
strictfp ** super switch synchronized this throw throws transient try while void volatile |
Tähdellä (*) merkityt sanat on varattu myöhempään käyttöön.
Vaikka muuttujan nimi saakin sisältää skandeja, kannattaa niiden käytöstä pidättäytyä toistaiseksi ainakin luokkien nimissä, koska luokan nimi on samalla tiedoston nimi ja skandit tiedostojen nimissä aiheuttavat edelleen ongelmia.
Javan nimeämiskäytännöistä katso lisää linkistä:
Tehtävä 8.2 Avainsanat
Merkitse edelliseen taulukkoon kunkin avainsanan viereen se, missä kohti monistetta ko. sana on selitetty.
Tehtävä 8.3 Muuttujan nimeäminen
Mitkä seuraavista ovat oikeita muuttujan esittelyjä ja mitkä niistä ovat hyviä:
int o;
int 9_kissaa;
int _9_kissaa;
double pitkä_matka, pitkaMatka;
int i, j, kissojen_maara, kissojenMäärä;
int auto, pyora, juna;
Muuttujalle voidaan antaa ohjelman aikana uusia arvoja käyttäen sijoitusoperaattoria = tai ++,––,+=,–=,*= jne. –operaattoreilla.
Sijoitusmerkin = vasemmalle puolelle tulee muuttujan nimi ja oikealle puolelle mikä tahansa lauseke, joka tuottaa halutun tyyppisen tuloksen (arvon). Lausekkeessa voidaan käyttää mm. operaattoreita +,–,*,/ ja funktiokutsuja. Lausekkeen suoritusjärjestykseen voidaan vaikuttaa suluilla ( ja ):
kenganKoko = 42;
pi = 3.14159265358979323846;
// usein käytetään Math-luokan PI vakiota
pi = Math.PI;
pinta_ala = leveys * pituus;
ympyranAla = pi*r*r;
hypotenuusa = vastainen_kateetti/sin(kulma);
matka_km = matka_mm*MITTAKAAVA/MM_KM;
Seuraava sijoitus on tietenkin mieletön:
r*r = 5.0; /* MIELETÖN USEIMMISSA OHJELMOINTIKIELISSA! */ L
Eli sijoituksessa tulee vasemmalla olla sen muistipaikan nimi, johon sijoitetaan ja oikealla arvo joka sijoitetaan.
Huom! Java–kielessä = merkki EI ole yhtäsuuruusmerkki, vaan nimenomaan sijoitusoperaattori. Yhtäsuuruusmerkki on = =.
Tehtävä 8.4 Muuttujien esittely
Esittele edellisissä sijoitus –esimerkeissä tarvittavat muuttujat.
Muuttujan esittelyn (declaration) yhteydessä muuttujalle voidaan antaa myös alkuarvo (alustus, definition). Muuttujien alustaminen tietyllä arvolla on tärkeää, koska alustamattoman muuttujan arvo saattaa olla hyvinkin satunnainen. Alustamattoman muuttujan käyttö onkin jälleen eräs tyypillinen ohjelmointivirhe. Java-kääntäjä tosin ilmoittaa virheenä jos muuttujaa yritetään käyttää ennenkuin sille on annettu alkuarvo.
int kengan_koko = 32, takin_koko = 52;
double pi = Math.PI, r = 5.0;
Javassa tosiaan on tehty melkoisen vaikeaksi tietojen lukeminen päätteeltä. Monissa muissa kielissä esimerkiksi kokonaisluvun lukemista varten on huomattavasti yksinkertaisemmat rakenteet tarjolla:
scanf("%d",&matka_mm); /* C-kieli */
cin >> matka_mm; // C++ -kieli
readln(matka_mm); // Pascal-kieli
Rehellisyyden nimissä on kyllä sanottava ettei oikeassa elämässä mikään noistakaan ole hyvä käytännön ratkaisu. Jos käyttäjä syöttää muuta kuin kokonaisluvun, on virheestä toipuminen kaikissa esitetyissä kielissä varsin työlästä.
Usein helpoin ratkaisu onkin lukea tieto ensin merkkijonoon ja sitten "kaivaa" merkkijonosta tarvittava informaatio. Tästä saadaan lisäetuna samalla se, että voidaan käsitellä myös muita kuin numeerisia arvoja eikä ohjelmasta tarvitse tehdä sellaista että jokin tietty luku tarkoittaa ohjelman lopettamista:
Javan IO-systeemi on varsin monimutkainen. Sitä ei olekaan suunniteltu aloittelevaa käyttäjää silmällä pitäen, vaan mahdollisimman laajennettavaksi. Sellaiseksi että samoilla luokilla voitaisiin hoitaa tiedon lukeminen tiedostosta ja verkosta.
// Syöttöpyyntö ja vastauksen lukeminen
System.out.print("Anna matka millimetreinä>");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
Alkuun tarvitsemme olion, joka pystyy lukemaan kokonaisen rivin ja tunnistaa meidän puolestamme rivin lopun. Tämä saadaan aikaiseksi yhdistämällä System-luokan olio in lukijaan (InputStreamReader) ja yhdistämällä se puskuroituun lukijaan (BufferedReader):
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
Sama voitaisiin tehdä useammallakin lauseella:
InputStreamReader instream = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(instream);
Tässä tapauksessa emme kuitenkaan tarvitse itse käyttää apuluokkaa instream, joten tyydymme yhden rivin versioon.
Saatu uusi olio in pystyy lukemaan päätteeltä tietoa. Esimerkiksi metodi readLine lukee kokonaisen rivin. Eli käyttäjä syöttää merkkejä päätteelle ja painaa Enter. Jos tulee jokin ongelma syöttövirran kanssa olio heittää poikkeuksen IOException. Tässä tapauksessa emme välitä poikkeuksista muuta kuin, että se on otettava vastaan (catch).
Nyt lohkon
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
jälkeen merkkijono-oliossa s on joko päätteeltä luettu arvo tai mikäli jokin meni vikaan, niin tyhjä merkkijono. Vielä on mahdollista että syöttövirta katkaistiin kesken kaiken. Windows-konsolilla tämä tapahtuu jos painetaan Ctrl-Z ja Unix/Linux-konsolilla Ctrl-C. Tällöin olioviite s ei viittaa mihinkään (sen arvo on null).
Siksipä tutkimmekin seuraavaksi mistä on kyse ja lopetamme ohjelman ilman sen suurempia mukinoita:
if ( s == null ) return;
if ( s.equals("") ) return;
Tuon voi kirjoittaa myös yhdelle riville, koska Javan ||-operaattori (tai) suorittaa totuusarvoista lauseketta vain siihen saakka kunnes totuusarvo selviää:
if ( ( s == null ) || ( s.equals("") ) ) return;
Huomattakoon että myös muoto
if ( ( s == null ) | ( s.equals("") ) ) return;
on syntaktisesti oikein, mutta tarkoittaa hieman eri asiaa. Looginen lopputulos molemmissa on ehdon lausekkeelle sama. Mutta | -operaattorilla molemmat lausekkeet suoritetaan aina. Ja tässä tapauksessa tämä olisi virhe jos s olisi null.
Kaiken edellä mainitun jälkeen meillä on käytössä oliossa s käyttäjän syöttämä merkkijono. Seuraava ongelma on saada tämä merkkijono muutettua numeroksi, jolla voidaan jopa jotakin laskeakin. Kokonaisluvun tapauksessa tämä onnistuu käyttämällä luokkaa Integer ja pyytämällä tätä selvittämään luvun arvon:
matka_mm = Integer.parseInt(s);
Mikäli käyttäjä on kiltisti syöttänyt kokonaisluvun, niin kaikki menee hienosti. Mutta jos käyttäjä antaa merkkijonon, joka on jotakin muuta kuin kokonaisluku, niin silloin parseInt heittää poikkeuksen:
bash-2.05a$ java Matka
Lasken 1:200000.0 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>kolme
Exception in thread "main" java.lang.NumberFormatException: kolme
at java.lang.Integer.parseInt(Integer.java:414)
at java.lang.Integer.parseInt(Integer.java:463)
at Matka.main(Matka.java:32)
bash-2.05a$
Jos haluamme tästäkin siististi selvitä ja vielä ystävällisesti huomauttaa käyttäjälle, tarvitsee muunnoksen ympärille laittaa myös poikkeuskäsittely ja vielä koko lukeminen silmukkaan. Kaikkien näiden muutosten jälkeen pelkkä yhden kokonaisluvun lukeminen viekin jo likemmäksi 20 riviä ja "sotkee" muuten yksinkertaisen ohjelmamme rakenteen lähes täysin.
Tämän takia onkin ilman muuta järkevää eristää lukemiskoodi omaksi metodikseen:
/**
* Kysytään kokonaisluku. Jos annetaan ei-luku, kysytään uudelleen.
* @param kysymys näytölle tulostettava kysymys
* @param oletus arvo jota käytetään jos painetaanpelkkä Ret
* @return käyttäjän kirjoittama kokonaisluku
*/
public static int kysy_int(String kysymys, int oletus)
{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while ( true ) {
System.out.print(kysymys+" >");
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
continue; // jatkaa silmukkaa
}
if ( ( s == null ) || ( s.equals("") ) ) return oletus;
try {
return Integer.parseInt(s);
} catch (NumberFormatException ex) {
System.out.println("Ei numero: " + s);
}
}
}
Nyt omassa ohjelmassamme voidaan korvata "koko hirveä sotku" vain yhdellä rivillä:
matka_mm = kysy_int("Anna matka millimetreinä",0);
Lisäsimme aliohjelmaamme vielä kutsuun yhden parametrin: oletus. Näin voidaan käyttäjälle antaa mahdollisuus painaa pelkästään Enter ja silti saadaan järkevä vastaus.
Tehtävä 8.5 Oletuksen tulostaminen
Lisää apumetodiin kysy_int vielä oletusarvon tulostaminen sulkuihin ennen väkäsen tulostamista. Eli tulostus olisi:
Anna matka millimeterinä (0) >
Seuraava kysymys sitten onkin että mihin tuo apumetodi lue_int kirjoitetaan? Yksinkertainen vaihtoehto on kirjoittaa se joko ennen tai jälkeen main-metodia. Tässä ratkaisussa olisi se huono puoli, että tuo metodi voisi olla käyttökelpoinen vaikka missä ohjelmassa. Siksipä se kannattaa kirjoittaa omaan luokkaansa. Mutta mihin tämä luokka. Yleiskäyttöisyyden nimissä tuo luokka kannattaa kirjoittaa omaan tiedostoonsa.
Kirjoitammekin koodin vaikkapa tiedostoon Syotto.java:
import java.io.*;
/**
* Aliohjelmia tietojen lukemiseen päätteeltä
* @author Vesa Lappalainen
* @version 1.0/08.01.2003
*/
public class Syotto {
/**
* Kysytään kokonaisluku. Jos annetaan ei-luku, kysytään uudelleen.
* @param kysymys näytölle tulostettava kysymys
* @param oletus arvo jota käytetään jos painetaanpelkkä Ret
* @return käyttäjän kirjoittama kokonaisluku
*/
public static int kysy_int(String kysymys, int oletus)
{
...
}
public static void main(String[] args) {
int i;
i = kysy_int("Anna kokonaisluku",12);
System.out.println("Luku oli: " + i);
}
}
Olio-ohjelmoinnin - samoin kun minkä tahansa muun ohjelmoinnin - yksi tavoite on modulaarinen testaus. Eli jokainen palanen testataan - jos suinkin vain mahdollista - omana kokonaisuutenaan. Näin lopullinen ohjelma voidaan koostaa toimiviksi todetuista palikoista.
Syotto-luokkaan on myös kirjoitettu pääohjelma ja nyt testaus voidaan tehdä ensin pelkälle Syotto-luokalle ennen sen liittämistä muuhun ohjelmaan. Komentoriviltä tämä tapahtuisi nyt vaikkapa:
bash-2.05a$ javac Syotto.java
bash-2.05a$ java Syotto
Anna kokonaisluku >
Luku oli: 12
bash-2.05a$ java Syotto
Anna kokonaisluku >392
Luku oli: 392
bash-2.05a$ java Syotto
Anna kokonaisluku >kolme
Ei numero: kolme
Anna kokonaisluku >0
Luku oli: 0
bash-2.05a$
Vaikka tässä tapauksessa luokka testattiinkin lukemalla tieto päätteeltä, ei missään tapauksessa pidä tätä yleistää. Yleensä paras testiohjelma on sellainen, joka automaattisesti kokeilee testattavaa yksikköä (oliota, metodia) niillä arvoilla joilla sitä tulee kuormittaa. Hyvä testiohjelma sitten kertoo millä arvoilla yksikkö toimi kuten pitikin ja millä ei toiminut. Ihminen testaajana on kaikista testaajista huonoin, koska ihminen väsyy ja muutoksen jälkeen helposti laiskuuksissaan jättää testaamatta niillä arvoilla, jotka jo ennen muutosta oli testattu. Kuitenkin muutos saattaa tuottaa virheitä jo testattuun osaan ja siksi testi pitää aina aloittaa aivan alusta jokaisen muutoksen jälkeen.
Nyt kun uusi luokka, tai oikeastaan tässä tapauksessa uusi apumetodi, on huolellisesti testattu, se voidaan ottaa käyttöön. Nyt kun yksinkertaisuuden vuoksi emme vielä käyttäneet paketteja, niin luokka löytyy jos se on samassa hakemistossa kuin sitä käyttävä luokka. Eli ainoa muutos ohjelmakoodiin on kertoa mistä luokasta metodi kysy_int löytyy.
matka_mm = Syotto.kysy_int("Anna matka millimetreinä",0);
Tehtävä 8.6 Muiden tyyppien lukeminen
Tee vastaavasti luokkaan Syotto metodit kysy_double ja kysyString. Tuleeko paljon samanlaista koodia? Kannattaisiko käyttää jotakin hyväksi? Lisää luokan testiohjelmaan testi uusillekin metodeille.
Tehtävä 8.7 Mittakaavan kysyminen
Muuta matka–ohjelmaa siten, että myös mittakaava kysytään käyttäjältä. Mikäli mittakaavaan vastataan pelkkä [RET] pitää mittakaavaksi tulla 1:200000.
C–kielessä osoittimet piti opetella heti ohjelmoinnin alussa, jos halusi tehdä minkäänlaisia järkeviä aliohjelmia. C++:ssa ongelmaa voidaan kiertää viitemuuttujien (references) avulla. Javassa on myös vastaava käsite, eli kaikki Javan olio-muuttujat ovat tosiasiassa viitemuuttujia. Ne ovat kuitenkin tietyssä mielessä perinteisen C:n osoittimen ja C++:n viitteen välimuoto. Javan viitemuuttujan voi laittaa osoittamaan toistakin oliota kesken koodin. C++:n viitemuuttuja osoittaa aina samaan olioon, mihin se luotiin osoittamaan.
Tutkimme seuraavaksi Javan viitemuuttujien käyttäytymistä. Tehdään ohjelma, jossa päällisin puolin näyttäisi olevan kaksi samanlaista merkkijonoa ja kaksi samanlaista kokonaislukuoliota. Merkkijonot ovat Javassa olioita ja merkkijonomuuttujat viitteitä noihin olioihin.
/**
* Tutkitaan olioviitteiden käyttäytymistä
* @author Vesa Lappalainen
* @version 1.0, 08.01.20003
*/
class Jonotesti {
private static void tulosta(boolean b) {
if ( b ) System.out.println("Samat ovat");
else System.out.println("Erilaiset ovat");
}
public static void main(String[] args) {
String s1 = "eka";
String s2 = new String("eka");
tulosta(s1 == s2); // Erilaiset ovat
tulosta(s1.equals(s2)); // Samat ovat
int i1 = 11;
int i2 = 10 + 1;
tulosta(i1 == i2); // Samat ovat
Integer io1 = new Integer(3);
Integer io2 = new Integer(3);
tulosta(io1 == io2); // Erilaiset ovat
tulosta(io1.equals(io2)); // Samat ovat
tulosta(io1.intValue()== io2.intValue()); // Samat ovat
s2 = s1;
tulosta(s1 == s2); // Samat ovat
}
}
Koodiin on rivien viereen kommentoitu mitä mikäkin rivi tulostaisi.
Javassa on kahden tyyppisiä muuttujia, aikaisemmin lueteltuja perustyyppisiä (boolean, char, byte, short, int, long, float, double) muuttujia ja sitten oliomuuttujia. Oliomuuttujat Javassa ovat aina vain viitteitä todellisiin olioihin. Edellisessä esimerkissä muuttujat s1,s2,io1,io2 ovat olioviitteitä. Silti olioviitteistä puhekielessä käytetään helposti nimitystä olio.
Ohjelman kaikki muuttujat ovat lokaaleja muuttujia. Eli ne on esitelty lokaalisti main-metodin sisällä eivätkä "näy" näin ollen main-metodin ulkopuolelle. Tällaisille muuttujille varataan tilaa yleensä kutsupinosta. Kun kaikki muuttujat on esitelty ja alustettu, pino voisi hieman yksinkertaistettuna olla näiden lokaalien muuttujien kohdalta suurin piirtein seuraavan näköinen:
Kuva 8.1 Olioviitteet
Javassa itse olioiden tila varataan muualta dynaamisen muistinhallinnan hoitamalta alueelta. Usein tätä muistia nimitetään keko- tai kasamuistiksi (heap). Kun ohjelmoija pyytää new-operaattorilla uuden olion, muistinhallinta etsii sopivan vapaan muistipaikan ja palauttaa viitteen tähän muistipaikkaan. Todellisuudessa olioviitteet ovat hieman monimutkaisempia. Asiasta voi lukea lisää sivuilta:
Asian ymmärtämiseksi meille kuitenkin riittää yllä piirretty yksinkertaistettu malli.
Vaikka molemmat viitteet s1 ja s2 osoittavat sisällöltään samanlaiseen olioon, palauttaa vertailu
( s1 == s2 ) // onko s1 sama kuin s2, => true tai false
epätoden arvon. Miksikö? Koska vertailussa verrataan muuttujien arvoja, ei niitä arvoja, joihin muuttujat viittaavat. Esimerkissä on kuviteltu että ensimmäinen "eka"-merkkijono olisi sijoittunut muistissa osoitteeseen 8010 ja toinen osoitteeseen 8040. Siis itse asiassa kysytäänkin:
( 8010 == 8040 )
mikä ei ole totta. Javan primitiivityypit sen sijaan sijoittuvat suoraan arvoina pinomuistiin (tai myöhemmin olioiden attribuuttien tapauksessa oliolle varattuun muistialueeseen). Siksi vertailu
( i1 == i2 )
on totta. Merkkijonoja vastaavasti myös kokonaislukuoliot io1 ja io2 käyttäytyvät samalla tavalla. Javassa on kokonaislukuoliot sitä varten, että primitiivityyppejä ei voi tallentaa Javan tietorakenneluokkiin. Piilottamalla primitiivityyppejä "kääreeseen", voidaan näitä "kääreitä" sitten tallentaa tietorakenteisiin.
Jos sijoitetaan "olio" toiseen "olioon", niin tosiasiassa sijoitetaan viitemuuttujien arvoja, eli sijoituksen s2 = s1 jälkeen molemmat merkkijono-olioviitteet "osoittavat" samaan olioon.
Kuva 8.2 Kaksi viitettä samaan olioon
Sijoituksen jälkeen kuvassa muistipaikkaan 8040 ei osoita (viittaa) enää kukaan ja tuo muistipaikka muuttuu "roskaksi". Kun Javan roskienkeruu (garbage-collection, gc) seuraavan kerran käynnistyy, "vapautetaan" tällaiset käyttämättömät muistialueet. Tätä automaattista roskienkeruuta on pidetty yhtenä syynä Javan menestykseen. Samalla täytyy kuitenkin varoittaa että muisti on vain yksi resurssi ja Javassa on automatiikka vain muistin hoitamiseksi. Muut resurssit kuten esimerkiksi tiedostot ja tietokannat pitää edelleen hoitaa samalla huolellisuudella kuin muissakin kielissä. Jopa C++:aa huolellisemmin, koska Javassa ei ole C++:n tapaan automaattisia olioita.
Javan viitemuuttuja voidaan siis laittaa "osoittamaan" milloin tahansa toista oliota. Tämä tapahtuu sijoittamalla viitemuuttujaan joko olemassa olevan olion viite
s2 = s1; // laitetaan s2 viittaamaan samaan paikkaan kuin s1
tai luomalla uusi olio,
String s2 = new String("eka"); // laitetaan s2 viittaamaan uuteen olioon
jolloin new-operaattorin palauttama viite sijoitetaan. Käytännössä Javan viitteet ovat siis oikeastaan osoittimia. Javan viitteillä ei kuitenkaan voi "edetä" C++:n osoittimien tapaan (esim. s1++). Tämä osoitinaritmetiikan puute on toinen Javan hyväksi puoleksi usein mainostettu ominaisuus (tosin ääneen tämä sanotaan "Javassa ei ole osoittimia", lisäksi on tosin totta että Javassa ei todellakaan ole viitteitä tai osoittimia primitiivityyppeihin).
Viitemuuttujan arvo voi olla myös null. Tämä tarkoittaa ettei oliomuuttuja viittaa mihinkään todelliseen olioon ja tällaista viitemuuttujaa ei saa käyttää ennen kuin siihen on sijoitettu jonkin todellisen olion viite. Yksi Java-ohjelmien yleisimmistä virheistä onkin "null pointer reference" kun ohjelmoija ei ole huolellinen viitteiden kanssa.
Hyvin usein pitää siis testata
if ( s1 != null ) { // nyt voi käyttää s1 viitettä huoletta
Eräs ohjelmoinnin tärkeimmistä rakenteista on aliohjelma. C–kielessä kaikkia eri tyyppisiä aliohjelmia nimitetään funktioiksi; joissakin muissa kielissä eri tyyppejä erotetaan eri nimille. Javassa oikeastaan aliohjelmia nimitetään metodeiksi. Kuitenkin kaikkia tähän asti käytettyjä metodeja voidaan suhteellisen hyvällä omallatunnolla nimittää aliohjelmiksi tai C:n tapaan funktioiksi. Aikaisempien esimerkkien metodit nimittäin kaikki ovat olleet static-määreellä varustettuja metodeja ja tällaisten metodien virallinen nimi on luokkametodi. Lisäksi kun esimerkkiemme luokkametodit eivät ole koskeneet mihinkään luokan ominaisuuteen, ei metodeilla ole oikeastaan ollut luokan kanssa muuta tekemistä kuin että ne ovat olleet luokan sisällä. Tällöin niitä voi aivan hyvin kutsua aliohjelmiksi. Luokan merkitys on toistaiseksi ollut vain pitää joukkoa metodeja omassa "nimiavaruudessaan". C++:ssa vastaava rakenne hoidetaankin yleensä käyttäen nimiavaruuksia.
Aliohjelmaa käytetään seuraavissa tapauksissa:
1. Haluttu tehtävä on valmiiksi jonkun toisen kirjoittamana aliohjelmana esimerkiksi standardikirjastossa (y=Math.sin(x))
2. Haluttua tehtävää suoritetaan usein liki samanlaisena joko samassa ohjelmassa tai jossain toisessa ohjelmassa.
3. Haluttu tehtävä muodostaa selvän kokonaisuuden, jonka toiminta on ilmaistavissa muutamalla sanalla riittävän selkeästi (= aliohjelman nimi).
4. Haluttua tehtävää ei juuri sillä hetkellä osata tai viitsitä ohjelmoida. Tällöin määritellään millainen aliohjelma tarvitaan ja kirjoitetaan tarvittavaan kohtaan pelkkä aliohjelman kutsu. Itse aliohjelma voidaan aluksi toteuttaa varsin yksinkertaisena ja korjata myöhemmin tekemään sen varsinainen tehtävä.
5. Rakenne saadaan selkeämmän näköiseksi.
Aliohjelma esitellään vastaavasti kuin "pääohjelmakin", eli Javan main-metodi. Esimerkiksi satunnaisen matkaajan mittakaavaohjelmassa voisimme kirjoittaa käyttöohjeet omaksi aliohjelmakseen:
import java.io.*;
/**
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
class Matka_a1 {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
/**
* Tulostaa ohjelman käyttöohjeet
*/
private static void ohjeet() {
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
}
public static void main(String[] args) {
int matka_mm;
double matka_km;
ohjeet();
matka_mm = Syotto.kysy_int("Anna matka millimetreinä",0);
// Datan käsittely
matka_km = matka_mm*MITTAKAAVA/MM_KM;
// Tulostus
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
}
Tämän etu on siinä, että saimme pääohjelman selkeämmän näköiseksi.
Voisimme jatkaa pääohjelman selkeyttämistä. Tavoite voisi olla aluksi vaikkapa kirjoittaa pääohjelma muotoon:
ohjeet();
matka_mm = Syotto.kysy_int("Anna matka millimetreinä",0);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
Tällainen pääohjelma tuskin tarvitsisi paljoakaan kommentteja.
Edellä on käytetty kolmen eri tyypin aliohjelmia (funktioita)
1. ohjeet();– parametriton aliohjelma
2. mittakaava_muunnos(matka_mm); – funktio, joka palauttaa tuloksen nimessään
3. tulosta_matka(matka_km); – aliohjelma, jolle vain viedään arvo, mutta mitään arvoa ei palauteta
Valmis ohjelma, jossa myös aliohjelmat on esitelty, näyttäisi seuraavalta (rivien numerointi on myöhemmin esitettävää pöytätestiä varten):
java-muut\Matka_a3.java - erilaisia funktioita
/**
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class Matka_a3 {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
/**
* Tulostaa ohjelman käyttöohjeet
*/
private static void ohjeet() {
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
}
/**
* Muuttaa mm mittakaavan mukaisesti kilometreiksi
* @param matka_mm muutettavat millit
* @return mittakavan mukaiset kilometrit
*/
private static double mittakaava_muunnos(int matka_mm)
{
return matka_mm*MITTAKAAVA/MM_KM;
}
/**
* Tulostaa matkan kilometreinä
* @param matka_km tulostettava kilometrimäärä
*/
private static void tulosta_matka(double matka_km)
{
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
/**
* Varsinainen pääohjelma matka kysymiseksi ja laskemiseksi
* @param args ei käyttöä
*/
public static void main(String[] args) {
int matka_mm;
double matka_km;
ohjeet();
matka_mm = Syotto.kysy_int("Anna matka millimetreinä",0);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
}
}
Edellä olevasta huomataan, että aliohjelmat jotka eivät palauta mitään arvoa nimessään, esitellään void–tyyppisiksi.
mittakaava_muunnos on reaaliluvun palauttava funktio, joten se esitellään double –tyyppiseksi.
Seuraavaksi pöytätestaamme ohjelmamme toiminnan:
|
main |
mi..muunnos |
tulosta |
|
||
lause |
matka_mm |
matka_km |
matka_mm |
tulos |
matka_km |
tulostus |
46 ohjeet() |
?? |
?? |
|
|
|
|
13-17 System |
|
|
|
|
|
Lasken 1:200000 |
47 matka_mm= |
352 |
|
|
|
|
Anna matka ... |
48 matka_km |
|
|
352 |
|
|
|
26 return |
|
|
|
70.4 |
|
|
48 matka_km |
|
70.4 |
|
|
|
|
49 tulosta |
|
|
|
|
70.4 |
|
33-36 System |
|
|
|
|
|
Matka on luo.. |
50 } |
|
|
|
|
|
|
Emme enää käyneet läpi sitä, mitä Syotto.kysy_int tekee, koska se oli testattu erikseen ja sen jälkeen aliohjelma voidaan käsittää "valmiina kieleen kuuluvana käskynä".
Mikäli kukin "omatekoinen" aliohjelmakin olisi testattu erikseen, riittäisi meille pelkkä pääohjelman testi:
|
main |
|
|
lause |
matka_mm |
matka_km |
tulostus |
46 ohjeet() |
?? |
?? |
Lasken 1:200000 |
47 matka_mm= |
352 |
|
Anna matka .. |
48 matka_km |
|
70.4 |
|
49 tulosta |
|
|
Matka on luo.. |
50 } |
|
|
|
Tämä on testaustapa, johon tulisi pyrkiä. Isossa ohjelmassa ei ole enää mitään järkeä testata sitä jokainen aliohjelma kerrallaan. Koodiin liitettyjen aliohjelmien tulee olla testattuja kukin erillisinä ja lopullinen testi on vain viimeisimmän mallin mukainen!
Huomattakoon, ettei parametrien nimillä aliohjelmien esittelyissä ja kutsuissa ole mitään tekemistä keskenään. Nimi voi olla joko sama tai eri nimi. Parametrien idea on nimenomaan siinä, että samaa aliohjelmaa voidaan kutsua eri muuttujien tai mahdollisesti vakioiden tai lausekkeiden arvoilla. Esimerkiksi nyt kirjoitettua tulosta_matka aliohjelmaa voitaisiin kutsua myös seuraavasti:
java-muut\Matka_a4.java - erilaisia tapoja kutsua funktiota
/**
* Esimerkkejä kutsua aliohjelmaa eri tavoin
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class Matka_a4 {
/**
* Tulostaa matkan kilometreinä
* @param matka_km tulostettava kilometrimäärä
*/
private static void tulosta_matka(double matka_km)
{
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
/**
* Varsinainen pääohjelma matka kysymiseksi ja laskemiseksi
* @param args ei käyttöä
*/
public static void main(String[] args) {
double d = 50.2;
tulosta_matka(d); // eri niminen muuttuja
tulosta_matka(30.7); // vakio
tulosta_matka(d+20.8); // lauseke
tulosta_matka(2*d-30.0); // lauseke
}
}
Edellä aliohjelman kutsut voidaan tulkita seuraaviksi sijoituksiksi aliohjelman tulosta_matka lokaaliin parametrimuuttujaan matka_km:
matka_km = d;
matka_km = 30.7;
matka_km = d+20.8;
matka_km = 2*d–30.0
Aliohjelma jouduttiin edellä vielä kirjoittamaan uudestaan (käytännössä kopioimaan edellisestä ohjelmasta), mutta myöhemmin opimme miten aliohjelmia voidaan kirjastoida standardikirjastojen tapaan (ks. moduuleihin jako), jolloin kerran kirjoitettua aliohjelmaa ei enää koskaan tarvitse kirjoittaa uudestaan (eikä kopioida).
Funktion arvo palautetaan return –lauseessa. Jokaisessa ei-void –tyyppiseksi esitellyssä funktiossa tulee olla vähintään yksi return –lause. void–tyyppisessäkin voi olla return–lause. Tarvittaessa return–lauseita voi olla useampiakin:
public static int suurempi(int a, int b)
{
if ( a >= b ) return a;
return b;
}
Kun return –lause tulee vastaan, lopetetaan HETI funktion suoritus. Tällöin myöhemmin olevilla lauseilla ei ole mitään merkitystä. Näin ollen useat return–lauseet ovat mielekkäitä vain ehdollisissa rakenteissa. Siis seuraavassa ei olisi mitään mieltä:
public static int hopo(int a) L
{
int i;
return 5; /* Palauttaa aina 5!!! */
i = 3 + a;
return i+2;
}
return–lausetta ei saa sotkea siihen, että parametrina vietyjä olioita voidaan pyytää muuttamaan sisältöään funktion aikana:
java-muut\FunJaOlio.java - sivuvaikutuksellinen funktio
/**
* Esimerkki funktiosta joka muuttaa myös parametriään
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class FunJaOlio {
private static int pituus_ja_muuta(StringBuffer s)
{
int pit = s.length();
s.delete(0,pit).append("toka"); // pääohjelman jono muuttuu nyt
return pit;
}
public static void main(String[] args) {
int i; StringBuffer jono = new StringBuffer("eka");
i = pituus_ja_muuta(jono);
System.out.println("i=" + i + ", jono="+jono); // tulostaa: i=3, jono=toka
}
}
Edellä ei kutsusta näe millään tavalla että kutsun jälkeen jono on muuttunut. Yhtenä Java-kielen miinuksena voidaankin pitää sitä, että siitä puuttuu C++-kielessä oleva mekanismi suojata oliot muutoksilta aliohjelman suorituksen aikana (const).
Näin paljon jääkin ohjelmoijan vastuulle, eli ohjelmoijan pitää nimetä aliohjelmat siten, että niiden nimi jo paljastaa jos jotakin parametria muutetaan ohjelman suorituksen aikana. Ja sitten aliohjelmat on tehtävä huolellisesti, etteivät ne todellakaan muuta kutsuparametrejaan jollei se ole aliohjelmien tarkoitus.
Tehtävä 8.8 Funktio ja osoitin
Mitä pääohjelma FunJaOlio tulostaisi jos aliohjelma olisikin ollut:
private static int pituus_ja_muuta(StringBuffer s)
{
s.append("toka");
return s.length();
}
Tehtävä 8.9 String vs. StringBuffer
Kirjoita edellisestä tehtävästä versio jossa muutat kaikki StringBuffer => String ja korvaat append-metodin concat-metodilla. Mitä tulostuu?
Koska funktio–aliohjelma palauttaa valmiiksi arvon, voitaisiin Matka_a3.java:n pääohjelma kirjoittaa myös muodossa:
public static void main(String[] args) {
double matka_mm;
ohjeet();
matka_mm = Syotto.kysy_int("Anna matka millimetreinä",0);
tulosta_matka(mittakaava_muunnos(matka_mm));
}
Funktioita käytetään silloin, kun aliohjelman tehtävänä on palauttaa vain yksi täsmällinen arvo. Math–luokan funktioita ovat:
abs, acos, asin, atan, atan2, ceil, cos, exp, floor, IEEEremainder, log, max, min, pow, random, rint, round, sin, sqrt, tan, toDegrees, toRadians
Funktioita käytetään kuten matematiikassa on totuttu:
double alpha = 1.32, a = 4, b=3;
double c = Math.sqrt(a*a+b*b) + Math.asin((Math.sin(alpha)+0.2)/2.0);
kysy_matka ja kysy_mittakaava voitaisiin kirjoittaa myös funktioiksi, ja tällöin niitä voitaisiin kutsua esim. seuraavasti:
matka_km = kysy_matka()*kysy_mittakaava()/MM_KM;
Vaarana olisi kuitenkin se, ettei voida olla aivan varmoja kumpiko funktiosta kysy_matka vai kysy_mittakaava suoritettaisiin ensin ja tämä saattaisi aiheuttaa joissakin tilanteissa yllätyksiä.
Tämän vuoksi pyrimmekin kirjoittamaan funktioiksi vain sellaiset aliohjelmat, jotka palauttavat täsmälleen yhden arvon ja jotka eivät ota muuta informaatiota ympäristöstä kuin sen mitä niille parametrinä välitetään. Eli tavoitteena on se, että funktioiden kutsuminen lausekkeen osana olisi turvallista. Tämä ei valitettavasti ole aina Javassa mahdollista, koska Javan aliohjelmakutsuista puuttuu muissa kielissä oleva muuttujaparametrin välitys (Pascal: var, C: osoitin *, C++ referenssi &).
Muissa kielissä aliohjelmat kirjoitamme siten, että arvot palautetaan osoitteen avulla. Hyvin yleinen C–tapa on kuitenkin palauttaa tällaisenkin aliohjelman onnistumista kuvaava arvo funktion nimessä (vrt. esim. scanf C-kielessä).
Tehtävä 8.10 Math-luokka
Katso SDK:n dokumenteista kunkin Math–luokan funktion parametrien määrä ja tyyppi sekä se mitä kukin todella tekee.
Tehtävä 8.11 Funktiot
Kirjoita edellä mainitut kysy_matka ja kysy_mittakaava nimessään arvon palauttavina funktioina.
Tehtävä 8.12 Ympyrän ala ja pallon tilavuus
Kirjoita funktiot, jotka palauttavat r–säteisen ympyrän pinta–alan ja r–säteisen pallon tilavuuden.
Kirjoita pääohjelma, jossa pinta–ala ja tilavuus –funktiot testataan.
Tehtävä 8.13 Pääohjelma yhtenä funktiokutsuna
Jatka edellä mainittua ketjuttamista siten, että koko pääohjelma on vain yksi lauseke (ohjeet–kutsu saa olla oma rivinsä jos haluat). Tosin tämä on C-hakkerismia eikä mikään tavoite helposti luettavalta ohjelmalta. Itse asiassa hyvä kääntäjä tekee automaattisesti tämän kaltaista optimointia (mitä muka voitiin säästää?).
Kuten aiemmin todettiin, kannattaa aliohjelmien testaamista varten kirjoittaa hyvin lyhyt testi–pääohjelma.
Esimerkiksi kerhon jäsenrekisterin päämenun tulostamista varten voisimme kirjoittaa aliohjelman nimeltä paamenu. Tämä päämenu voitaisiin sitten testata vaikkapa seuraavalla testipääohjelmalla:
java-muut\Paamenu.java - päämenun totetutus ja testi
/**
* Testataan Kerho-ohjelman päämenun tulostamista
* @author Vesa Lappalainen
* @version 1.0, 13.01.2003
*/
public class Paamenu {
private static void tulosta(String s) {
System.out.println(s);
}
/**
* Tulostaa Kerho-ohjelman päämenun
* @param jasenia kerhon jäsenten lukumäärä
*/
public static void paamenu(int jasenia) {
tulosta("\n\n\n\n");
tulosta("Jäsenrekisteri");
tulosta("==============");
tulosta("");
tulosta("Kerhossa on " + jasenia + " jäsentä.");
tulosta("");
tulosta("Valitse:");
tulosta(" ? = avustus");
tulosta(" 0 = lopetus");
tulosta(" 1 = lisää uusi jäsen");
tulosta(" 2 = etsi jäsenen tiedot");
tulosta(" 3 = tulosteet");
tulosta(" 4 = tietojen korjailu");
tulosta(" 5 = päivitä jäsenmaksuja");
tulosta(" :");
}
public static void main(String[] args) {
paamenu(10);
}
}
Huomattakoon, että aliohjelma on saatu kopioiduksi suoraan aikaisemmasta ohjelman suunnitelmasta lisäämällä vain kunkin rivin alkuun tulosta("ja loppuun ");. Tällaiset toimenpiteet voidaan automatisoida tekstinkäsittelyn avulla.
Valmiin aliohjelman kutsuminen on helppoa: etsitään aliohjelman esittely ja kirjoitetaan kutsu, jossa on vastaavan tyyppiset parametrit vastaavissa paikoissa.
Esimerkiksi funktion Math.sin esittely saattaa olla muotoa:
sin
public static double sin(double a)
Returns the trigonometric sine of an angle. Special cases:
- If the argument is NaN or an infinity, then the result is NaN.
- If the argument is zero, then the result is a zero with the same
sign as the argument.
A result must be within 1 ulp of the correctly rounded result.
Results must be semi-monotonic.
Parameters:
a
- an angle, in radians.
Returns:
the sine of the argument.
Funktion tyyppi on double ja sille viedään double tyyppinen parametri. Funktio ei muuta mitään parametrilistassa esiteltyä parametriaan (mistä tietää?). Siis funktiota ei ole mitään mieltä kutsua muuten kuin sijoittamalla sen palauttama arvo johonkin muuttujaan tai käyttämällä funktiota osana jotakin lauseketta. x:ää vastaava parametri voi olla mikä tahansa double tyyppisen arvon palauttava lauseke (tietysti mielellään sellainen joka tarkoittaa kulmaa radiaaneissa):
double kulman_sini,a,b,x,y;
...
kulman_sini = Math.sin(x);
...
y = Math.sin(x/2) + Math.cos(a/3);
...
Funktiota voitaisiin tietysti kutsua myös muodossa:
double x = 3.1;
Math.sin(x); L
mutta kutsussa olisi yhtä vähän järkeä kuin kutsussa
double x=3.1;
x + 3.0; L
tai jopa
3.0; L
Mihin lausekkeiden arvot menisivät? Eivät minnekään! Tosin Javassa kääntäjäkään ei päästä lävitse kahta viimeksi mainittua vaihtoehtoa, eli pelkää vakioita tai muuttujia sisältävää lauseketta, jota ei sijoiteta mihinkään.
Usein aloittelijan näkee yrittävän kutsua muodoissa
y = double Math.sin(double a);
y = Math.sin(double a)
mutta näissäkään ei ole järkeä, koska parametrin tyypin esittely kuuluu vain aliohjelman otsikon puoleiseen päähän, ei kutsupäähän.
Yksi yleinen aloittelijan virhe on tehdä paljon aliohjelmia, jotka tulostavat. Pikemminkin pitää toimia päinvastoin, eli aliohjelmien on tehtävä oma työnsä ja annettava sitten tulokset muille tulostettavaksi. Näin samoja aliohjelmia voidaan käyttää myös järjestelmässä, jossa varsinaista konsolitulostusta ei voi tehdä. Tällaisia ovat mm. graafiset käyttöliittymät.
Jos halutaan että aliohjelma kuitenkin tulostaa, niin useimmiten sille kannattaa siinä tapauksessa viedä parametrina tietovirta johon tulostetaan. Palaamme tähän esimerkin kanssa seuraavissa luvuissa. Alla kuitenkin pikainen esimerkki:
import java.io.*;
/**
* Testataan tietovirran viemistä parametrina
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Tulostustesti {
private static void tulosta(OutputStream os,int h, int m) {
PrintStream out = new PrintStream(os);
out.println("" + h + ":" + m);
}
public static void main(String[] args) throws FileNotFoundException,
IOException {
int h=12, m=15;
// Tulostaminen näyttöön
tulosta(System.out,h,m);
// Tulostaminen tiedostoon
FileOutputStream f = new FileOutputStream("Tulostustesti.txt");
try {
tulosta(f,h,m);
} finally {
f.close();
}
// Tulostaminen tavutietovirtaan, joka voidaan muuttaa sitten merkkijonoksi
ByteArrayOutputStream bs = new ByteArrayOutputStream();
tulosta(bs,h,m);
String s = bs.toString();
System.out.println(s); // Lisätty, jotta nähdään tulos.
}
}
Suuressa osassa edellisissä esimerkeissämme meillä on ollut vain 0 tai yksi parametria välitettävänä aliohjelmaan. Käytännössä usein tarvitsemme useampia parametrejä. Esimerkiksi edellisessä paamenu–aliohjelmassa pitäisi oikeastaan tulostaa myös kerhon nimi.
Ottakaamme esimerkiksi mittakaava_muunnos –funktio. Mikäli ohjelma haluttaisiin muuttaa siten, että myös mittakaavaa olisi mahdollista muuttaa, pitäisi myös mittakaava voida välittää muunnos–aliohjelmalle parametrinä. Kutsussa tämä voisi näyttää esim. tältä:
matka_km = mittakaava_muunnos(32,10000.0);
Vastaavasti funktio–esittelyssä täytyisi olla kaksi parametria:
private static double mittakaava_muunnos(int matka_mm, double mittakaava)
{
return matka_mm*mittakaava/MM_KM;
}
Kun kutsu suoritetaan, välitetään aliohjelmalle parametrit siinä järjestyksessä, missä ne on esitelty. Voitaisiin siis kuvitella aliohjelmakutsun aiheuttavan sijoitukset aliohjelman parametrimuuttujiin (tosin sijoitusjärjestystä ei taata, eli ei tiedetä kumpi sijoitus suoritetaan ensin):
mittakaava = 10000.0;
matka_mm = 32;
Jos kutsu on muotoa
matka_km = mittakaava_muunnos(matka_mm, MITTAKAAVA);
kuvitellaan sijoitukset:
matka_mm = matka_mm; // Pääohjelman muuttuja matka_mm sijoitetaan aliohjelman
// vastinpaikassa olevaan muuttujaan
mittakaava = MITTAKAAVA; // Ohjelman vakio toiseeen aliohjelman muuttujaan
Siis vaikka kutsussa ja esittelyssä esiintyykin sama nimi, ei nimien samuudella ole muuta tekemistä kuin mahdollisesti se, että turha on väkisinkään keksiä lyhennettyjä huonoja nimiä, jos kerran on hyvä nimi keksitty kuvaamaan jotakin asiaa.
Parametreista osa, ei yhtään tai kaikki voivat olla myös oliota.
Huom! Vaikka kaikilla aliohjelman parametreille olisikin sama tyyppi, täytyy jokaisen parametrin tyyppi mainita silti erikseen:
public static double nelion_ala(double korkeus, double leveys)
Tehtävä 8.14 Päämenuun kerhon nimi
Lisää Paamenu.java:n aliohjelmaan paamenu parametriksi myös kerhon nimi.
Tehtävä 8.15 Toisen asteen yhtälön juuri
Kirjoita funktio root_1(a,b,c), joka palauttaa jomman kumman toisen asteen yhtälön ax2+bx+c=0 juurista (oletetaan tällä kertaa, että a<>0 ja D = b2–4ac >= 0. Miksi oletetaan?).
Tehtävä 8.16 Toisen asteen polynomi, root_1
Kirjoita funktio root_1 joka palauttaa toisen asteen polynomin P(x) = ax2+bx+c arvon (muista viedä parametrinä myös a,b ja c).
Tehtävä 8.17 root_1 testaus
Kirjoita pääohjelma, jolla voidaan testata root_1 – aliohjelma (jotenkin myös se, että tulos toteuttaa yhtälön).
Aloitteleva ohjelmoija sotkee yleensä aliohjelmakutsua tehdessään kutsuvan ja kutsuttavan parametrien nimiä keskenään. Parametrien nimillä ei ole Java-kielessä mitään merkitystä. Aliohjelmakutsussa ratkaisee vain parametrien paikka. Kunkin kutsussa oleva arvo "sijoitetaan" vastinparametrilleen kun aliohjelmaan mennään. Seuraava esimerkki havainnollistaa tätä:
/**
* Esimerkki miten parametrin paikka ratkaisee, ei nimi
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Parampaikka {
private static void ali(int a, int b, int c) {
System.out.println("a=" + a + " b=" + b + " c=" + c);
}
public static void main(String[] args) {
int a=1,b=2,c=3;
ali(a,b,c); // Tulostaa: a=1 b=2 c=3
ali(b,a,c); // Tulostaa: a=2 b=1 c=3
ali(c,a,b); // Tulostaa: a=3 b=1 c=2
ali(10,c,c); // Tulostaa: a=10 b=3 c=3
}
}
On olemassa myös kieliä, joissa parametrit ovat nimettyjä. Tällainen on tarpeen jos parametreja on niin paljon, ettei niitä kaikkia välitetä joka kutsussa. Esimerkki tällaisesta kielestä on vaikkapa Microsoft Visual Basic for Application (VBA).
Javassa - samoin kuin monessa muussakin nykykielessä - on mahdollista kuormittaa (overload) aliohjelman nimeä. Eli samassa näkyvyysalueessa saa esiintyä samannimisiä aliohjelmia kunhan niiden parametrit eroavat toisistaan määrältään ja/tai tyypiltään.
private static double mittakaava_muunnos(int matka_mm, double mittakaava)
{
return matka_mm*mittakaava/MM_KM;
}
private static double mittakaava_muunnos(int matka_mm)
{
return matka_mm*MITTAKAAVA/MM_KM;
}
...
matka_km = mittakaava_muunnos(20);
...
matka_km = mittakaava_muunnos(32,20000.0);
Kääntäjä pystyy kutsussa päättelemään oikean aliohjelman parametrien määrän ja tyypin mukaan.
Tehtävä 8.18 Toisiaan kutsuvat aliohjelmat
Kirjoita yhden parametrin mittakaava_muunnos siten, että se kutsuu kahden parametrin mittakaava_muunnosta.
Kukin aliohjelma muodostaa oman kokonaisuutensa. Edellä olleissa esimerkeissä aliohjelmat eivät tiedä ulkomaailmasta mitään muuta, kuin sen, mitä niille tuodaan parametreinä kutsun yhteydessä.
Vastaavasti ulkomaailma ei tiedä mitään aliohjelman omista muuttujista. Näitä aliohjelman lokaaleja muuttujia on esim. seuraavassa:
private static int pituus_ja_muuta(StringBuffer s)
{
int pit = s.length();
s.delete(0,pit).append("toka"); // pääohjelman jono muuttuu nyt
return pit;
}
s – aliohjelman parametrimuuttuja (tässä tapauksessa viite merkkijonoon).
pit – aliohjelman lokaali apumuuttuja pituuden säilyttämiseksi
Yleensäkin Java–kielessä lausesulut { ja } muodostavat lohkon, jonka ulkopuolelle mikään lohkon sisällä määritelty muuttuja tai tyyppimääritys ei näy. Näkyvyysalueesta käytetään englanninkielisessä kirjallisuudessa nimitystä scope. Lokaaleilla muuttujilla voi olla vastaava nimi, joka on jo aiemmin esiintynyt jossakin toisessa yhteydessä. Lohkon sisällä käytetään sitä määrittelyä, joka esiintyy lohkossa:
/** L
* Testataan Javan muuttujien lokaalisuutta
* @author Vesa Lappalainen
* @version 1.0, 13.01.2003
*/
public class Lokaali {
static private char ch='A';
static private void ali() {
double ch = 4.5;
System.out.println("Reaaliluku " + ch);
}
public static void main(String[] args) {
System.out.println("Kirjain " + ch);
{
int ch = 5;
System.out.println("Kokonaisluku " + ch);
ali();
}
System.out.println("Kirjain " + ch);
}
}
Ohjelma tulostaa:
Kirjain A
Kokonaisluku 5
Reaaliluku 4.5
Kirjain A
Saman tunnuksen käyttäminen eri tarkoituksissa on kuitenkin kaikkea muuta kuin hyvää ohjelmointia.
Tehtävä 8.19 Eri nimet
Korjaa edellinen ohjelma siten, että kullakin erityyppisellä muuttujalla on eri nimi.
Ainoa Java–kielen tuntema parametrinvälitysmekanismi on parametrien välittäminen arvoina. Tämä tarkoittaa sitä, että aliohjelma saa käyttöönsä vain (luku)arvoja, ei muuta. Olkoon meillä esimerkiksi ongelmana tehdä aliohjelma, jolle viedään parametreinä tunnit ja minuutit sekä niihin lisättävä minuuttimäärä. Jos ensimmäinen yritys olisi seuraava:
/** L
* Yritetään lisätä metodissa parametrien arvoja
* @author Vesa Lappalainen
* @version 1.0, 18.01.2003
*/
public class Aikalisa {
private static void lisaa(int h, int m, int lisa_min) {
int yht_min = h*60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
private static void tulosta(int h, int m) {
System.out.println("" + h + ":" + m);
}
public static void main(String[] args) {
int h=12,m=15;
tulosta(h,m);
lisaa(h,m,55);
tulosta(h,m);
}
}
Tämä ei tietenkään toimisi! Hyvä (C-) - kääntäjä jopa varoittaisi että:
Warn : aikalisa.cpp(8,2):'m' is assigned a value that is never used
Warn : aikalisa.cpp(7,2):'h' is assigned a value that is never used
Mutta miksi ohjelma ei toimisi? Seuraavan selityksen voi ehkä ohittaa ensimmäisellä lukukerralla. Tutkitaanpa tarkemmin mitä aliohjelmakutsussa oikein tapahtuu. Oikaisemme seuraavassa hieman joissakin kohdissa liian tekniikan kiertämiseksi, mutta emme kovin paljoa. Esimerkki on kirjoitettu vastaavasta C++-ohjelmasta. Javassa periaatteessa tapahtuu samalla tavalla. Katsotaanpa ensin miten kääntäjä kääntäisi aliohjelmakutsun (Borland C++ 5.1, 32-bittinen käännös, rekisterimuuttujat kielletty jottei optimointi tekisi konekielisestä ohjelmasta liian monimutkaista):
lisaa(h,m,55);
muistiosoite assembler selitys
-------------------------------------------------------------------------
004010F9 push 0x37 pinoon 55
004010FB push [ebp-0x08] pinoon m:n arvo
004010FE push [ebp-0x04] pinoon h:n arvo
00401101 call lisaa mennään aliohjelmaan lisää
00401106 add esp,0x0c poistetaan pinosta 12 tavua (3 x int)
Kun saavutaan aliohjelmaan lisaa, on pino siis seuraavan näköinen:
muistiosoite sisältö selitys
------------------------------------------------------------------------
064FDEC 00401106 <-ESP paluuosoite kun aliohjelma on suoritettu
064FDF0 0000000C h:n arvo, eli 12
064FDF4 0000000F m:n arvo, eli 15
064FDF8 00000037 lisa_min, eli 55
Eli aliohjelmaan saavuttaessa aliohjelmalla on käytössään vain arvot 12,15 ja 55. Näitä se käyttää tässä järjestyksessä omien parametriensa arvoina, eli m,h,lisa_min.
Esimerkiksi Pascal ja C/C++ -kielissä olisi tarjota tähän sellainen ratkaisu, että aliohjelman parametrit olisivatkin viitteitä (tai osoittimia) kutsuvan ohjelman muuttujiin ja niihin tehty muutos muuttaisi suoraan kutsuvan ohjelman muuttujia. Javassa tämä on mahdollista vain olioille, koska oliot välitettiin viitteinä.
C++: void lisaa(int &h, int &m, int lisamin); kutsu: lisaa(h,m,55);
Pascal: procedure lisaa(var h,m:integer; lisamin:integer); kutsu: lisaa(h,m,55);
C: void lisaa(int *h, int *m, int lisamin); kutsu: lisaa(&h,&m,55)
Tehtävä 8.20 Muotoilu?
Kokeilepa laittaa ajaksi esim. 12:05. Mitä tulostuu? Miten vian voisi korjata?
Tehtävä 8.21 Tiedon lukeminen
Kirjoita aliohjelma lue_kello, joka kysyy ja lukee arvon kellonajalle, syöttö muodossa 12:15.
Uuden aliohjelmien kirjoittaminen kannattaa aina aloittaa aliohjelmakutsun kirjoittamisesta vähintään testiohjelmaan. Näin voidaan suunnitella mitä parametrejä ja missä järjestyksessä aliohjelmalle viedään. Näinhän teimme mittakaava–ohjelmassakin.
Muuttujat voidaan esitellä myös luokan kaikissa metodeissa näkyväksi. Mikäli muuttujat esitellään kaikkien ohjelman aliohjelmalausesulkujen ulkopuolella, näkyvät muuttujat koko luokan alueella. Jos muuttujat vielä varustetaan vaikkapa public määreellä, niin luokan ulkopuolisetkin luokat voivat niitä käyttää. Tällaista on syytä välttää. Seuraava ohjelma on kaikkea muuta kuin hyvän ohjelmointitavan mukainen, mutta pöytätestaamme sen siitä huolimatta:
java-muut\Alisotku.java - parametrin välitystä
/**
* Mitä ohjelma tulostaa??
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Alisotku {
/**
* Palauttaa merkkijonon kokonaislukuna
* @param s muutettava merkkijono
* @return merkkijonosta saatu kokonaisluku
*/
private static int i(StringBuffer s) {
return Integer.parseInt(s.toString());
}
/**
* Sijoittaa kokonaisluvun arvon merkkijonoon
* @param s merkkijono johon tulos sijoitetaan
* @param i kokonaisluku joka sijoitetaan
*/
private static void set(StringBuffer s,int i) {
s.delete(0, s.length()).append(""+i);
}
/* 01 */ static int a; static StringBuffer b; static int c;
/* 02 */
/* 03 */ private static void ali_1(StringBuffer a, int b)
/* 04 */ {
/* 05 */ int d;
/* 06 */ d = i(a);
/* 07 */ c = b + 3;
/* 08 */ b = d - 1;
/* 09 */ a.append(""+(c - 5));
/* 10 */ }
/* 11 */
/* 11 */ static private void ali_2(StringBuffer a, StringBuffer b)
/* 13 */ {
/* 14 */ int c;
/* 15 */ c = i(a) + i(b);
/* 16 */ set(a,9 - c);
/* 17 */ set(b,32);
/* 18 */ }
/* 19 */
/* 20 */ public static void main(String[] args) {
/* 21 */ StringBuffer d = new StringBuffer(); b = new StringBuffer();
/* 22 */ a=1; set(b,2); c=3; set(d,4);
/* 23 */ ali_1(d,c);
/* 24 */ ali_2(b,d);
/* 25 */ ali_1(d,3+i(d));
/* 26 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 27 */ }
}
Käsittelemme (huonosti nimettyjä) metodeja i ja set "operaattoreina", eli oletamme niiden toiminnan tunnetuksi, eikä pöytätestissä askelleta niihin sisälle.
Pöytätestin tekeminen aloitetaan piirtämällä sarakkeet kutakin isompaa ohjelmassa olevaa kokonaisuutta varten. Esimerkissä näitä ovat
· suoritettava lause
· luokkamuuttujat
· main-metodi
· metodit ali_1 ja ali_2
· keko
· lisäksi kannattaa laskea välitulokset jonnekin auki
Sitten kukin sarake jaetaan vielä osiin siinä olevien muuttujien määrän mukaan. Kekoa varten tarvitaan karkeasti yhtä monta saraketta kuin ohjelmassa on suoritettavia new-operaattoreita (tai String a = "kissa"; tyyppisiä lauseita) .
Lyhyyden vuoksi olemme seuraavassa merkinneet N1 = ensimmäinen new:llä luotu olio ja N2 on toinen. Lisäksi on otettu c-mäinen merkintä &N1, eli viite olioon N1. Merkintä L.c tarkoittaa seuraavassa luokan c -muuttuja (jos on vaara sekaantua muuhun). Merkintää := on käytetty välilaskutoimituksissa erottamaan sijoitusta = -merkistä. Merkintä * muuttujien yläpuolella on muistutuksena sitä, että kyseessä on viitemuuttujat ja niiden käsittely muuttaa aina jotakin muuta muistipaikkaa. Pöytätestissä siis sarakkeet ovat muistipaikkoja ja rivit muistipaikkojen arvo tiettynä ajanhetkenä. Muistipaikka on merkitty harmaalla jos se ei ole voimassa tiettynä ajanhetkenä.
|
luokan |
main |
ali_1 |
ali_2 |
keko |
apulaskut |
|||||||
|
|
* |
|
|
* |
|
|
* |
* |
|
SB |
SB |
|
lause |
a |
b |
c |
d |
a |
b |
d |
a |
b |
c |
N1 |
N2 |
|
01 int a; |
0 |
null |
0 |
|
|
|
|
|
|
|
|
|
|
21 d = new |
|
&N2 |
|
&N1 |
|
|
|
|
|
|
"" |
"" |
syntyy tyhjät merkkijonot |
22 a=1; b=2 |
1 |
o-> |
3 |
o-> |
|
|
|
|
|
|
"4" |
"2" |
|
23 ali_1(d,c |
|
|
|
|
&N1 |
3 |
|
|
|
|
|
|
ali_1(&N1,c) |
05 int d |
|
|
|
|
|
|
? |
|
|
|
|
|
|
06 d = i(a); |
|
|
|
|
|
|
4 |
|
|
|
|
|
d:=i(N1)=4 |
07 c = b+3; |
|
|
6 |
|
|
|
|
|
|
|
|
|
L.c:= 3+3 = 6 |
08 b = d-1; |
|
|
|
|
|
3 |
|
|
|
|
|
|
b:= 4-1 = 3 |
09 a.ap(c-5) |
|
|
|
|
o-> |
|
|
|
|
|
"41" |
|
L.c-5=1; N1:="4"+"1"="41" |
24 ali_2(b,d |
|
|
|
|
|
|
|
&N2 |
&N1 |
|
|
|
ali_2(&N2,&N1) |
14 int c; |
|
|
|
|
|
|
|
|
|
? |
|
|
|
15 c=i(a)+i( |
|
|
|
|
|
|
|
|
|
43 |
|
|
c:=41+2 = 43 |
16 set(a,9-c |
|
|
|
|
|
|
|
o-> |
|
|
|
"-34" |
N2:=9-c=-34; |
17 set(b,32) |
|
|
|
|
|
|
|
|
o-> |
|
"32" |
|
|
25 ali_1(d,3 |
|
|
|
|
&N1 |
35 |
|
|
|
|
|
|
ali_1(&N1,3+32) |
06 d = i(a) |
|
|
|
|
|
|
32 |
|
|
|
|
|
d:=i(N1)=32 |
07 c = b+3; |
|
|
38 |
|
|
|
|
|
|
|
|
|
L.c:= 35+3 = 38 |
12 b = d-1; |
|
|
|
|
|
5 |
|
|
|
|
|
|
b:= 32-1 = 31 |
09 a.ap(c-5 |
|
|
|
|
o-> |
|
|
|
|
|
|
|
L.c-5=33; N1:="32"+"33"="3233" |
26 printl |
|
|
|
|
|
|
|
|
|
|
"3233" |
|
Tulostus: 1 -34 38 3233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
============= |
Luokkamuuttujat ovat rinnastettavissa globaaleihin muuttujiin. Samoin kun seuraavassa luvussa päästään käsiksi varsinaiseen olio-ohjelmointiin, niin myös julkiset attribuutit ovat rinnastettavissa globaaleihin muuttujiin. Globaaleiden muuttujien käyttöä tulee ohjelmoinnissa välttää. Tuskin mistään on tullut yhtä paljon ohjelmointivirheitä, kuin vahingossa muutetuista globaaleista muuttujista!
Käytännössä pöytätestiä voidaan monesti korvata hyvällä debuggerilla. Debuggerista valitettavasti ei useinkaan näe suorituksen historiaa. Ennen kun debuggerit eivät olleet niin yleisiä, korvattiin niitä sijoittamalla ohjelmakoodin sekaan muuttujien arvoja tulostavia lauseita. Joissakin tapauksissa tähänkin vielä joudutaan turvautumaan.
Tehtävä 8.22 Muuttujien näkyvyys
Pöytätestaa seuraava ohjelma:
java-muut\Alisotk2.java - parametrin välitystä
/**
* Mitä ohjelma tulostaa??
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Alisotk2 {
private static int i(StringBuffer s) {
return Integer.parseInt(s.toString());
}
private static void set(StringBuffer s,int i) {
s.delete(0, s.length()).append(""+i);
}
/* 01 */ private static StringBuffer b; private static int c;
/* 02 */
/* 03 */ private static void s_1(StringBuffer a, int b)
/* 04 */ {
/* 05 */ int d;
/* 06 */ d = i(a);
/* 07 */ c = b + 3;
/* 08 */ b = d - 1;
/* 09 */ set(a,c - 5);
/* 10 */ }
/* 11 */
/* 12 */ private static void a_2(int a, StringBuffer b)
/* 13 */ {
/* 14 */ c = a + i(b);
/* 15 */ { int c; c = i(b);
/* 16 */ a = 8 * c; }
/* 17 */ set(b,175);
/* 18 */ }
/* 19 */
/* 20 */ public static void main(String[] args) {
/* 21 */ StringBuffer a = new StringBuffer("4"); int d=9;
/* 22 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 23 */ b=new StringBuffer("3"); c=2; d=1;
/* 24 */ s_1(b,c);
/* 25 */ a_2(d,a);
/* 26 */ s_1(a,3+d);
/* 27 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 28 */ }
}