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 InputStream
→ InputStreamReader
→ BufferedReader
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);
}
}
}