Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

3 Ohjelmointitekniikkaa: standardisyöttövirta ja Scanner

(Muutettu viimeksi 29.9.2011, sivu perustettu 17.9.2011. Arto Wikla)

Kurssilla on tähän saakka luettu syöttiedot ponnahdusikkunoita käyttäen. Tuloksia on ponnahdusikkunoiden liskäksi kirjoitettu standarditulosvirtaan System.out. Nyt on aika oppia lukemaan syöttötietoja myös standardisyöttövirrasta System.in. Oletusarvoisesti nämä "virrat" tarkoittavat kuvaruutua ja näppäimistöä, mutta ne voidaan ohjata tarkoittamaan myös tekstitiedostoja. Tästä lisää jatkokurssilla.

Syöttötietojen lukeminen

Javalla syöttötietoja voi lukea käyttämällä ns. Scanner-oliota. Scanner-luokalla on mm. sellainen konstruktori, jolle annetaan parametriksi standardisyöttövirta System.in. Luokka tarjoaa mm. seuraavat aksessorit:

Tuttu esimerkki ilman ponnahdusikkunoita:

import java.util.Scanner;   // Scanner-luokka tuodaan käännösyksikköön

public class Viisas {

  private static Scanner lukija = new Scanner(System.in); // luodaan Scanner-olio

  public static void main(String[] args) {

    String nimi;
    int    ika;
    double pituus;

    System.out.println("Mikä on nimesi?");
    nimi = lukija.nextLine();       // sovelletaan aksessoria nextLine();

    System.out.print("Ja ikäsi: ");
    ika = lukija.nextInt();         // sovelletaan aksessoria nextInt();

    System.out.print("Entä pituutesi? ");
    pituus = lukija.nextDouble();   // Syötteessä käytettävä desimaalipilkkua!!

    System.out.println("Moi " + nimi +"!");
    System.out.print("Tiedän että olet " + ika + "-vuotias ");
    System.out.println("ja että olet " + pituus + " senttiä pitkä.");
    System.out.println("Enkö olekin viisas!");
  }
}

Syöttö tapahtuu siis nyt Java-ohjelman komentotulkki-ikkunassa, siinä samassa, jonne System.out.println tulostaa.

Laitoksen koneissa desimaaliluku on siis syötettävä desimaalipilkullisena vaikka ohjelma itse käyttääkin desimaalipistettä! Syynä on "oletuslokalisaatio". Se voi omassa koneessasi olla eri...

Mikä on nimesi?
Violetta
Ja ikäsi: 20
Entä pituutesi? 167,8
Moi Violetta!
Tiedän että olet 20-vuotias ja että olet 167.8 senttiä pitkä.
Enko olekin viisas?

Jos ohjelmalle syöttää väärän tyyppistä tietoa, ohjelman suoritus päättyy virheeseen. Myös desimaalipisteen käyttö siis kaataa ohjelman.

---> Lisätietoa desimaalipisteen sallimisesta

Syöttöpuskuri ja loppumerkki

Javassa näppäimistöltä syötettävä teksti (samoin kuin tekstitiedostojen sisältö) muodostuu riveistä. Tietokoneen muistia ei tietenkään ole painetun tai kirjoitetun tekstin tavoin organisoitu riveittäin. Koneessa rivinvaihto muiden merkkien tapaan esitetään bittijonoksi koodattuna. Linuxissa/Unixissa rivin loppu ilmaistaan yhdellä merkillä, Windowsissa kahdella. Java-ohjelmoijan ei tarvitse tästä kantaa huolta – Java tietää, millaisessa ympäristössä sitä käytetään.

Kun tietoa kirjoitetaan näppäimistöltä, ohjelma saa kirjoitetun tekstin ns. syöttöpuskurissa, jonne viedään kaikki teksti seuraavaan rivinvaihtoon saakka (eli enter-näppäimen painallukseen saakka).

Jos kirjoitetaan vaikkapa kaksi välilyöntiä, merkit "2", "1", "3" ja vielä kolme välilyöntiä, syöttöpuskurissa on:

  213   @ (loppumerkki)
^ (seuraavaksi luettava merkki)

Merkitään näissä erimerkeissä loppumerkkiä näin: @. Syöttöpuskurissa on myös osoitin, joka osoittaa ensimmäistä lukemantonta merkkiä. Ilmaistaan se tässä merkillä ^.

Kun tässä tilanteessa luetaan

int i = nextInt();

Muuttuja i saa arvokseen kokonaisluvun 213 ja syöttöpuskurin tilanne on seuraava:

  213   @
     ^

Jos samassa lähtötilanteessa

  213   @
^

luetaan merkkijo tyyliin

String jono = nextLine();
puskuriin syntyy tilanne:

Eli puskuri on tyhjä ja muuttuja jono on arvoltaan "__123___" (välilyönnit on tässä merkattu alaviivalla).

Scanner-aksessori nextLine() siis lukee myös loppumerkin, vaikkei sisällytäkään sitä palauttamaansa merkkijonoon!

Lukuoperaatiot ja syöttöpuskuri

Kuten yllä opittiin, Scanner-luokka siis mm. lukuoperaatiot:

Näistä operaatioista kolme ensin lueteltua toimivat eri logiikalla kuin viimeksi mainittu:

Operaatio nextLine() sen sijaan ei white space -merkkejä tunnista. Se palauttaa arvonaan merkkijonona kaiken "seuraavaksi luettavasta merkistä" aina seuraavaan rivinvaihtoon saakka (ilman rivinvaihtomerkkiä, jonka se kuitenkin "syö"):

Esimerkki: Olkoon lähtötilanne:

213@
^

Operaation

int i = nextInt();

jälkeen puskurissa on

213@
   ^

Eli seuraavaksi tarjolla on loppumerkki. Jos nyt luetaan

String jono = nextLine();

muuttuja jono saa arvokseen tyhjän merkkijonon "" ja "ensimmäisen lukemattoman merkin" osoitin siirtyy seuraavan rivin alkuun, joka vuorovaikutteisen ohjelman tapauksessa syntyy vasta, kun käyttäjä päättää syöttää seuraavan rivin.

Tämä selittänee joidenkin varmasti havaitseman "synkronointiongelman" tietojen syötössä: Jos käytetään nextLine()-operaatiota noiden kolmen muun kanssa, loppumerkin käsittelyn kanssa on syytä olla tarkkana.

Kurkistus tulevaisuuteen

Virheellisen tyyppinen syöte johtaa ohjelman suorituksen keskeytymiseen: Jos esimerkiksi nextDouble()-operaatiolle syötetään desimaaliluvuksi kelpaamaton arvo, kaikki päättyy ikävällä tavalla:

Exception in thread "main" java.util.InputMismatchException
        at java.util.Scanner.throwFor(Scanner.java:819)
        at java.util.Scanner.next(Scanner.java:1431)
        at java.util.Scanner.nextDouble(Scanner.java:2335)
        at KolmeKarvo.main(KolmeKarvo.java:14)

Tällainen ei tietenkään ole oikeissa ohjelmissa hyväksyttävää! Niissä syöttötietojen oikeellisuus pitää siis tarkistaa.

Scanner-luokassa on joukko totuusarvoisia eli boolean-tyyppisiä aksessoreita, joilla voi kysyä: "Jos lukisin, saisinko sen ja sen tyyppisen arvon". Metodit siis tavallaan "kurkistavat tulevaisuuteen". Oikeasti ne vain käyvät kurkkaamassa syöttöpuskurin sisältöä:

Näillä välineillä voidaan tarkistaa esimerkiksi, onko seuraava syöttöalkio kokonaisluku:

import java.util.Scanner;
public class Tarkista1 {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    int luku;
    System.out.println("Anna kokonaisluku.");

    if (lukija.hasNextInt()) {
      luku = lukija.nextInt();
      System.out.println("Annoit luvun " + luku);
    } 
    else {
      System.out.println("Et syöttänyt kokonaislukua!");
    }
  }
}

Toki voidaan myös vaatia kunnon kokonaisluku:

import java.util.Scanner;
public class Tarkista2 {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    int luku;

    while (true) {  // ***** keskeytys breakilla!
      System.out.println("Anna kokonaisluku.");

      if (lukija.hasNextInt() ) {
        luku = lukija.nextInt();   // nyt uskaltaa lukea!
        break;      // ***** keskeytys breakilla!
      }

      String virheellinen = lukija.next();  // ohitetaan kelvoton alkio
      System.out.print  (virheellinen + " ei ole kokonaisluku! ");
      System.out.println("Yritä uudelleen!");
    }

    System.out.println("Annoit luvun " + luku);
  }
}

Koska tällä kurssilla harjoitellaan ohjelmoinnin perusvälineistöä, useinkaan ei ole syytä käyttää voimia ja ohjelmarivejä syöttötietojen tarkastamiseen, jottei opeteltava uusi asia hukkuisi muun ohjelmatekstin sisään. Jos ja kun harjoitus- ja koetehtävissä syötteiden tarkistamista vaaditaan, se sanotaan erikseen.

On kuitenkin syytä pitää mielessä se jo useampaan kertaan todettu sääntö, että "oikeissa" käyttäjille tarkoitetuissa ohjelmissa syöttötiedot tarkistetaan aina!

Huom: "Kurkistus tulevaisuuteen" on luontevinta, kun luetaan tekstitiedostoja. Interaktiivisessa tietojen syöttämisessä "tulevaisuus" on käyttäjän aivoituksissa. Tällöinkin "tiedoston loppumisen" voi ilmaista: Linuxissa ctrl-d, Windowsissa ctrl-z. (Jos edehdyt Linuxissa syöttämään ctrl-z, ohjelma joutuu ns. "backgroundiin" ja uloskirjoittautuminen voi estyä. Pelastus on Linux-komento fg, joka palauttaa ohjelman näkyville, "foreground"!) Tiedostojen käsittelyä opetellaan jatkokurssilla.

Scanner ja String

Scanner-luokka on monipuolinen väline. Sen lisäksi, että sillä voidaan kehittynein välinein lukea syöttövirtaa, aivan samoin aksessorein voidaan tutkia myös yksittäisen merkkijonon sisältöä!

Olio-ohjelmointiterminologialla ilmaisten: Scanner-luokassa on myös konstruktori, jolla voidaan luoda yksittäistä String-arvoa "skannaava" Scanner-olio. Samat tutut metodit ovat tällöinkin käytettävissä.

Tämä tarjoaa yhden mahdollisuuden virheitä sietävän tietojen lukemisen toteuttamiseen: Idea on, että luetaan syöttövirtaa rivi kerrallaan operaatiolla nextLine(). Yksi kerrallaan jokaisesta rivistä (String) luodaan Scanner-olio, jonka avulla rivin sisältöä sitten tutkitaan ja kaivetaan tiedot esiin.

Esimerkki:

import java.util.Scanner;
public class RiviLuku {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    System.out.println("Anna kokonaislukuja. Tyhjä rivi päättää.");
    String rivi = lukija.nextLine();

    while (rivi.length() > 0) {
      Scanner rivinSisalto = new Scanner(rivi); // joka rivistä oma Scanner!
      if (rivinSisalto.hasNextInt()) {
        int luku = rivinSisalto.nextInt();
        System.out.println("Luku on: " + luku);
      }
      else
        System.out.println("Virheellinen rivi: " + rivi);

      rivi = lukija.nextLine();
    }
  }
}