12 Streams

27
12 Streams 12.1 Inleiding Vanuit een Java-programma kun je gegevens opslaan in een bestand op de schijf. Dit wordt aangeduid met de term schrijven (Engels: to write). Ook kun je gegevens ophalen uit een bestand, meestal aangeduid met de term lezen (to read). Wat betreft het schrijven en lezen van bestanden is er in Java een groot verschil tussen applets en applicaties. De mogelijkheden voor applets zijn zeer beperkt. Om veiligheidsredenen is het voor een applet niet toegestaan een bestand op de schijf van de gebruiker te lezen of te schrijven. Dat is begrijpelijk, omdat een applet in het algemeen deel uitmaakt van een webpagina en kwaadwillenden via een applet anders heel makkelijk toegang zouden kunnen hebben tot de gegevens op de computer van de argeloze gebruiker die de desbetreffende webpagina opent. Applicaties hebben op het gebied van bestanden in principe geen beperkingen. Daarom geef ik alle voorbeelden in dit hoofdstuk in de vorm van applicaties. In feite kent Java twee soorten applicaties: grafische en niet-grafische. De laatste soort wordt ook wel console-applicatie genoemd. In plaats van lezen en schrijven worden ook de termen invoer en uitvoer gebruikt, in het Engels: input en output. Het duo input en output wordt vaak afgekort tot I/O, een afkorting die onder andere terug te vinden is in de naam van een belangrijke package uit de Java klassenbibliotheek die voorzieningen bevat voor het lezen en schrijven, de package java.io. Er zijn in Java twee wezenlijk verschillende manieren om met bestanden te werken: door een bestand te behandelen als een random access file of als een stream. In een random access file, het woord zegt het al, heb je op willekeurige manier toegang tot de gegevens in het bestand. Dat wil zeggen: je kunt achter in het bestand beginnen met lezen, dan driehonderd plaatsen terug, opnieuw iets lezen of iets schrijven, honderd posities vooruit in het bestand, et cetera. In dit boek komen random acces files niet, maar streams wel aan bod. Een stream is een stroom van gegevens. Met een stream doorloop je de gegevens in een bestand van voor naar achter. De gegevens komen een voor een voorbij, zoals blaadjes op het water van een stroom. Je hebt bij een stream sequentiële toegang tot de gegevens, dat wil zeggen: je hebt toegang tot de gegevens precies in de volgorde waarin ze in het bestand staan. Bij een random access file ben je niet gehouden aan sequentiële toegang. Streams hebben niet alleen met bestanden te maken, maar een stream is een manier om allerlei vormen van gegevenstransport in een computer te beschrijven: gege-

Transcript of 12 Streams

Page 1: 12 Streams

12 Streams

12.1 Inleiding

Vanuit een Java-programma kun je gegevens opslaan in een bestand op de schijf.Dit wordt aangeduid met de term schrijven (Engels: to write). Ook kun je gegevensophalen uit een bestand, meestal aangeduid met de term lezen (to read).

Wat betreft het schrijven en lezen van bestanden is er in Java een groot verschiltussen applets en applicaties. De mogelijkheden voor applets zijn zeer beperkt. Omveiligheidsredenen is het voor een applet niet toegestaan een bestand op de schijfvan de gebruiker te lezen of te schrijven. Dat is begrijpelijk, omdat een applet in hetalgemeen deel uitmaakt van een webpagina en kwaadwillenden via een appletanders heel makkelijk toegang zouden kunnen hebben tot de gegevens op decomputer van de argeloze gebruiker die de desbetreffende webpagina opent.

Applicaties hebben op het gebied van bestanden in principe geen beperkingen.Daarom geef ik alle voorbeelden in dit hoofdstuk in de vorm van applicaties. Infeite kent Java twee soorten applicaties: grafische en niet-grafische. De laatste soortwordt ook wel console-applicatie genoemd.

In plaats van lezen en schrijven worden ook de termen invoer en uitvoer gebruikt, inhet Engels: input en output. Het duo input en output wordt vaak afgekort tot I/O,een afkorting die onder andere terug te vinden is in de naam van een belangrijkepackage uit de Java klassenbibliotheek die voorzieningen bevat voor het lezen enschrijven, de package java.io.

Er zijn in Java twee wezenlijk verschillende manieren om met bestanden te werken:door een bestand te behandelen als een random access file of als een stream.

In een random access file, het woord zegt het al, heb je op willekeurige maniertoegang tot de gegevens in het bestand. Dat wil zeggen: je kunt achter in hetbestand beginnen met lezen, dan driehonderd plaatsen terug, opnieuw iets lezen ofiets schrijven, honderd posities vooruit in het bestand, et cetera. In dit boek komenrandom acces files niet, maar streams wel aan bod.

Een stream is een stroom van gegevens. Met een stream doorloop je de gegevens ineen bestand van voor naar achter. De gegevens komen een voor een voorbij, zoalsblaadjes op het water van een stroom. Je hebt bij een stream sequentiële toegang totde gegevens, dat wil zeggen: je hebt toegang tot de gegevens precies in de volgordewaarin ze in het bestand staan. Bij een random access file ben je niet gehouden aansequentiële toegang.

Streams hebben niet alleen met bestanden te maken, maar een stream is een manierom allerlei vormen van gegevenstransport in een computer te beschrijven: gege-

Page 2: 12 Streams

376 OO-PROGRAMMEREN IN JAVA MET BLUEJ

vens die via een toetsenbord of via een netwerk binnenkomen, of gegevens dietussen twee programma’s worden uitgewisseld kun je beschouwen als een stream.

Het woord stream is een abstractie voor alle vormen van sequentieel gegevens-transport in een computer of netwerk. In Java is een stream een instantie van eenvan de vele streamklassen uit de package java.io.

Streams zijn in Java verdeeld in twee hiërarchieën, byte-streams en character-streams. Een byte-stream transporteert zijn gegevens in eenheden van een byte(8 bits), en character-streams in eenheden van Unicode-tekens (16 bits). Character-streams zijn dan ook speciaal geschikt voor het transporteren van teksten.

12.2 Niet-grafische applicaties

In een niet-grafische applicaties zijn er geen knoppen, menu’s of invoervakken. Eenniet-grafische applicatie draait in een console-venster en wordt ook wel console-applicatie genoemd. De communicatie met de gebruiker verloopt via tekst die hetprogramma van links naar rechts en van boven naar onder in het venster kanzetten. De gebruiker kan tekst invoeren, niet in een tekstvak, maar de tekst die jeintikt kom, net als de uitvoer, in het console-venster.

12.2.1 Uitvoer in een console-applicatie

Een console-applicatie kan tekst in het console-venster zetten met behulp vanSystem.out.print() of System.out.println(). Het verschil tussen beideopdrachten staat uitgelegd aan het eind van deze paragraaf.

Hier is een voorbeeld van een eenvoudige console-applicatie:

// Uitvoer in console-vensterpublic class Deel3en5 { public static void main( String[] args ) { int x = 3, y = 5; double d = (double) x / y;

System.out.print( “x=” ); System.out.println( x );

System.out.println( “y=” + y );

System.out.print( “x gedeeld door y = “ ); System.out.println( d ); }}

De uitvoer van deze applicatie kun je zien in figuur 12.1.

Page 3: 12 Streams

37712 Streams

De hele applicatie bestaat uit een klasse met één statische methode met de naammain(). Deze methode wordt automatisch aangeroepen bij het starten van deapplicatie.

Merk op dat er in de broncode van dit voorbeeld geen import-opdrachten staan.Dat is niet nodig omdat alle opdrachten in dit programma uit de klasse Systemkomen, waar elk Java-programma (applet of applicatie) zonder meer over kanbeschikken.

De klasse System is een klasse die een public attribuut heeft met de naam out. Ditattribuut is een referentie naar een object van de klasse PrintStream. Het attribuutout wordt bij de start van elk programma (applet of applicatie) automatisch geïni-tialiseerd en zorgt voor de uitvoer naar het console-venster.

De methoden print() en println() van de klasse PrintStream (waar out dus eeninstantie van is) kennen een heleboel overloaded varianten, zoals:

void print( boolean b ) void println( boolean b )void print( char c ) void println( char c )void print( int i ) void println( int i )void print( double d ) void println( double d )void print( String s ) void println( String s )void print( Object o ) void println( Object o )

Het verschil tussen print() en println() is het volgende:• De methode println() stuurt nadat hij zijn argument op het scherm gezet heeft

de cursor naar de volgende regel. Dit is vergelijkbaar met het drukken op Enteraan het einde van een regel in een tekstverwerker. Alles wat je ná println()op het scherm zet komt dus op een volgende regel.

• De methode print() zet zijn argument op het scherm, maar dan blijft decursor op dezelfde regel staan, zodat het argument van de volgende print() ofprintln() op dezelfde regel komt.

12.2.2 Invoer in een console-applicatie

Behalve het public attribuut out heeft de klasse System nog een public attribuutmet de naam in. Dit is een referentie naar een instantie van de klasse InputStream.

Figuur 12.1

Page 4: 12 Streams

378 OO-PROGRAMMEREN IN JAVA MET BLUEJ

Deze klasse is een zogenaamde low-level klasse, dat wil zeggen dat je met methodenvan deze klasse alleen karakter voor karakter kunt lezen en het is lastig om bijvoor-beeld een getal in te lezen met behulp van een InputStream.

Er bestaat een andere klasse voor invoer met de naam BufferedReader. Hiermeekun je een hele regel inlezen als een String, om deze eventueel om te zetten naareen getal. De constructor van BufferedReader verwacht een argument van het typeInputStreamReader, die op zijn beurt een InputStream zoals System.in verwacht.

In het volgende voorbeeld zie je hoe je een geheel en een gebroken getal kuntinvoeren.

// Invoer in een console-applicatieimport java.io.*;

public class InvoerToetsenbord { public static void main( String[] args ) { BufferedReader toetsenbord = new BufferedReader( new InputStreamReader( System.in) ); int i = 0; double d = 0.0; System.out.println( “Typ een geheel getal:” );

try { String s = toetsenbord.readLine(); i = Integer.parseInt( s ); System.out.println( “Typ een gebroken getal:” ); s = toetsenbord.readLine(); d = Double.parseDouble( s ); System.out.println( “Je tikte “ + i + “ en “ + d ); } catch( IOException ioe ) { System.out.println( “Fout bij inlezen”); } catch( NumberFormatException nfe ) { System.out.println( “Fout bij omzetten van invoer naar getal”); } }}

Mogelijk resultaat van dit programma zie je in figuur 12.2.

De methode readLine() leest een regel invoer (tot je op Enter drukt). Omdat er bijhet lezen wel eens iets mis kan gaan kan deze methode een IOException leveren. Inhoofdstuk 10 kun je zien dat dit een checked exception is die je moet opvangen.

De methoden parseInt() en parseDouble() kunnen een NumberFormatExceptionveroorzaken als het ingevoerde getal niet omgezet kan worden naar een int of eendouble. Ook deze exceptie kun je in hetzelfde try-catch blok opvangen.

Page 5: 12 Streams

37912 Streams

12.3 Tekstbestanden

Een tekstbestand is een bestand dat uit letters, cijfers en andere leestekens bestaaten dat je gewoon kunt lezen als je het opent in een tekstverwerker.

Er zijn veel verschillende manieren om een tekstbestand te maken, onder anderemet een FileWriter, met een BufferedWriter of met een PrintWriter. In essentiekomen deze drie manieren op hetzelfde neer, maar in details zijn ze verschillend. Ikzal alle drie manieren in de voorbeelden in de volgende paragrafen bespreken.

Om een bestand te maken moet je in het algemeen de volgende vier dingen doen:• een naam voor het bestand bedenken en het bestand openen met behulp van een

constructor;• gegevens naar het bestand schrijven;• het bestand sluiten;• de mogelijke excepties veroorzaakt door de vorige handelingen opvangen.

Zowel bij het openen als het sluiten van een bestand voert het besturingssysteemvan de computer waarop het programma draait een aantal handelingen uit waar-door het bestand daadwerkelijk op de schijf komt.

12.3.1 Een tekstbestand maken met een FileWriter

Het volgende programma maakt een bestand met behulp van een FileWriter. Deconstructor van FileWriter kan een IOExceptie leveren. Een FileWriter isspeciaal geschikt voor het maken van tekstbestanden.

// Tekstbestand maken met FileWriterimport java.io.*;

public class TekstbestandMaken1 { public static void main( String[] args ) { FileWriter uit; try { uit = new FileWriter( “dagen.txt” );

Figuur 12.2

Page 6: 12 Streams

380 OO-PROGRAMMEREN IN JAVA MET BLUEJ

System.out.println( “Bestand wordt nu gemaakt...” ); for( int i=1; i<=7; i++ ) { String dag = “ dag “ + i; uit.write( dag ); } uit.close(); System.out.println( “Bestand is gesloten” ); } catch( IOException e ) { System.out.println( “Fout bij het openen, maken of sluiten bestand” ); e.printStackTrace(); } }}

De uitvoer van dit programma bestaat, behalve uit wat mededelingen op hetscherm, vooral uit het bestand dat is gemaakt. De inhoud van het bestand kun je(bijvoorbeeld) met een programma als Kladblok of de editor van JCreator lezen, ziefiguur 12.3.

Figuur 12.3

In de eerste regel van main() staat de declaratie van een FileWriter:

FileWriter uit;

Vervolgens maak je een instantie van FileWriter door de constructor de naamvoor het nieuw te maken bestand te geven:

uit = new FileWriter( “dagen.txt” );

Je kunt in de naam van het bestand een pad opgeven. Vervelend is dat het schei-dingsteken in een pad voor verschillende besturingssystemen anders is, in Unix ishet de slash / en in Windows de backslash \. Via de public-variabele separator inde klasse File kun je het scheidingsteken opvragen. Dat betekent dat je op eenWindows-machine het pad C:\Temp\dagen.txt als volgt kunt maken:

String filenaam = “C:” + File.separator + “Temp” + File.separator + “dagen.txt”

Page 7: 12 Streams

38112 Streams

Daarna kun je het bestand openen met:

uit = new FileWriter( filenaam );

Als je een pad opgeeft dat niet bestaat, levert dit een exceptie. Als het bestand albestaat wordt de tekst die je schrijft aan het eind van het bestaande bestand toe-gevoegd, zie ook paragraaf 12.3.2.

De eerste fase, het bestand openen, is hiermee achter de rug. Je kunt nu in gegevensin het bestand schrijven. Hiertoe heeft de klasse FileWriter een paar varianten vande methode write(), waarvan de belangrijkste een String schrijft:

for( int i = 1; i <= 7; i++ ) { String dag = “ dag “ + i; uit.write( dag );}

De laatste fase bestaat uit het sluiten van het bestand:

uit.close();

Hierdoor krijgt het besturingssysteem opdracht het schrijven naar het bestand af teronden en het benodigde interne geheugen dat nodig was voor het maken van hetbestand weer vrij te geven.

Het geheel staat in een try-catch-blok en de reden is de volgende: zowel deconstructor van FileWriter als de methode write() én de methode close()kunnen een IOException opleveren. Er kan immers bij het openen, of bij hetschrijven, of bij het sluiten van een bestand iets mis gaan. Een IOException is eengecontroleerde exceptie en deze moet je deze opvangen óf achter throws melden inde kop van de methode.

Er zijn in dit geval drie plaatsen die een IOException kunnen genereren en als je ereen opvangt weet je niet welke van de drie de oorzaak is: het openen, schrijven ofsluiten. Door in het catch-blok met e.printStackTrace() de call-stack op hetscherm te laten zetten krijg je meer informatie, zoals het regelnummer in de bron-code waar de exceptie is opgetreden. Een alternatief is dat je elk van de drie hande-lingen in een apart try-catch-blok zet, zodat je elk een eigen handler kunt geven.

12.3.2 Toevoegen of overschrijven

De klasse FileWriter heeft verschillende constructoren waarvan ik er hier tweenoem. Dit is de eerste:

FileWriter( String fileName ) throws IOException

Wanneer je met deze constructor een bestand opent dat al bestaat en je schrijftvervolgens tekst naar dat bestand, komt deze tekst automatisch aan het eind vanhet bestaande bestand. De tekst wordt dus toegevoegd (Engels: to append).

Page 8: 12 Streams

382 OO-PROGRAMMEREN IN JAVA MET BLUEJ

Een andere constructor ziet er zo uit:

FileWriter( String fileName, boolean append ) throws IOException

Via het tweede argument van deze constructor kun je aangeven dat de tekst die jewegschrijft niet toegevoegd moet worden: als je append de waarde false geeftwordt de tekst niet toegevoegd, maar het bestaande bestand wordt overschrevendoor de nieuwe tekst.

12.3.3 Een nieuwe regel in een bestand maken

In het bestand dagen.txt, waarvan je de inhoud in figuur 12.3 kunt zien, staan alledagen op dezelfde regel. Als je ze onder elkaar wilt hebben moet je het teken vooreen nieuwe regel naar het bestand schrijven. Dat kun je doen met de tekens ‘\n’ en‘\r’, die de betekenis hebben van newline (nieuwe regel) en return ( terug naar hetbegin van de regel). De tekens ‘\r’ en ‘\n’ zijn voorbeelden van zogenaamdeescape-sequences. Deze twee tekens zorgen op een Windows-machine voor watnormaal de Enter-toets in een tekstverwerker doet. Op een Unix-machine doetalleen het teken ‘\n’ hetzelfde.

De twee tekens kun je samen als string schrijven: “\n\r” en om in het bestand ophet Windows-platform de dagen onder elkaar te krijgen vervang je het for-state-ment in het vorige voorbeeld door het volgende:

for( int i = 1; i <= 7; i++ ) { String dag = “ dag “ + i + “\n\r”; uit.write( dag );}

Dit is een vrij primitieve manier om een bestand van een regelindeling te voorzien,vooral omdat deze manier platform-afhankelijk is. Java heeft een betere oplossingin de vorm van een BufferedWriter.

12.3.4 Een tekstbestand maken met een BufferedWriter

De klasse BufferedWriter spaart de tekens die hij moet schrijven op in een buffer,een tijdelijke opslagplaats van 8 kilobyte (8192 bytes) in het interne geheugen, omze vervolgens in één keer naar het bestand te schrijven. Dat werkt veel efficiënterdan het telkens schrijven van kleine stukjes tekst wat FileWriter normaal doet.

Veel input en output vindt plaats tussen apparaten die met verschillende snelhedenwerken. Zo is intern geheugen vele malen sneller toegankelijk dan schijfgeheugenen een printer is veel langzamer dan de computer die de gegevens verstuurt. Omhet snelle apparaat niet onnodig te laten wachten op het langzame apparaat dientde buffer. Een buffer is een stuk geheugen dat fungeert als tijdelijke opslag van degegevens die van het ene naar het andere apparaat gaan.

Bij wijze van voorbeeld zie je in het schema in figuur 12.4 een PC die gegevens naareen bestand stuurt. In feite stuurt de PC de gegevens niet rechtstreeks naar het

Page 9: 12 Streams

38312 Streams

bestand maar via een buffer die ingebouwd is in BufferedWriter. De verbindingtussen PC en buffer is een snelle verbinding. Als de buffer vol is kan de PC (en dusjij als gebruiker) andere dingen gaan doen. De buffer stuurt ondertussen op zijngemak de gegevens naar het bestand. Als de buffer leeg is vult de PC dezeopnieuw, en herhaalt het proces zich.

Figuur 12.4

Het principe van een buffer wordt veel toegepast, en niet alleen in computers. Infeite is de voorraad pakjes boter in de supermarkt ook een buffer tussen de snellegroothandel en de langzame klanten: de groothandel brengt binnen een paarminuten 1000 pakjes die de klanten verspreid over een hele dag een voor eenkomen kopen.

Een BufferedWriter heeft nog een voordeel: hij heeft een methode newLine() diehet platform-afhankelijke teken voor een nieuwe regel wegschrijft.

De klasse BufferedWriter heeft twee constructoren:

BufferedWriter( Writer out )BufferedWriter( Writer out, int size )

Met de tweede constructor kun je eventueel de standaardgrootte van de bufferveranderen. In beide gevallen maakt BufferedWriter gebruikt van een Writer. DeFileWriter uit de vorige paragraaf is een voorbeeld van een Writer. Er bestaan ernog meer, zie figuur 12.5.

Figuur 12.5

Uit de definitie van de constructor volgt dat je pas een BufferedWriter kunt makenals je beschikt over een andere Writer, zoals een FileWriter, bijvoorbeeld zo:

FileWriter filewriter = new FileWriter( “dagen.txt” );BufferedWriter uit = new BufferedWriter( filewriter );

PCsnel langzaam

buffer bestand

BufferedWriter

CharArrayWriter

FilterWriter

OutputStreamWriter FileWriter

Writer (abstract)

PipedWriter

PrintWriter

StringWriter

Page 10: 12 Streams

384 OO-PROGRAMMEREN IN JAVA MET BLUEJ

Het kan korter door een anoniem FileWriter-object te maken en dit aan de con-structor door te geven:

BufferedWriter uit = new BufferedWriter( new FileWriter( “dagen.txt” ) );

In figuur 12.6 zie je hoe je deze koppeling kunt voorstellen.

Het volgende voorbeeld gebruikt een BufferedWriter om een rijtje dagen onderelkaar in een bestand te zetten.

// Tekstbestand maken met BufferedWriter (en FileWriter)import java.io.*;

public class TekstbestandMaken2 { public static void main( String[] args ) { BufferedWriter uit;

try { String bestandnaam = “dagen.txt”; uit = new BufferedWriter( new FileWriter( bestandnaam ) );

for( int i = 1; i <= 7; i++ ) { String dag = “ dag “ + i; uit.write( dag ); uit.newLine(); } uit.close(); System.out.println( “Bestand “+ bestandnaam + “ is geschreven” ); } catch( IOException e ) { System.out.println( “Fout bij het openen, maken of sluiten bestand” ); e.printStackTrace(); } }}

Het gemaakte bestand ziet er geopend in Kladblok uit als in figuur 12.7.

In dit voorbeeld zijn twee streams geopend, een FileWriter en een Buffered-Writer, maar je hoeft er maar één te sluiten: als je de BufferedWriter sluit wordtautomatisch de zogeheten onderliggende stroom ook gesloten.

Figuur 12.6

char Java- programma

String bestand

BufferedWriter FileWriter

char

Page 11: 12 Streams

38512 Streams

De klasse BufferedWriter heeft niet veel methoden. De belangrijkste twee heb ik alin dit voorbeeld gebruikt:

void write( String s ) throws IOExceptionvoid newLine() throws IOException

Als je getallen wilt schrijven met een BufferedWriter moet je de getallen eerstconverteren naar een String.

De wijze waarop BufferedWriter en FileWriter in dit programma samenwerkenis typisch voor de manier waarop je in Java met stream-klassen kunt omgaan. Veelvan die klassen kun je via hun constructor aan elkaar koppelen, zodat je precies defunctionaliteit krijgt die je nodig hebt. Het aan elkaar koppelen van twee of drieinstanties van stream-klassen komt heel veel voor. In de volgende paragraaf zie jedaar een ander voorbeeld van.

12.3.5 Een tekstbestand maken met PrintWriter

Een interessante klasse is PrintWriter, die allerlei varianten heeft van de metho-den print() en println():

void print( boolean b ) void println( boolean b )void print( char c ) void println( char c )void print( int i ) void println( int i )void print( double d ) void println( double d )void print( String s ) void println( String s )void print( Object o ) void println( Object o )

Ook PrintWriter heeft een constructor die een Writer accepteert, dat betekent datje er bijvoorbeeld een FileWriter aan kunt koppelen. Dat is niet heel efficiëntomdat FileWriter noch PrintWriter een buffer heeft. Als je een bestand wiltmaken met FileWriter, gebruik wilt maken van de veelheid aan methoden vanPrintWriter en ook van efficiëntie van BufferedWriter, dan koppel je instantiesvan deze drie klassen aan elkaar:

Figuur 12.7

Page 12: 12 Streams

386 OO-PROGRAMMEREN IN JAVA MET BLUEJ

Writer uit = new PrintWriter( new BufferedWriter( new FileWriter( “maand.txt” ) ) );

In figuur 12.8 zie je hoe je de koppeling van deze drie instanties kunt voorstellen.

In het volgende voorbeeld is een en ander toegepast.

// Tekstbestand maken met PrintWriter// (en BufferedWriter en FileWriter)import java.io.*;

public class TekstbestandMaken3 { public static void main( String[] args ) { PrintWriter uit = null;

try { String bestandnaam = “maand.txt”; uit = new PrintWriter( new BufferedWriter( new FileWriter( bestandnaam ) ) ); System.out.println( “Bestand “+ bestandnaam + “ is gemaakt” ); } catch( IOException e ) { System.out.println( “Fout bij het openen van het bestand” ); e.printStackTrace(); }

if( uit != null ) { for( int i = 1; i <= 12; i++ ) { uit.print( “maand “ ); uit.println( i ); } uit.close(); }

if( uit.checkError() ) System.out.println( “Fout in PrintWriter” ); }}

Figuur 12.8

boolean char double int Object String

char charbestand

BufferedWriterFileWriter

char

PrintWriter

Java- programma

Page 13: 12 Streams

38712 Streams

Geen van de methoden van PrintWriter levert een IOException zoals de metho-den van de andere Writer-klassen wel doen. In plaats daarvan heeft PrintWritereen methode checkError() die true levert als er iets mis is gegaan bij het gebruikvan PrintWriter.

12.3.6 Een tekstbestand lezen

Een tekstbestand lezen lijkt sterk op het maken van een bestand. Ook bij het lezenmoet je in het algemeen vier dingen doen:• het bestand openen;• gegevens uit het bestand lezen;• het bestand sluiten;• de mogelijke excepties veroorzaakt door de vorige handelingen opvangen.

Voor het lezen uit een bestand kun je FileReader gebruiken. Net als FileWriter isdit een vrij primitieve klasse en het leesproces kun je efficiënter maken door File-Reader te koppelen aan een BufferedReader. Het voordeel van BufferReader isbovendien dat je gebruik kunt maken van de methode readLine() die een heleregel tekst inleest.

Het programma in het volgende voorbeeld leest het tekstbestand dagen.txt dat inhet voorbeeld TekstbestandMaken2 in paragraaf 12.3.4 gemaakt is met behulp vaneen BufferedWriter en FileWriter.

// Tekstbestand lezen met BufferedReader en FileReaderimport java.io.*;

public class TekstbestandLezen { public static void main( String[] args ) { BufferedReader in; String regel; try { in = new BufferedReader( new FileReader( “dagen.txt” ) ); while ( ( regel = in.readLine() ) != null ) { System.out.println( regel ); } in.close(); } catch( FileNotFoundException e ) { System.out.println( “Kan bestand niet vinden” ); } catch( IOException e ) { System.out.println( “Fout bij het lezen of sluiten bestand” ); e.printStackTrace(); } }}

Page 14: 12 Streams

388 OO-PROGRAMMEREN IN JAVA MET BLUEJ

De constructor van FileReader levert een FileNotFoundException, wat eensubklasse is van IOException. In principe kun je een FileNotFoundExceptionopvangen met catch(IOException e), maar voor de duidelijkheid heb ik er voorgekozen hem apart op te vangen. Hij moet dan wel als eerste in de catch-lijst staan(zie paragraaf 10.6.1).

De methode readLine() van BufferedReader leest een hele regel uit de stream,dus tot aan het eerste newline-character of tot het einde van de stream, en levertvervolgens deze regel af als een String. Het newline-character komt niet in destring.

Als de stream geen tekens meer bevat, in dit voorbeeld is dat zo als je aan het eindevan het bestand bent, levert readLine() de waarde null. Van deze eigenschapmaakt het vorige voorbeeld gebruik:

while ( ( regel = in.readLine() ) != null ) { System.out.println( regel );}

De conditie in dit while-statement is wat complex, maar wordt in deze situatie veeltoegepast. Het is een assignment en een vergelijking in één. Dat dit een geldigeuitdrukking is komt doordat de waarde van een assignment-statement gelijk is aande waarde die het linkerlid na afloop van het assignment-statement heeft. Dus dewaarde van de assignment regel = in.readLine() is regel. En de waarde vanregel kun je vergelijken met null.

Het while-statement hierboven is dus gelijkwaardig met volgende fragment:

regel = in.readLine();while ( regel != null ) { System.out.println( regel ); regel = in.readLine();}

12.3.7 Getallen in een tekstbestand

Getallen kun je in een tekstbestand opslaan, maar je moet er rekening mee houdendat je ze als string wegschrijft en ook weer als string inleest. Om er mee te kunnenrekenen moet je de strings na het inlezen dus converteren naar een getalwaarde. Ditprincipe is bekend van getallen in een tekstvak.

Voor het correct inlezen van getallen uit een tekstbestand is het handig elke getalop een aparte regel te zetten, de overgang op een nieuwe regel fungeert dan alsscheidingsteken tussen de getallen. Als je dat niet doet krijg je bij het inlezen metreadLine() een string met meer dan één getal, wat het converteren lastiger maakt,maar het kan met bijvoorbeeld de methode split(), zie paragraaf 12.4.

Het volgende fragment schrijft een tekst, een geheel en een gebroken getal naar eenbestand:

Page 15: 12 Streams

38912 Streams

// Schrijven van tekst en getallentry { BufferedWriter uit = new BufferedWriter( new FileWriter( “getallen.txt” ) );

String s = “twee getallen:”; int i = 1234567; double d = 3.14;

uit.write( s ); uit.newLine();

uit.write( “” + i ); uit.newLine();

uit.write( “” + d ); uit.newLine();

uit.close();}catch( IOException e ) { System.out.println( “Fout bij het openen, maken of sluitenbestand” ); e.printStackTrace();}

Dit fragment produceert het bestand dat je in figuur 12.9 kunt zien.

Figuur 12.9

Het inlezen van de getallen gaat in principe als in het volgende fragment.

// Lezen van tekst en getallenString regel = null;String tekst = null;int geheelGetal = 0;double gebrokenGetal = 0;

try { BufferedReader in = new BufferedReader( new FileReader( “getallen.txt” ) );

Page 16: 12 Streams

390 OO-PROGRAMMEREN IN JAVA MET BLUEJ

tekst = in.readLine(); regel = in.readLine(); geheelGetal = Integer.parseInt( regel );

regel = in.readLine(); gebrokenGetal = Double.parseDouble( regel );

in.close();

System.out.println( tekst ); System.out.println( geheelGetal ); System.out.println( gebrokenGetal );}catch( FileNotFoundException e ) { System.out.println( “Kan bestand niet vinden” );}catch( IOException e ) { System.out.println( “Fout bij het lezen of sluiten bestand” ); e.printStackTrace();}catch( NumberFormatException e ) { System.out.println( “Fout in omzetten naar getal” + regel );}

De broncode van beide fragmenten kun je vinden in het project GetallenSchrijven-Lezen in de map Voorbeelden\Hoofdstuk 12

12.4 Een string splitsen

Stel dat je een tekstbestand krijgt met regels die bestaan uit tekst en twee getallen,zoals bijvoorbeeld deze bestellijst met een omschrijving, een aantal en een prijs pereenheid:

Capuccino 4 2.50Thee 3 2.00Gebak 7 3.50

In dit voorbeeld scheidt een spatie de woorden en getallen van elkaar. De verschil-lende elementen die in een regel voorkomen kun je splitsen met de methodesplit() van de klasse String. Deze methode is onderdeel van niet al te oudeversies van de JDK (vanaf versie 1.4).

Met het volgende stukje code splits je bijvoorbeeld de eerste regel. De methodesplit() levert het resultaat van de splitsing af in een array van strings. Het schei-dingsteken komt er niet in terecht.

String regel = “Capuccino 4 2.50”;String match = “ “; // spatie als scheidingstekenString[] veld = regel.split( match );

Page 17: 12 Streams

39112 Streams

for( int i = 0; i < veld.length; i++ ) System.out.println( veld[ i ] );

Na afloop bevat de array veld drie String-elementen:

Capuccino42.50Desgewenst kun je de getallen omzetten naar int of double.

12.4.1 Reguliere expressies

De string die fungeert als scheidingsteken in de methode split() moet een zogehe-ten reguliere expressie bevatten. Reguliere expressies gebruik je om in een tekst eenbepaald teken of een bepaald patroon te vinden. Sommige symbolen hebben in eenreguliere expressie een speciale betekenis, vergelijkbaar met het sterretje * alszogeheten wild card of joker in een zoekopdracht naar een bestand. De uitdrukkingreguliere expressie wordt soms afgekort tot regex.

Een paar voorbeelden. Stel dat de volgende regel tekst is gegeven en dat je die jewilt ontleden met de spatie als scheidingsteken:

String regel = “Cappucino, Koffie, Thee”;String match = “ “;String[] veld;

Merk op dat er voor het woord Thee vier spaties staan. Met de volgende opdrachtsplits je de regel:

veld = regel.split( match );

Na afloop van deze opdracht bevat de array veld de volgende strings:

Cappucino,Koffie,(lege string)(lege string)(lege string)Thee

Dit komt omdat de string als volgt wordt ontleed:

Cappucino,spatieKoffie,spatielege stringspatielege stringspatielege stringspatieThee

Page 18: 12 Streams

392 OO-PROGRAMMEREN IN JAVA MET BLUEJ

De vier spaties scheiden het woord Koffie (met komma), drie lege strings en hetwoord Thee van elkaar.

12.4.2 Het plusteken in een reguliere expressie

Het plusteken + fungeert in een reguliere expressie als symbool voor: een of meervan het voorgaande. Dus de reguliere expressie a+ betekent: één of meer a’s. Met deregex spatie gevolgd door plusteken kun je een tekst waarin een of meer spaties tussende woorden staat op een nette manier splitsen:

String regel = “Cappucino, Koffie, Thee”;String match = “ +”; // spatie en plustekenString[] veld = regel.split( match );

Dit levert:

Cappucino,Koffie,Thee

In feite is niet de spatie, maar een komma gevolgd door een (of meer) spaties hetscheidingsteken tussen de drie woorden. In een reguliere expressie geef je dat aanmet komma spatie plusteken, wat betekent: een komma gevolgd door één of meerspaties.

String regel = “Cappucino, Koffie, Thee”;String match = “, +”;String[] veld = regel.split( match );

Dit levert:

CappucinoKoffieThee

Merk op dat de komma nu niet meer in het resultaat voorkomt.

12.4.3 De punt als scheidingsteken

De punt als scheidingsteken is een apart probleem, omdat de punt in een reguliereexpressie staat voor: een willekeurig teken. Er geldt dat backslash gevolgd door eenpunt, dus \. in een regex de letterlijke punt voorstelt. Maar de Java-compiler zietde backslash in een string als een speciaal teken dat gebruikt wordt voor eenzogeheten escape sequence. Zo stelt de escape sequence \n de overgang op eennieuwe regel voor. En de escape sequence \\ wordt door de Java-compiler omgezetin één enkele backslash.

De escape sequence \. die in een regex de letterlijke punt voorstelt is niet geldig inJava. Dat betekent dat je in Java twee backslashes gevolgd door een punt moetschrijven, dus \\. om \. als reguliere expressie te krijgen.

Page 19: 12 Streams

39312 Streams

Concreet:

String regel = “javax.swing.*”;String match= “\\.”;String[] veld = regel.split( match );

Dit levert:

javaxswing*

12.4.4 Vierkante haken in een reguliere expressie

Vierkante haken hebben in een reguliere expressie de betekenis van of, toegepast opalles wat tussen de haken staat. Dat wil zeggen: [abc] betekent a, of b of c. En met[\\. ] kun je in Java een tekst splitsen bij een punt of een spatie.

String regel = “import javax.swing.*”;String match= “[\\. ]”;String[] veld = regel.split( match );

Dit levert:

importjavaxswing*

12.4.5 Meer over reguliere expressies

Over reguliere expressies valt veel meer te vertellen en ze kunnen bijzonder com-plex worden. Een grapje onder programmeurs is wel: je hebt een probleem en jebesluit voor de oplossing een reguliere expressies te gebruiken; dan heb je tweeproblemen. Daar staat tegenover dat reguliere expressies bijzonder krachtig zijn en,indien goed gebruikt, veel regels code kunnen uitsparen. Op internet is heel veelinformatie over reguliere expressies te vinden.

12.5 Byte-streams

Naast character-streams die je met Writers maakt en met Readers leest, kent Javabyte-streams. Vrijwel alle informatie in een computer codeer je in de vorm van bytes,dat wil zeggen dat je met byte-streams in principe elke soort informatie kuntschrijven en lezen: tekst, getallen, objecten, afbeeldingen, muziekbestanden, video,et cetera.

Aan de basis van de hiërarchie van byte-streamklassen staan twee abstracte klas-sen: OutputStream en InputStream (zie figuur 12.10 en figuur 12.11). Alle stream-klassen erven van een van beide. Voor het schrijven en lezen van bestanden zijn de

Page 20: 12 Streams

394 OO-PROGRAMMEREN IN JAVA MET BLUEJ

klassen FileOutputStream en FileInputStream van belang. Instanties van dezeklassen zijn low-level streams, dat wil zeggen ze creëren een stroom van bytes en jekunt met deze streams niet veel meer doen dan bytes in de stroom stoppen of bytesuit de stroom halen.

Voor de meeste programmeurs is het natuurlijk vervelend om alle soorten gege-vens te converteren van of naar bytes. Daarom zijn er high-level streams die voordeze conversie zorgen. Bijvoorbeeld DataOutputStream en DataInputStream. Deklasse DataOutputStream beschikt bijvoorbeeld over methoden als writeInt(),writeDouble(), writeUTF(). Door een high-level stream aan een low-level streamte koppelen krijgt de stream de functionaliteit die je wenst.

Figuur 12.10

12.5.1 Het maken van een bestand met een byte-stream

Het volgende programma maakt een bestand met drie gegevens over een baby,voor de verandering is het geen console- maar een grafische-applicatie. Uitleg volgtna de broncode.

import java.io.*;import java.awt.*;import javax.swing.*;

Figuur 12.11

ByteArrayInputStream

FileInputStream

FilterInputStream

ObjectInputStream

InputStream

PipedInputStream

SequenceInputStream

BufferedInputStream

DataInputStream

LineNumberInputStream

PushBackInputStream

StringBufferInputStream

ByteArrayOutputStream

FileOutputStream

FilterOutputStream

ObjectOutputStream

OutputStream

PipedOutputStream

BufferedOutputStream

DataOutputStream

PrintStream

Page 21: 12 Streams

39512 Streams

public class Bytestreambestand extends JFrame { private JPanel paneel;

public Bytestreambestand() { paneel = new Bestandpaneel(); setContentPane( paneel ); }

public static void main( String args[] ) { JFrame frame = new Bytestreambestand(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setBounds( 100, 100, 350, 100 ); frame.setTitle( “Maak een bestand met een byte-stream” ); frame.setVisible( true ); }}

public class Bestandpaneel extends JPanel { public Bestandpaneel() { try { DataOutputStream uit = new DataOutputStream( new FileOutputStream( “geboorteGegevens.dat” ) );

// Schrijf gegevens uit.writeUTF( “Welmer”); uit.writeInt( 1973 ); uit.writeDouble( 3.020 );

// Sluit de stream uit.close(); } catch( IOException e ) { System.out.println( “IO-exceptie” ); } }

public void paintComponent( Graphics g ) { super.paintComponents( g ); g.drawString( “Bestand is klaar”, 20, 50 ); }}

Het eerste wat in dit programma gebeurt is het openen van een nieuw bestand meteen FileOutputStream. De naam van het bestand is het argument van de construc-tor:

new FileOutputStream( “geboorteGegevens.dat” )

Page 22: 12 Streams

396 OO-PROGRAMMEREN IN JAVA MET BLUEJ

Vervolgens koppel je aan deze stream een DataInputStream om de gegevensmakkelijker naar de stream te kunnen schrijven. De enige constructor van DataInputStream heeft een argument van het (abstracte) type OutputStream. Daar kunje dus een FileOutputStream voor invullen:

DataOutputStream uit = new DataOutputStream( new FileOutputStream( “geboorteGegevens.dat” ) );

Hierna kun je de gegevens naar deze stream schrijven.

uit.writeUTF( “Welmer”);uit.writeInt( 1973 );uit.writeDouble( 3.020 );

Voor het schrijven van strings gebruik je writeUTF(). Het meer voor de handliggende writeString() bestaat niet. De letters UTF zijn een afkorting van UCSTransformation Format, waarbij UCS weer een afkorting is van Universal CharacterSet.

Veel Aziatische talen maken gebruik van een grote verzameling karakters. Dezekunnen niet weergegeven worden in 16-bits Unicode die normaal is in Java. UTF isontstaan als een universele codering voor alle mogelijke tekens die in alle talen vande wereld gebruikt worden. UTF maakt gebruik van flexibele codering met 1, 2 of3 bytes. Hierdoor kunnen veel meer verschillende tekens gecodeerd worden. Kortgezegd: de methode writeUTF() zorgt ervoor dat een String-object uit Java in eenbestand komt.

Voor het schrijven van een int is er de methode writeInt() en voor het schrijvenvan een double de methode writeDouble().

De DataOutputStream met de naam uit geeft de gegevens automatisch in de vormvan bytes door aan de FileOutputStream, die ze op zijn beurt in het bestand metde aangegeven naam schrijft.

Schematisch kun je je dit voorstellen als in figuur 12.12.

Figuur 12.12

12.5.2 Het lezen van een bestand met een byte-stream

Uit een bestand dat je met het koppel DataOutputStream/FileOutputstreamgemaakt hebt, kun je lezen met het koppel DataInputStream/FileInputstream. Inhet volgende programma–fragment wordt dat gedaan.

byte byteJava- programma

int double String bestand

DataOutputStream FileOutputStream

Page 23: 12 Streams

39712 Streams

String naam;int gebJaar;double gewicht;

try { DataInputStream in = new DataInputStream( new FileInputStream( “geboorteGegevens.dat” ); );

naam = in.readUTF(); gebJaar = in.readInt(); gewicht = in.readDouble(); in.close();}catch( IOException e ) { System.out.println( “Kan niet uit het bestand lezen” ); e.printStackTrace();}

12.5.3 Schrijven en lezen van objecten

Het schrijven en lezen van primitieve typen is interessant, maar in een object-georiënteerd programma ligt het meer voor de hand dat je de toestand van eenobject wilt vastleggen om later weer met het object in dezelfde toestand verder tegaan. Dit heet in het Engels: to serialize en kun je voor elkaar krijgen met Object-OutputStream en ObjectInputStream.

Deze klassen implementeren respectievelijk de interfaces DataOutput en DataInputen beschikken dan ook over alle methoden als writeInt(), writeDouble(),writeUTF() en readInt() en readDouble() en readUTF().

Daarnaast hebben ze een methode writeObject() en readObject() en om dezemethoden gaat het nu. De enige voorwaarde om een object te kunnen schrijven isdat de klasse waartoe het object behoort de interface Serializable implementeert.

Een interface implementeer je door al zijn methoden te implementeren, maar in ditgeval is dat erg eenvoudig want de interface Serializable heeft geen enkelemethode, dus je hoeft niets anders te doen dan de woorden implements Serial-izable aan de klasse toe te voegen. Hierdoor behoren alle objecten die je weg-schrijft (of inleest) tot een en hetzelfde type en dat is genoeg voor writeObject()en readObject() om hun werk goed te doen.

Een instantie van een klasse kan attributen hebben die zelf objecten zijn. Dezeobjecten worden automatisch ook geschreven onder de voorwaarde dat ze van eenserialiseerbare klasse zijn.

Hieronder staat een klasse Persoon die de interface Serializable implementeert.De klasse heeft als attributen onder andere een String en een Datum. De standaard-klasse String is serialiseerbaar, en de zelfgedefinieerde klasse Datum is dat ook.

Page 24: 12 Streams

398 OO-PROGRAMMEREN IN JAVA MET BLUEJ

public class Persoon implements Serializable { private String naam; private Datum gebDatum; private double gewicht;

public Persoon( String naam, Datum gebDatum, double gewicht ) { this.naam = naam; this.gebDatum = gebDatum; this.gewicht = gewicht; }

public String toString() { return naam + “, geb: “+ gebDatum + “, gew: “ + gewicht + “ kilo”; }}

public class Datum implements Serializable { private int dag, maand, jaar;

public Datum( int dag, int maand, int jaar ) { this.dag = dag; this.maand = maand; this.jaar = jaar; }

public String toString() { return dag + “-” + maand + “-” + jaar; }}

De volgende broncode maakt een instantie van Persoon (en daarmee van Datum),schrijft het object naar een bestand en leest dat object vervolgens weer in.

// Schrijven en lezen van een objectimport java.io.*;import java.awt.*;import java.awt.event.*;import javax.swing.*;

public class SchrijfLeesObject extends JFrame { public SchrijfLeesObject() { setContentPane( new Persoonpaneel() ); }

public static void main( String args[] ) { JFrame frame = new SchrijfLeesObject(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setBounds( 100, 100, 350, 200 );

Page 25: 12 Streams

39912 Streams

frame.setTitle( “Object schrijven en lezen met een stream” ); frame.setVisible( true ); }}

public class Persoonpaneel extends JPanel { private Persoon kind; private Persoon ingelezenKind;

public Persoonpaneel() { // Schrijven van object try { ObjectOutputStream uit = new ObjectOutputStream( new FileOutputStream( “persoon.dat” ) );

kind = new Persoon( “Charlotte”, new Datum( 1,11,1994 ), 3.1 ); uit.writeObject( kind ); uit.close(); } catch( IOException e ) { System.out.println( “IO-exceptie” ); }

// Lezen van object try { ObjectInputStream in = new ObjectInputStream( new FileInputStream( “persoon.dat” ) ); ingelezenKind = (Persoon) in.readObject(); in.close(); } catch( ClassNotFoundException e ) { System.out.println( “Onbekende klasse” ); } catch( IOException e ) { System.out.println( “IO-exceptie” ); } }

public void paintComponent( Graphics g ) { super.paintComponent( g ); g.drawString( “Object is geschreven”, 50, 50 ); g.drawString( ingelezenKind.toString(), 50, 80 ); }}

Page 26: 12 Streams

400 OO-PROGRAMMEREN IN JAVA MET BLUEJ

De uitvoer zie je in figuur 12.13.

Figuur 12.13

Het schrijven van een object is niet veel ingewikkelder dan het schrijven van eenint of een double. Het enige waar je voor moet zorgen is dat de betreffende klasseen zijn attributen de interface Serializable implementeren.

De methode readObject() levert een object af van het type Object (de moeder vanalle klassen in Java). Via typecasting kun je het type van dit object omzetten in hetgewenste, in dit geval door de cast (Persoon):

persoon = (Persoon) in.readObject();

Behalve een IOException kan de methode readObject() een ClassNotFound-Exception genereren, en ook deze exceptie moet je opvangen.

12.6 Samenvatting

• Een stream is een stroom van gegevens.• Tot een stream heb je sequentiële toegang.• Om een bestand te kunnen maken of lezen moet je het eerst openen.• Een tekstbestand schrijven en lezen doe je met een of meer van de Writer- en

Reader-klassen.• Een ingelezen string kun je splitsen met de methode split() uit de klasse

String.• Bij het splitsen maak je gebruik van reguliere expressies.• Voor het schrijven van bestanden die geen tekstbestanden zijn kent Java byte-

streams.• Ook objecten kun je met een byte-stream schrijven en lezen, mits ze de interface

Serializable implementeren.

Page 27: 12 Streams

40112 Streams

12.7 Vragen

1. Wat is een random access file?2. Wat is een stream?3. Waarom zijn applets beveiligd tegen lezen en schrijven van bestanden?4. Waarom is het handig een FileWriter en een BufferedWriter aan elkaar te

koppelen?5. Kun je met een byte-stream ook teksten lezen?6. Welke interface moet je implementeren om een object in een bestand te kunnen

schrijven. Welke methoden heeft deze interface?7. Welke streams gebruik je om een object te schrijven?

12.8 Opgaven

1. Schrijf een applicatie die een willekeurig bestand met java-broncode op hetscherm laat zien waarbij elke regel voorafgegaan wordt door een regelnummeren een dubbele punt.Bovendien komt aan het einde een melding of het aantal openingsaccolades inde broncode gelijk is aan het aantal sluitaccolades.

2. Schrijf een applicatie dat een willekeurig bestand met java-broncode inleest enhet volgende bepaalt:• de gemiddelde regellengte;• de lengte van de langste regel;• de eerste regel met die langste lengte;• het aantal keren dat een regel met deze langste lengte voorkomt.

3. Schrijf een applicatie die, wanneer een woord en een bestandsnaam gegevenzijn, alle regels van dat bestand laat zien waarin dat woord letterlijk voorkomt.

4. Schrijf een applicatie die je de kans geeft een tekstbestand te openen in eenJTextArea, zie de JDK-documentatie voor informatie over JTextArea.Je kunt hierin niet alleen .java-bestanden openen, maar ook.html-, .asp- of.txt-bestanden. Zorg dat een aantal van deze bestanden in een map staan diebereikbaar is vanuit de applicatie.Wanneer je op de knop Openen klikt wordt het bestand geopend en de inhoudvan het bestand op het scherm getoond.Wanneer je wijzigingen hebt aangebracht in de tekst, kun je de nieuwe versiebewaren onder een andere naam.