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 yksinkertaiset funktiot tavumuotoisen syötteen lukuun. Tästä syystä syötteiden lukuun käytetään yleensä jotain hienostuneempaa menetelmää. Javassa on ollut jo pitkään saatavilla java.util.Scanner-luokka, jota on käytetty jo edellisellä kierroksella merkkijonojen lukuun. Kun Scanner-olio luodaan siten, että rakentajan parametriksi annetaan standardisyöteolio: new Scanner(System.in), saadaan aikaiseksi oletusarvoisesti näppäimistöltä syötteitä lukeva olio. Scanner-luokassa on funktioita eri tyyppisten tietojen lukemiseen, jolloin syötteitä ei tarvitse lukea aina merkkijonona ja muuntaa sitten kohdetyypikseen. Myös tiedoston lukeminen onnistuu Scanner-luokan avulla. Rakentajalle on vain annettava standardisyöteolion asemasta java.io.File-luokasta luotu tiedosto-olio, joka puolestaan luodaan antamalla File-luokan rakentajalle tiedoston nimi. Esimerkiksi tiedostoa data.txt lukevan Scanner-olion saa luotua lausekeella new Scanner(new File("data.txt")).

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.util.Scanner;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

public class LineWrap1 {
  // The stream operations in the main function may cause an exception of type
  // IOException. Therefore we have added an exception specification
  // "throws IOException" to the end of its header.
  public static void main(String args[]) throws IOException {
    // Create a Scanner object "user" for reading System.in.
    // Note that, unlike with files, we should not use try-with-resources here
    // because the standard input stream should not be closed.
    Scanner user = new Scanner(System.in);
    while(true) {  // An infinite loop that will be exited upon a "quit" command.
      System.out.print("Give input filename (or 'quit'): ");
      final String inputFilename = user.nextLine();
      if("quit".equalsIgnoreCase(inputFilename)) {
        break;
      }
      System.out.print("Give line wrap length limit (or 'quit'): ");
      String lineLimStr = user.nextLine();
      if("quit".equalsIgnoreCase(lineLimStr)) {
        break;
      }
      final int lineLim = Integer.parseInt(lineLimStr);
      System.out.print("Give output filename (or 'quit'): ");
      final String outputFilename = user.nextLine();
      if("quit".equalsIgnoreCase(outputFilename)) {
        break;
      }

      // Also count statistics about input and output lines.
      int inputLines = 0;
      int outputLines = 0;

      // The form of try-with-resources:
      //   try(resource variable statements separated by semicolons) {
      //     the code block that uses the resources
      //   }
      // The resources listed by the resource variable statements will be closed
      // automatically when the program exits the following code block. The resources
      // must have a member function "close". In the try-with-resources below we create
      // a Scanner input for file reading and a PrintStream output for file writing.
      // This is also an example where using the inferred var type might be ok: the
      // concrete type has a longish name and is anyway spelled out in the new operation.
      try(var input = new Scanner(new File(inputFilename));
          var output = new PrintStream(outputFilename)) {
        int lineLen = 0;
        while(input.hasNextLine()) {
          String line = input.nextLine();
          inputLines += 1;
          // Split interprets its parameter as a regular expression, where "\\s+"
          // means one or more space characters (space, tabulator, etc.).
          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;
        }
      }
      // At this point the input and output have been closed automatically.
      // Print out input and output line statistics. This time as an example to System.err.
      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);
    }
  }
}

Vanhempi tapa 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.

Yllä annettu esimerkkiohjelma voitaisiin toteuttaa vanhemmalla tavalla seuraavasti:

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

public class LineWrap2 {
  // The stream operations in the main function may cause an exception of type
  // IOException. Therefore we have added an exception specification
  // "throws IOException" to the end of its header.
  public static void main(String args[]) throws IOException {
    // Create a BufferedReader object "user" for reading System.in.
    // Note that, unlike with files, we should not use try-with-resources here
    // because the standard input stream should not be closed.
    BufferedReader user = new BufferedReader(new InputStreamReader(System.in));
    while(true) {  // An infinite loop that will be exited upon a "quit" command.
      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;
      }

      // Also count statistics about input and output lines.
      int inputLines = 0;
      int outputLines = 0;

      // The form of try-with-resources:
      //   try(resource variable statements separated by semicolons) {
      //     the code block that uses the resources
      //   }
      // The resources listed by the resource variable statements will be closed
      // automatically when the program exits the following code block. The resources
      // must have a member function "close". In the try-with-resources below we create
      // a BufferedReader input for file reading and a PrintStream output for file writing.
      // This is also an example where using the inferred var type might be ok: the
      // conrete type has a longish name and is anyway spelled out in the new operation.
      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 interprets its parameter as a regular expression, where "\\s+"
          // means one or more space characters (space, tabulator, etc.).
          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;
        }
      }
      // At this point the input and output have been closed automatically.
      // Print out input and output line statistics. This time as an example to System.err.
      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ä: