Näkyvyysalueet


Näkyvyydensäätelymääreillä public, private ja protected säädellään luokkien, kenttien, konstruktoreiden ja metodien näkyvyyttä (metodien ja konstruktoreiden paikalliset muuttujat eivät näy niiden ulkopuolelle):



Esimerkiksi luokalla, kentällä ja metodilla voi olla sama nimi. Kääntäjä käyttää hyväksi nimen käyttöyhteyttä nimen merkityksen ymmärtämiseen.



Nimien merkityksen etsintäjärjestys




Jos esimerkiksi konstruktorin parametrilla on sama nimi kuin luokan kentällä, pitää kenttään viitatessa käyttää ilmausta this:


class Opiskelija {

String nimi;


public Opiskelija(String nimi) {

this.nimi = nimi;

}


...

}

Poikkeukset


Ohjelman suorituksen aikana törmätään usein virhetilanteisiin esimerkiksi siksi, että käyttäjän antama syöte ei ollut odotetunlainen tai siksi, että ohjelmassa on virhe.


Esimerkki: ohjelma pyytää käyttäjältä kokonaislukua, mutta käyttäjä antaa merkkijonon 2r5


Java tarjoaa välineitä tällaisten virhetilanteiden, poikkeusten, käsittelyyn.


Poikkeukset on toteutettu luokkina. Kaikkien poikkeusten yliluokka on Execption. Kaikki mahdolliset poikkeukset ovat siis luokan Execption olioita.


Erilaisia poikkeuksia varten on omia aliluokkia, esimerkiksi ArithmeticException, ArrayIndexOutOfBoundsException, ClassNotFoundException, NumberFormatException, FileNotFoundException.


Ohjelmassa on mahdollisuus tutkia, minkä luokan poikkeus on aiheutunut ja sen mukaan mahdollisuus päätellä, mikä on aiheuttanut ongelman. Ohjelmoija voi määritellä myös omia poikkeuksiaan.


Poikkeukset jaetaan kahteen luokkaan:

Poikkeusten käsittely


Jos joku metodin suorituksessa aiheuttaa poikkeuksen, niin se voidaan käsitellä kahdella eri tavalla:

  1. Siepataan poikkeus ja käsitellään sen aiheuttama virhetilanne. Jos esimerkiksi ohjelma pyytää kokonaislukua, mutta käyttäjä antaa kelvottoman merkkijonon, pyydetään käyttäjältä parempi luku. Tämä voidaan tehdä try-catch-rakenteella.

  2. Heitetään poikkeus kutsuvalle metodille. Kutsuva metodi voi joko heittää poikkeuksen edelleen sitä kutsuvalle metodille tai käsitellä poikkeuksen aiheuttaneen virhetilanteen. Poikkeus voidaan heittää

    throws-ilmaisulla. Jos pääohjelma heittää poikkeuksen, koko ohjelma kaatuu.

Poikkeuksen heittäminen


Laaditaan ohjelma, joka laskee ja tulostaa komentoriviparametrina annetun liukuluvun neliöjuuren.


Komentoriviparametrit ovat aina merkkijonoja. Neliöjuuren laskemista varten merkkijono pitää muuttaa desimaaliluvuksi.


Luokasta Double löytyy konstruktori Double(), jolle voi antaa parametrina merkkijonon. Konstruktori muodostaa parametrimerkkijonoa vastaavan Double-olion. Olion sisältämän desimaaliluvun arvon saa selville metodilla doubleValue().


luku = new Double(args[0]).doubleValue();

juuri = Math.sqrt(luku);


Konstruktori Double voi kuitenkin aiheuttaa NumberFormatException-luokan poikkeuksen, jos parametrina annetusta merkkijonosta ei voi muodostaa liukulukua.


NumberFormatException on tarkistettava poikkeus. Konstruktoria kutsuvassa metodissa on joko käsiteltävä mahdollinen poikkeus tai heitettävä poikkeus eteenpäin.


Metodin otsikossa oleva ilmaus throws Exception kertoo, että tämä metodi voi heittää poikkeuksen.




public class Neliojuuri {
  public static void main(String[] args) 
           throws Exception {
    double luku, juuri;

    luku = new Double(args[0]).doubleValue();
    juuri = Math.sqrt(luku);
    System.out.println(juuri);
  }
}


Kun ohjelmaa ajetaan erilaisilla komentoriviparametreilla, saadaan seuraavia tulostuksia:


> java Neliojuuri 16.0

4.0

> java Neliojuuri 4.0r5

java.lang.NumberFormatException: 4.0r5

at java.lang.Double.<init>(Compiled

Code)

at Neliojuuri.main(Compiled Code)

> java Neliojuuri mita

java.lang.NumberFormatException: mita

at java.lang.Double.<init>(Compiled

Code)

at Neliojuuri.main(Compiled Code)

> java Neliojuuri

java.lang.ArrayIndexOutOfBoundsException: 0

at Neliojuuri.main(Compiled Code)

> java Neliojuuri 4.0e2

20.0


Tämä ei kuitenkaan ole hyvää ohjelmointityyliä. Sen sijaan on hyväksyttävää heittää poikkeus jostain metodista ja käsitellä se metodia kutsuvassa metodissa (esimerkkejä myöhemmin)

Poikkeuksen sieppaaminen


Toinen vaihtoehto poikkeuksen käsittelyyn on siepata poikkeus ja käsitellä jotenkin sen aihettanut virhetilanne.


Poikkeus siepataan try-catch-lauseella:


try {

... yritetään jotain, joka voi aiheuttaa

poikkeuksen Poikkeuksen_tyyppi.

Jos poikkeusta ei aiheuteta, try-osa

suoritetaan loppuun ja jätetään

catch-osa väliin

}

catch (Poikkeuksen_tyyppi poikkeuksen_nimi) {

... käsitellään siepattu poikkeus, jos

try-osa aiheutti poikkeuksen

Poikkeuksen_tyyppi.

}


Lauseen suoritus aloitetaan suorittamalla try-osassa olevia käskyjä normaalissa järjestyksessä.


Jos mikä tahansa try-osan käsky aiheuttaa poikkeuksen, jonka tyyppi on Poikkeuksen_tyyppi, siirrytään heti catch-osan suoritukseen.


Jos mikään try-osan suoritus ei aiheuta Poikkeuksen_tyyppi-tyyppistä poikkeusta, hypätään try-osan suorituksen jälkeen catch-osan yli.


catch-osia voi olla useita erityyppisille poikkeuksille. Tällöin suoritettava catch-osa valitaan tapahtuneen poikkeuksen tyypin mukaan.

Neliöjuuren laskeva ohjelma uudelleen:



public class Neliojuuri2 {
  public static void main(String[] args) {
    double luku, juuri;

    try {
      luku = new Double(args[0]).doubleValue();
      juuri = Math.sqrt(luku);
      System.out.println(juuri);
    } catch (NumberFormatException e) {
      System.out.println("Parametria ei voi tulkita luvuksi");
    }
  }
}


Nyt ohjelman ajaminen eri parametreilla tulostaa seuraavaa:


> java Neliojuuri2 16.0

4.0

> java Neliojuuri2 4.0r5

Parametria ei voi tulkita luvuksi

> java Neliojuuri2 mita

Parametria ei voi tulkita luvuksi

> java Neliojuuri2

java.lang.ArrayIndexOutOfBoundsException: 0

at Neliojuuri2.main(Compiled Code)

> java Neliojuuri2 4.0e2

20.0

Edellinen ohjelma kaatui, jos sille ei antanut komentoriviparametria. Muutetaan ohjelmaa siten, että se käsittelee myös komentoriviparametrin puuttumisesta aiheutuvan poikkeuksen:



public class Neliojuuri3 {
  public static void main(String[] args) {
    double luku, juuri;

    try {
      luku = new Double(args[0]).doubleValue();
      juuri = Math.sqrt(luku);
      System.out.println(juuri);
    } catch (NumberFormatException e1) {
      System.out.println("Parametria ei voi tulkita luvuksi");
    } catch (ArrayIndexOutOfBoundsException e2) {
      System.out.println("Parametri puuttuu");
    }
  }
}


Ohjelman tulostusta eri parametreilla:

> java Neliojuuri3 16.0

4.0

> java Neliojuuri3 4.0r5

Parametria ei voi tulkita luvuksi

> java Neliojuuri3 mita

Parametria ei voi tulkita luvuksi

> java Neliojuuri3

Parametri puuttuu

> java Neliojuuri3 4.0e2

20.0

Seuraava ohjelma sieppaa minkä tahansa poikkeuksen



public class Neliojuuri4 {
  public static void main(String[] args) {
    double luku, juuri;

    try {
      luku = new Double(args[0]).doubleValue();
      juuri = Math.sqrt(luku);
      System.out.println(juuri);
    } catch (NumberFormatException e1) {
      System.out.println("Parametria ei voi tulkita luvuksi");
    } catch (ArrayIndexOutOfBoundsException e2) {
      System.out.println("Parametri puuttuu");
    }
  }
}


Ohjelman tulostusta:


> java Neliojuuri4 16.0

4.0

> java Neliojuuri4 4.0r5

Lasku ei onnistu!

> java Neliojuuri4 mita

Lasku ei onnistu!

> java Neliojuuri4

Lasku ei onnistu!

> java Neliojuuri4 4.0e2

20.0



Seuraavassa ohjelmassa metodi heittää poikkeuksen, joka käsitellään pääohjelmassa (kutsuvassa metodissa):



public class Neliojuuri5 {

  private static double laskeJuuri(String lukumjono) 
         throws Exception { 
    double luku, juuri;

    luku = new Double(lukumjono).doubleValue();
    juuri = Math.sqrt(luku);
    return juuri;
  }
    

  public static void main(String[] args) {
    double juuri;

    try {
      if (args.length >= 1) {
	juuri = laskeJuuri(args[0]);
	System.out.println(juuri);
      }
      else
	System.out.println("Parametri puuttuu!");
    } catch (Exception e) {
      System.out.println("Lasku ei onnistu!");
    }
  }
}


Poikkeuksen aiheuttaminen


Ohjelmoija voi itse aiheuttaa poikkeuksen throw-käskyllä. Sillä aliohjelman suorituksen voi katkaista sellaisesta kohdasta, jossa virhe tapahtui.


Aiheutettava poikkeus luodaan Exception-luokan tai sen aliluokan konstruktorilla.


Luotavaan poikkeukseen voi liittää poikkeuksesta kertovan viestin, johon pääsee muualta käsiksi Exception-luokan getMessage-metodilla.


Esimerkki: aiheutetaan poikkeus, johon liittyy viesti "Liian pieni exp":


if (exp <= 0)

throw new Exception("Liian pieni exp");


Seuraava esimerkkiohjelma tutkii, riittääkö koepistemäärä kokeen läpäisyyn. Koe on läpäisty, jos on saatu vähintään puolet maksimipisteistä. Jos pisteitä on yli maksimipistemäärän tai alle nollan, aiheutetaan poikkeus.


public class PoikkeusPisteet {

  private static boolean lapaisy(int maxPisteet, int koePisteet) 
    throws Exception {
    boolean tulos = false;

    if (koePisteet > maxPisteet) 
      throw new Exception("Koepisteet yli maksimirajan");
    else if (koePisteet < 0)
      throw new Exception("Koepisteet alle nollan");
    else if (1.0 * koePisteet / maxPisteet > 0.5)
      tulos = true;
    return tulos;
  }


  public static void main(String[] args) {
    try {
      if (lapaisy(30, 14)) 
	System.out.println("Eka läpi!");
      else
	System.out.println("Eka hylsy!");
      if (lapaisy(30, 35)) 
	System.out.println("Tok läpi!");
      else
	System.out.println("Toka hylsy!");
    } catch (Exception e) {
      System.out.println("Poikkeus: " +
			 e.getMessage());
    }
  }
}

Syötteen lukeminen


Tähän asti käyttäjän antamaa syötettä on luettu Lue-luokan avulla. Nyt opitaan, miten syötettä luetaan Javan perusvälineillä.


Java 1.0 tarjosi mm. luokan InputStream syötteen lukemista varten. Luokan tarjoamilla metodeilla lukeminen oli kuitenkin hankalaa, koska syötettä pystyttiin lukemaan vain tavuittain.


Java 1.1 tarjoaa luokan BufferedReader syötteen lukemiseen. BufferedReader sisältää mm. metodin readLine(), jolla voidaan lukea yksi rivi syötteestä.


Jotta syötettä voitaisiin lukea, pitää ensin muodostaa BufferedReader-luokan olio. Olion luonnin yhteydessä kerrotaan, mistä syötettä luetaan - standardisyöttövirrasta (yleensä näppäimistö) vai jostain tiedostosta.


Luokassa System on määritelty standardisyöttövirta kenttänä in:


public static final InputStream in;


InputStream-oliota ei voi kuitenkaan antaa suoraan paramteriksi BufferedReader-luokan konstruktorille, vaan pitää ensin luoda sen avulla InputStreamReader-olio, joka annetaan parametriksi.


BufferedReader-luokan olio, jonka avulla voidaan lukea syötettä standarisyöttövirrasta, voidaan nyt luoda seuraavasti:


BufferedReader stdin;

stdin = new BufferedReader(new

InputStreamReader(System.in));


Tämän jälkeen syötettä voidaan lukea rivi kerrallaan String-olioon:


String mjono;

mjono = stdin.readLine();


Luettu rivi on nyt merkkijonomuodossa. Jos kuitenkin haluttiin lukea kokonaisluku, merkkijono on muutettava kokonaisluvuksi:


int i;

i = Integer.parseInt(mjono);


Jos merkkijono ei kelpaa kokonaisluvuksi, aiheutuu poikkeus NumberFormatException.


Jos haluttiin lukea desimaaliluku, on merkkijono muutettava liukuluvuksi (myös tämä voi aiheuttaa poikkeuksen):


double d;

d = new Double(mjono).doubleValue();


Lukeminen tiedostosta tehdään muuten samaan tapaan, mutta InputStreamReader-luokan konstruktorille annetaan erilainen parametri.

Seuraavassa esitetään joitain esimerkkiohjelmia arvojen lukemisesta. Huomattavia asioita:




import java.io.*;

public class PoikkeusKokeita {

  public static void main(String[] args) {

    BufferedReader stdin =
      new BufferedReader(new InputStreamReader(System.in));

    String jono = null;
    int kluku;
    double dluku;
    boolean ok;

    try {
      System.out.print("Anna kokonaisluku: ");
      System.out.flush();
      jono = stdin.readLine();
      kluku = Integer.parseInt(jono);
      System.out.println("Se oli: "+kluku);
    } catch (Exception e) {
      System.out.println("Virhe: " + e);
    }
    do {
      try {
	System.out.print("Anna desimaaliluku: ");
	System.out.flush();
	jono = stdin.readLine();
	dluku = new Double(jono).doubleValue();
	System.out.println("Se oli: "+dluku);
	ok = true;
      } catch (Exception e) {
	System.out.println("Virhe :" + e);
	ok = false;
      }
    } while (!ok && jono != null);
  }
}



import java.io.*;

public class PoikkeusKokeita2 {

  public static void main(String[] args) {

    BufferedReader stdin =
      new BufferedReader(new InputStreamReader(System.in));

    String jono = null;
    double dluku;
    boolean loppu = false;

    do {
      try {
	System.out.print("Anna desimaaliluku: ");
	System.out.flush();
	jono = stdin.readLine();
	dluku = new Double(jono).doubleValue();
	System.out.println("Se oli: "+dluku);
	loppu = true;
      } catch (NumberFormatException e) {
	System.out.println("Virhe: ei ole luku");
      } catch (NullPointerException e) {
	System.out.println("Virhe: tiedosto loppui");
	loppu = true;
      } catch (IOException e) {
	System.out.println("Virhe: ongelma tiedostossa");
	loppu = true;
      }
    } while (!loppu);
  }
}


import java.io.*;

public class PieniSuuri {
  public static void main(String[] args) {

    BufferedReader stdin =
      new BufferedReader(new InputStreamReader(System.in));
    int i;
    boolean loppu = false;
    String mjono;
    char[] taulu;

    System.out.println("Pienet suuriksi ja suuret pieniksi, "+
		       "lopeta ctrl-d:llä");
    while (!loppu) {
      try {
	System.out.print("Anna muutettava: ");
	System.out.flush();
	mjono = stdin.readLine();
	taulu = mjono.toCharArray();
	for (i=0; i


Tekstitiedostosta lukeminen


Tähän asti ohjelmat ovat lukeneet syötteensä aina standardisyöttövirrasta. Se on yleensä näppäimistö, mutta käyttöjärjestelmän puolella voidaan standarisyöttövirta yhdistää myös tiedostoon, esimerkiksi käynnistämällä ohjelma Prog käskyllä


> java Prog <teksti.txt


saadaan ohjelma lukemaan syötteensä näppäimistön sijasta tiedostosta teksti.txt.


Tällä tavalla ohjelma voi kuitenkin lukea vain yhdestä tiedostosta ajonsa aikana. Lisäksi sama ohjelma ei voi lukea tietoja sekä näppäimistöltä että tiedostosta.


BufferedReader-luokan avulla ohjelma voi lukea tietoja monesta tiedostosta ajonsa aikana. Lukeminen tapahtuu samaan tapaan kuin standarisyöttövirrasta lukeminen. Ero on BufferedReader-luokan konstruktorin parametrissa.


BufferedReader-olion avulla voi lukea syötettä tiedostosta "juttu.txt" esimerkiksi luomalla olion seuraavasti:


BufferedReader syotto =

new BufferedReader(

new InputStreamReader(

new FileInputStream("juttu.txt")));


Tämän jälkeen tiedostosta voi lukea rivin kerrallaan String-tyyppiseen muuttujaan rivi:

rivi = syotto.readLine();

Seuraava ohjelma lukee tiedoston "juttu.txt" rivi kerrallaan ja tulostaa rivit kuvaruudulle:



import java.io.*;

public class Listaa {
  public static void main(String[] args) {
    String rivi;

    try {
      BufferedReader syotto =
	new BufferedReader(new InputStreamReader(new 
					 FileInputStream("juttu.txt")));
      while((rivi = syotto.readLine()) != null)
	System.out.println(rivi);
    } catch (IOException e) {
      System.out.println("Virhe tiedoston lukemisessa");
    }
  }
}
			  


Ohjelmassa on kuitenkin joitakin puutteita:


Puutteita voi korjata ottamalla käyttöön luokan File.

File-luokan avulla voidaan luoda olio, jonka avulla voidaan tutkia tiedoston olemassaoloa ja tehdä muita tiedostoille tyypillisiä asioita.

Luokka File


Luokan File avulla voi hallinnoida tiedostoja. Luokan avulla voi esimerkkiksi selvittää, onko tiedosto olemassa, mikä sen pituus on sekä uudelleennimetä ja poistaa tiedostoja.


Luokalla on mm. konstruktori

public File(String path)


jolle annetaan parametrina käsiteltävän tiedoston nimi mahdollisine hakemistopolkuineen.


Tärkeimpiä File-luokan metodeita



Esimerkkiohjelma vaihtaa tiedoston juttu.txt nimeksi uusinimi.txt:



import java.io.*;

public class Tiedostot {
  public static void main(String[] args) {
    File vanha, uusi;
    
    vanha = new File("juttu.txt");
    uusi = new File("uusinimi.txt");
    if (!uusi.exists())
      vanha.renameTo(uusi);
    else
      System.out.println("Tiedosto uusinimi.txt on jo olemassa");
  }
}


Ohjelma on aika kankea, koska tiedostojen nimet ovat vakioita.



Seuraava ohjelma saa tiedostojen nimet komentoriviparametreina:



import java.io.*;

public class Tiedostot2 {
  public static void main(String[] args) {
    File vanha, uusi;
    
    if (args.length != 2)
      System.out.println("Väärä määrä parametreja!");
    else {
      vanha = new File(args[0]);
      uusi = new File(args[1]);
      if (!uusi.exists())
	vanha.renameTo(uusi);
      else
	System.out.println("Tiedosto " + args[1] + " on jo olemassa");
    }
  }
}

File-luokan käyttö tiedostojen lukemisessa


Esimerkkiohjelma tulostaa tiedoston kuvaruudulle. Tiedoston nimi annetaan komentoriviparametrina.



import java.io.*;

public class Listaa2 {
  public static void main(String[] args) {
    File syoTd;
    BufferedReader syotto;
    String rivi;

    if (args.length != 1)
      System.out.println("Väärä määrä parametreja");
    else {
      syoTd = new File(args[0]);
      if (!syoTd.exists()) 
	System.out.println("Tiedostoa " 
			   + args[0] + " ei löydy");
      else
	try {
	  syotto = new BufferedReader(
		      new InputStreamReader(
			 new FileInputStream(syoTd)));
	  while ((rivi = syotto.readLine()) != null)
	    System.out.println(rivi);
	} catch (IOException e) {
	  System.out.println("Virhe tiedoston lukemisessa");
	}
    }
  }
}

Seuraava ohjelma pyytää tulostettavan tiedoston nimen käyttäjältä. Nyt tarvitaan kaksi BufferedReader-oliota: toinen tiedoston lukemiseen ja toinen käyttäjän syötteen lukemiseen.



import java.io.*;

public class Listaa3 {
  public static void main(String[] args) {
    File syoTd;
    BufferedReader stdin, tdsto;
    String nimi, rivi;

    try {
      stdin = new BufferedReader(new InputStreamReader(System.in));
      System.out.println("Minkä tiedoston haluat nähdä?");
      nimi = stdin.readLine();
      syoTd = new File(nimi);
      if (!syoTd.exists()) 
	System.out.println("Tiedostoa " 
			   + nimi + " ei löydy");
      else {
	tdsto = new BufferedReader(
		   new InputStreamReader(
		      new FileInputStream(syoTd)));
	while ((rivi = tdsto.readLine()) != null)
	  System.out.println(rivi);
      }
    } catch (Exception e) {
      System.out.println("Virhe syotteen tai tiedoston lukemisessa");
    }
  }
}

Tekstitiedoston kirjoittaminen


Luokan PrintWriter olioiden avulla voidaan tulostaa tekstitiedostoon print- ja println-metodeilla samaan tyyliin kuin kuvaruudulle.


Luokan ilmentymiä (tulostiedostoja) voidaan luoda konstruktorilla


PrintWriter(OutputStream out,

boolean autoFlush)


Jälkimmäinen parametri ohjaa tulostuspuskurin tiedojen siirtämistä varsinaiseen tiedostoon. Kun parametri asetetaan trueksi, tiedostoa ei tarvitse itse sulkea ohjelman lopussa.


Ensimmäisen parametrin avulla tulostus ohjataan haluttuun tiedostoon. Parametriksi ei voi kuitenkaan antaa suoraan tiedoston nimeä tai File-oliota. Sen sijaan parametriksi voi antaa FileOutputStream-olion, jonka voi luoda jollakin konstruktoreista


FileOutputStream(String name)

FileOutputStream(String name, boolean append)

FileOutputStream(File file)


Kahden ensimmäisen parametrina on tiedoston nimi, kolmannen File-olio. Toisen konstruktorin avulla voi tulostaa olemassa olevan tiedoston loppuun antamalla parametrin append arvoksi true.

Tiedostoon voi tulostaa rivin esimerkiksi seuraavasti


PrintWriter tulos =

new PrintWriter(

new FileOutputStream("tulos.txt"),

true);

...

tulos.println("Hip hei!");

tulos.println("Toinen rivi");


Jos tiedosto tulos.txt oli jo olemassa, sen vanha sisältö häviää. Seuraava esimerkki tulostaa tiedoston juttu.txt loppuun (tiedoston vanha sisältö säilyy)


PrintWriter juttu =

new PrintWriter(

new FileOutputStream("tulos.txt", true),

true);

...

juttu.println("Rivi loppuun");

juttu.println("Toinen rivi loppuun");


Seuraava ohjelma kirjoittaa käyttäjän antamat rivit tiedostoon juttu.txt. Käyttäjä voi lopettaa rivien antamisen tiedoston loppu -merkillä (UNIXissa ctrl-d).



import java.io.*;

public class Talleta1 {
  public static void main(String[] args) {
    PrintWriter tdsto;
    BufferedReader stdin;
    String rivi;

    try {
      tdsto = new PrintWriter(new FileOutputStream("juttu.txt"),
			      true);
      stdin = new BufferedReader(new InputStreamReader(System.in));      
      System.out.println("Kirjoitetaan tiedostoon juttu.txt");
      while ((rivi = stdin.readLine()) != null)
	tdsto.println(rivi);
    } catch (Exception e) {
      System.out.println("Virhe lukemisessa tai kirjoittamisessa");
    }
    System.out.println("Kiitos!");
  }
}