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 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.
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);
}
}
}