Javan syöte- ja tulostevirrat

Java-virtuaalikone alustaa automaattisesti ohjelmien käyttöön luokan System julkisina luokkamuuttujina tulostusoliot System.out ja System.err sekä syöteolion System.in. Näiden rooli on samanlainen kuin esim. C++:n virtaolioilla cout, cerr ja cin. System.out tulostaa standarditulostevirtaan ja System.err standardivirhetulostevirtaan (kumpikin oletuksena ruudulle), ja System.in lukee standardisyötevirtaa (oletuksena näppäimistöltä).

System.out ja System.err ovat tarkemmin ottaen luokan java.io.PrintStream olioita. Tämän luokan yleisimmin käytetyt tulostusfunkiot lienevät println ja print, jotka tulostavat parametrina annetun arvon tarvittaessa automaattisesti merkkijonoksi muunnettuna. Niiden ainoa ero on, että println tulostaa automaattisesti lopuksi rivinvaihdon ja sitä voi kutsua myös ilman parametria (pelkän rivinvaihdon tulostamiseksi). Kolmas yleisesti käytetty tulostusfunktio on format, joka vastaa C-kielen printf-funktiota ja Pythonin format-funktiota. Funktiolle annetaan parametriksi ns. muotoilumerkkijono, johon voi upottaa muuttujien arvoja. Muotoilumerkkijono tulostetaan muuten sellaisenaan, mutta se voi sisältää prosenttimerkillä % alkavia muotoilumääreitä, joiden kohtiin upotetaan muotoilumerkkijonon perässä erillisinä parametreina annetut arvot. Muotoilumääreet ilmaisevat upotettavan arvon tyypin sekä mahdollisesti muotoilua koskevia seikkoja, kuten leveyden tai tarkkuuden. Esimerkiksi %s tarkoittaa merkkijonoa, %d kokonaislukua ja %f liukulukua. Alla on pari yksinkertaista esimerkkiä. Formatter-luokan dokumentaatio kuvaa muotoilumääreiden käytön yksityiskohtaisemmin.

String name = "Lionel Messi";
int birthYear = 1987;
double height = 1.69;
System.out.format("Name: %s, Birth year: %d, Height: %.2f%n", name, birthYear, height);
System.out.format("Pi with three decimals: %10.3f", Math.PI);

Edellä esim. määre %.2f tarkoittaa kahden desimaalin tarkkuudella esitettyä liukulukua ja määre %10.3f kolmen desimaalin ja 10 merkin leveydellä (tyhjä osuus täytetään välilyönnein) esitettyä desimaalilukua. Lisäksi %n tarkoittaa järjestelmäkohtaista rivinvaihtoa, joka on esimerkiksi Linuxissa \n ja Windowsissa \r\n. Koodinpätkä tulostaisi:

Name: Lionel Messi, Birth year: 1987, Height: 1.69
Pi with three decimals:      3.142

PrintStream-luokan avulla on helppoa tulostaa tiedostoon: jos luokan rakentimelle antaa parametrina merkkijonon, on tuloksena PrintStream-olio, joka tulostaa parametrin nimeämään tiedostoon. Huom! Tällöin tiedoston mahdollinen vanha sisältö kirjoitetaan yli. Ellei tätä haluta, pitää ensin erikseen avata loppuunkirjoittamismoodissa java.io.FileOutputStream-tiedostovirta, joka sitten välitetään PrintStream:n rakentimelle merkkijonon sijaan.

System.in on luokan java.io.InputStream olio. InputStream itsessään on abstrakti syötevirtaluokka, joka tarjoaa vain rudimentaaliset funktiot tavumuotoisen syötteen lukuun. Yksi usein näppärä apuväline tekstimuotoisen syötteen lukuun on puskuroitu lukuluokka java.io.BufferedReader, joka tarjoaa esimerkiksi jäsenfunktion readLine kokonaisten syöterivien lukuun. Jotta syötevirtaa voisi lukea BufferedReader-oliolla, pitää meidän ensin luoda virtaa lukeva puskuroimaton lukuolio, ja antaa sitten se BufferedReader-luokan rakentimelle. Standardisyötteen lukemiseen soveltuva puskuroimaton lukuluokka on java.io.InputStreamReader, jonka rakentimelle annetaan luettava syöte.

Edellisen yhteenvetona voisimme luoda esimerkiksi standardisyötevirtaa lukevan BufferedReader-olion seuraavasti: new BufferedReader(new InputStreamReader(System.in)). Tällainen kerroksittainen rakenne InputStreamInputStreamReaderBufferedReader tuntuu ehkä varsinkin alkuun hieman monipolviselta, mutta vastaava periaate on Javassa (ja muuallakin) kohtalaisen yleinen.

BufferedReader sopii hyvin myös tiedoston lukemiseen. Ainoa ero edelliseen on, että nyt BufferedReader:n rakentimelle annetaankin tiedostoa lukeva puskuroimaton lukuolio. Sellaisen saa luotua helposti luokalla java.io.FileReader, jonka rakentimelle voi antaa avattavan tiedoston nimen merkkijonona.

Virtojen käsittely ⇒ tarvitaan yleensä poikkeusmääritys

Jos syöte- tai tulostevirran käsittelyn yhteydessä aiheutuu virhe, kuten esimerkiksi tiedoston avaaminen epäonnistuu, voi aiheutua ns. poikkeus. Poikkeuksia käsitellään tarkemmin myöhemmin. Jo tässä vaiheessa on kuitenkin paikallaan todeta, että Java monin paikoin vaatii meitä varautumaan tällaisiin poikkeuksiin: muuten kääntäjä antaa virheilmoituksen. Nimittäin ellemme itse määritä poikkeukseen reagoivaa koodia, leviää poikkeus funktiota kutsuneen tahon hoidettavaksi. Koska funktiota kutsuvan tahon olisi hyvä voida varautua funktiokutsun mahdollisesti aiheuttamaan poikkeukseen, täytyy Javassa funktiomäärityksen yhteydessä mainita, minkä tyyppisiä poikkeuksia funktion sisältä voi levitä ulos. Tällainen ns. poikkeusmääritys lisätään funktion otsakkeen loppuun ja on muotoa “throws poikkeusTyyppi1, ..., poikkeusTyyppiN”. Eli avainsana throws, jonka perässä luetellaan poikkeustyypit pilkulla eroteltuina. Virtoja koskevat perusoperaatiot aiheuttavat tyypillisimmin IOException-tyyppisiä poikkeuksia.

Edellinen ei koske Javan kaikkia mahdollisia poikkeuksia. Javan poikkeukset on jaettu tarkistettuihin (checked) ja tarkistamattomiin (unchecked) poikkeuksiin, ja vain tarkistettuihin poikkeuksiin on pakko jollain tapaa varautua.

Kehittyneemmät IDE:t (esim. NetBeans) osaavat neuvoa automaattisesti, minkä tyyppisiin poikkeuksiin koodissamme tarvitsee varautua. Poikkeusten tyypit voi myös selvittää koodissa käytettyjen luokkakirjastojen dokumentaatioista tai viimekädessä Java-kääntäjän antamista virheilmoituksista.

Avatut tiedostot pitäisi sulkea, kun niitä ei enää tarvita. Tämän voi tehdä erikseen kutsumalla lukuolion close-funktiota, mutta Javassa on myös erityinen ns. “try-with-resources”-rakenne, joka sulkee avatun tiedoston automaattisesti ja on siksi suositeltava käyttää.

Alla on esimerkkiohjelma, joka havainnollistaa syötteen lukua ja tulostusta. Ohjelma lukee käyttäjältä standardisyötteestä syötetiedoston nimen, rivin pituusrajan ja tulostiedoston nimen, ja kirjoittaa syötetiedostosta tulostiedostoon sellaisen version, jossa kukin rivi on korkeintaan annetun pituusrajan pituinen ja sanat on eroteltu toisistaan yksittäisillä välilyönneillä. Ohjelma päättyy, jos käyttäjä antaa komennon “quit”, muuten ohjelma kysyy aina uudet tiedostojen nimet ja pituusrajan.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class LineWrap {

  // main-funktion sisällä tehtävät virtaoperaatiot voivat aiheuttaa
  // IOException-tyyppisen poikkeuksen. Tämän vuoksi main-funktion
  // otsakkeen perään on lisätty poikkeusmääritys "throws IOException".
  public static void main(String args[]) throws IOException {
    // Luodaan BufferedReader-olio user syöteolion System.in kautta lukemiseen.
    // Huom! Toisin kuin alempana tiedostojen yhteydessä, tässä ei ole käytetty
    // try-with-resources-rakennetta, koska standardisyötevirtaa ei tarvitse sulkea.
    BufferedReader user = new BufferedReader(new InputStreamReader(System.in));
    while(true) {  // Ikuinen silmukka, josta poistutaan "quit"-komennolla.
      System.out.print("Give input filename (or 'quit'): ");
      final String inputFilename = user.readLine();
      if("quit".equalsIgnoreCase(inputFilename)) {
        break;
      }
      System.out.print("Give line wrap length limit (or 'quit'): ");
      String lineLimStr = user.readLine();
      if("quit".equalsIgnoreCase(lineLimStr)) {
        break;
      }
      final int lineLim = Integer.parseInt(lineLimStr);
      System.out.print("Give output filename (or 'quit'): ");
      final String outputFilename = user.readLine();
      if("quit".equalsIgnoreCase(outputFilename)) {
        break;
      }

      // Lasketaan näihin syöte- ja tulostiedoston rivien määrät.
      int inputLines = 0;
      int outputLines = 0;

      // Try-with-resources-rakenne on muotoa:
      //   try(resursseja varaavat muuttujamääritykset puolipisteellä eroteltuina) {
      //     resursseja käyttävä koodilohko
      //   }
      // Avainsanan try jälkeen suluissa varatut resurssit suljetaan automaattisesti,
      // kun resursseja käyttävästä koodilohkosta poistutaan. Resursseilla on oltava
      // jäsenfunktio close. Alla try-with-resources, jossa avataan sekä syötetiedostoa
      // lukeva BufferedReader input että tulostiedostoon kirjoittava PrintStream output.
      // Samalla esimerkki tilanteesta, jossa tyyppimääreen var käyttö voisi olla ok:
      // konkreettisen tyypin nimi on pitkähkö ja toistuisi kaksi kertaa lähes peräkkäin.
      try(var input = new BufferedReader(new FileReader(inputFilename));
          var output = new PrintStream(outputFilename)) {
        String line = null;
        int lineLen = 0;
        while((line = input.readLine()) != null) {
          inputLines += 1;
          // Split-funktio tulkitsee parametrinsa ns. säännölliseksi lauseeksi, jolloin
          // "\\s+" tarkoittaa yhtä tai useampaa välimerkkiä (välilyönti, sarkain, jne.).
          String[] words = line.split("\\s+");
          for(String word : words) {
            if(word.length() > 0) {
              if(lineLen > 0 && lineLen + 1 + word.length() > lineLim) {
                output.println();
                outputLines += 1;
                lineLen = 0;
              }
              if(lineLen > 0) {
                output.print(" ");
                lineLen += 1;
              }
              output.print(word);
              lineLen += word.length();
            }
          }
        }
        if(lineLen > 0) {
          output.println();
          outputLines += 1;
        }
      }
      // Tässä vaiheessa input ja output on jo suljettu automaattisesti.
      // Tulostetaan tietoa rivien lukumääristä esimerkin vuoksi standardivirhetulosteeseen.
      System.err.format("The input file %s had %d lines.%n", inputFilename, inputLines);
      System.err.format("The wrapped file %s has %d lines.%n", outputFilename, outputLines);
    }
  }
}

Haluat tulostaa seuraavat tiedot: String author = William Shakespeare; String title = Othello; integer published = 1603; Mikä seuraavista tulostaa ne muodossa: Author: William Shakespeare, Title: Othello, First performance: 1604

Sinulla on double[] temperatures, johon on talletettu lämpötilalukemia trooppisesta kasvihuoneesta. Mikä seuraavista tulostaa siitä alkion kolmen desimaalin tarkkuudella omalle rivilleen silmukan for(double d : temperatures) {} sisällä: