© 1999-2001, Flemming Koch Jensen
Alle rettigheder forbeholdt
Streams

Abstract:

Næsten alt i java.io bliver berørt i dette kapitel, så det kunne lige så godt have heddet "java.io". Det eneste der direkte ikke berøres er filer, som er henlagt til sit eget kapitel. Først behandles de forskellige former for streams, opdelt i Input- og OutputStreams, der behandler data som bytes, dernæst Readers og Writers der behandler data som tegn. StreamTokenizers giver mulighed for at arbejde med en stream på samme måde som StringTokenizer arbejder på en String.

Forudsætninger:

Det er nødvendigt at have et vist kendskab til exceptions. I eksempler kan det ikke undgåes at exceptions anvendes, da visse eksempler ellers ikke ville kunne compileres, endsige køres. Man skal have kendskab til threads for at forstå afsnittet om PipedInput-/OutputStream. Det er en fordel at kende StringTokenizer i forbindelse med afsnittet om StreamTokenizer. Eksemplet i afsnittet om StreamTokenizer kræver kendskab til tekstfiler.

 

 

 

1. Input- og OutputStreams

Datakilden er sekvens af bytes

Alle Input-/OutputStreams ser på datakilden som en sekvens af bytes. De to abstrakte klasser InputStream og OutputStream er superklasser for en lang række streams. Selv har de kun få metoder, men deres subklasser realiserer forskellige måder at arbejde med datakilden. De er ofte brugt som et abstrakte interfaces af andre streams, der kan hente deres data fra datakilden gennem en anden stream uden at vide konkret hvilken slags der er tale om.

 

1.1 InputStream

En InputStream henter data fra datakilden og sender dem til client.
Figur 1:
Data fra datakilden gennem InputStream
InputStream giver naturligvis mulighed for at læse bytes fra datakilden. Dette kan gøres med en af følgende tre metoder:
int read()
int read( byte[] buffer )
int read( byte[] buffer, int offset, int length )
Den parameterløse version er erklæret abstract og er dermed den der gør selve klassen abstract. Alle andre metoder i InputStream er som et minimum implementeret med stubbe. Metoden returnerer den næste byte fra datakilden som int. Det betyder at byten behandles som 8 bit uden fortegn, og derfor ikke som den primitive datatype byte. De mulige værdier bliver derfor fra 0 til 255. Hvis der ikke er flere bytes tilbage i datakilden returneres -1.
De to sidste versioner af read, returnerer data i et array henholdsvis et del-array, som er givet som parameter. I begge tilfælde indlæses der det antal bytes der er plads til i arrayet henholdsvis del-arrayet. Metoderne returnerer hvor mange bytes der reelt blev indlæst; hvilket kan være mindre end pladsen tillod.

 

1.1.1 Del-arrays

Del-arrays anvendes i forbindelse med en del streams. Et del-array angives ved arrayet, et offset og en længde. F.eks. følgende array.
Figur 2:
Del-array
En del af arrayet er markeret lysere, det er det del-array vil vi arbejde med. Det angives med offset 2, der er index for første position i arrayet, og længden 5, der er længden af del-arrayet.
Hvis vi derfor ønskede at indlæse dette del-array fra en InputStream kunne vi gøre det med:
int antal;

antal = s.read( vorTabel, 2, 5 );

System.out.println( "Indlæst " + antal + " bytes" );

Indlæst 5 bytes

Hvor antal efterfølgende ville indeholde antallet af bytes det lykkedes at læse fra datakilden.

 

1.1.2 Blokkering

Alt efter hvilken datakilde der gemmer sig bag InputStreams interface kan den parameterløse read blokkere, dvs. den ikke returnerer før datakilden kan levere en byte. Dette gør sig f.eks. gældende hvis der er tale om en netværksforbindelse. Man har derfor mulighed for at anvende metoden:
int available()
til at undersøge hvor mange bytes der er klar fra datakilden. F.eks. kunne man betinge kaldet af read for at undgå blokkering:
Source 1:
Sikring mod blokkering
if ( s.available() > 0 )
  next = s.read();
else
  ...
En anden metode der beskæftiger sig med bytes ud fra en mængde-betragtning er:
long skip( long n )
Metoden prøver at overspringe de næste n bytes, og returnerer hvor mange det lykkedes at overspringe. Metoden blokkerer derfor ikke.

 

1.1.3 Mærker

Det er muligt at sætte et mærke, for senere at vende tilbage til et sted i datastrømmen og gentage læsningen. Man kan kun sæt ét sådant mærke, hvilket gøres med metoden:
void mark( int limit )
Når metoden kaldes, vil InputStream huske positionen og hvis man senere kalder metoden:
void reset()
vil den repositionere sig hvor mærket blev sat, og man kan gentage læsningen af de bytes man tidligere har haft lejlighed til at læse.
Fra starten er mærket placeret på første byte. Hvis man kalder reset uden at have kaldt mark tidligere, kommer man derfor tilbage til starten af datastrømmen og kan læse det hele igen.
Implementeringen af mærker kan være problematisk alt efter hvilken datakilde dergemmer sig bag InputStream interfacet. Derfor behøver en stream ikke understøtte mærker. Til at angive om man understøtter mærker, findes følgende metode:
boolean markSupported()
Hvis man er i tvivl bør man derfor kalde denne metode for at få vished om hvorvidt mærker understøttes.
Som man måske bemærkede vedrørende mark, så tager den en parameter. Denne parameter vedrører også vanskeligheden i at tilbyde mærker. Parameteren angiver hvor mange bytes frem (fra mærket) streamen er forpligtiget til at understøtte en tilbagevenden til mærket. Når man har læst ud over denne afstand kan man ikke komme tilbage ved at kalde reset. Denne udformning af mark overlader ansvaret for effektiviteten til client, da det er op til den ikke at fråse.
Den sidste metode i InputStream er:
void close()
men den vil vi i det følge kun berøre i forbindelse med filer.

 

1.2 OutputStream

En OutputStream modtager data fra client og sender dem videre til en destination.
Figur 3:
Data til destinationen gennem OutputStream
Svarende til InputStream's tre read-metoder har OutputStream tre write-metoder:
void write()
void write( byte[] buffer )
void write( byte[] buffer, int offset, int length )
Den parameterløse version er igen den der gør klassen abstract. Den tager en int som parameter, der maskes til en byte før den sendes til destinationen.
Analogt til InputStream's to sidste read-metoder, arbejder de to sidste versioner af write med henholdsvis et array og et del-array. Data hentes fra disse parametre og sendes til destinationen.
OutputStream understøtter ikke mærker. Man kunne i princippet forestille sig mærker anvendt på den måde, at man kunne vende tilbage og overskrive data man allerede havde skrevet.
Alt efter hvilken stream der gemmer sig bag OutputStream's interface, kan der være anvendt en buffer i implementationen. Vi skal se nærmere på buffere i forbindelse med BufferedInput-/OutputStream. Til at gennemtvinge en tømning af bufferen findes metoden:
void flush()
Metoden er naturligvis virkningsløs hvis der ikke anvendes en buffer i implementationen af streamen.
Endelig har OutputStream en close-metode svarende til InputStream's. Vi vil ligeledes kun berøre close i forbindelse med filer.

 

1.3 ByteArrayInput-/OutputStream

Datakilde og destination er et array af bytes ByteArrayInputStream og ByteArrayOutputStream bruger et array af bytes som datakilde/destination. datakilden/destionationen er derfor meget konkret, da den ikke er en anden InputStream, men derimod den mest primitive sekventielle datastruktur der findes: et array.

 

1.3.1 ByteArrayInputStream

ByteArrayInputStream har to konstruktorer:
ByteArrayInputStream( byte[] buffer )
ByteArrayInputStream( byte[] buffer, int offset, int length )
De tager begge datakilden som parameter, henholdsvis et array eller et del-array af bytes. Man skal være opmærksom på at der ikke sker nogen kopiering af datakilden, hvilket åbner mulighed for at man kan ændre i den mens man læser fra den.
Mærker er understøttet. I den forbindelse har reset en speciel effekt hvis man har taget udgangspunkt i et del-array. Hvis man ikke har sat nogen mærker vil et kald af reset bringe os tilbage til elementet med index 0, også selv om offset var angivet til  en værdi større end 0. Dette giver adgang til elementer før del-arrayet, mens det ikke er muligt at få adgang til elementer på positioner efter del-arrayet. Om dette er man kan springe fra del-arrayet og ud i arrayet på denne måde er en fejl der er blevet ophævet til feature er uvist!

 

1.3.2 ByteArrayOutputStream

ByteArrayOutputStream er naturligvis modstykket til ByteArrayInputStream, og de fleste metoder er da også i tråd med dette.
Der er to konstruktorer:
ByteArrayOutputStream()
ByteArrayOutputStream( int size )
Parameteren size angiver startstørrelsen på det array som bruges som intern datarepræsentation (default er 32). Hvis arrayet bliver for lille "udvides" det på samme måde som java.util.Vector gør. Altså på samme måde et dynamisk tilbud til client, som implementeres med en statisk datastruktur. Behageligt at arbejde med, men måske ikke det mest effektive i større målestok.
Interfacet for ByteArrayOutputStream er udvidet med en række metoder. Først og fremmest er der:
int size()
der returneres ikke arrayets længde, men hvor mange bytes der er skrevet til arrayet.
Hvis man skulle fortryde de data man har sendt til arrayet, kan man slette dem alle med:
void reset()
Hvis man ønsker at få arrayet efter endt påfyldning af data, kan man bruge
byte[] toByteArray()
der returnerer et nyt array af bytes, indeholdende de data der står i den interne repræsentation. Bortset fra kopieringen over i et nyt array, kommer ByteArrayOutputStream dermed til at fungere som en Builder med toByteArray som getProduct-metode (se evt. Builder pattern).
Specielt til testformål kan metoden toString være nyttig. Der returneres en String, hvor de enkelte bytes er oversat til tegn.

 

1.3.3 Eksempel

Følgende eksempel illustrerer hvordan ByteArrayInputStream og ByteArrayOutputStream kan anvendes:
Source X:
Anvendelse af ByteArray-InputStream og ByteArray-OutputStream
import java.io.*;

class TestByteArrayStreams {

  public static void main( String[] argv ) {

    ByteArrayOutputStream output = new ByteArrayOutputStream( 3 );

    for ( byte b=65; b<91; b++ )
      output.write( b );

    System.out.println( output.size() );

    System.out.println( output.toString() );

    ByteArrayInputStream input = new ByteArrayInputStream( output.toByteArray() );

    output.reset();

    System.out.println( output.size() );

    input.skip( 20 );

    while ( input.available() > 0 )
      System.out.print( input.read() + " " );
    System.out.println();
  }
}

26
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0
85 86 87 88 89 90

Værdierne 65 til 90 er valgt fordi de er ASCII-værdierne for de store bogstaver i det engelske alfabet. Der startes ud med et lille internt array på længde 3 for at illustrere at det "udvides" ved flere værdier.

 

1.3 FilterInput-/OutputStream

FilterInputStream og FilterOutputStream er kun Bridges til InputStream henholdsvis OutputStream (se evt. Bridge pattern).
Selvom man kan lave instanser af dem begge er deres rolle i virkeligheden at være superklasser for en række streams der ikke selv har data men blot er delegerende streams til og fra en anden stream. Disse subklasser realiserer en udvidet funktionalitet i forhold til det grundlæggende interface fra InputStream og OutputStream.

 

1.3.1 DataInput-/OutputStream

Idéen med DataInputStream og DataOutputStream er at gøre det nemt at arbejde med de primitive datatyper. De har en række af read- og write-metoder for hver af disse typer.
Disse metoder er en implementation af interfacene DataInput og DataOutput.

 

1.3.1.1 DataInput

Dette interface specificerer en række metoder der retter sig mod de primitive typer. Det er:
boolean readBoolean()
byte    readByte()
char    readChar()
double  readDouble()
float   readFloat()
int     readInt()
long    readLong()
short   readShort()
Ud over disse er der også:
int readUnsignedByte()
int readUnsignedShort()
der er nyttige, hvis man vil arbejde med bytes uden fortegn (en short er to bytes) og ikke som i java, hvor alle primitive numeriske typer regnes med fortegn.
To andre metoder er
void readFully( byte[] buffer )
void readFully( byte[] buffer, int offset, int length )
der er blokkerende read-metoder, der kun returnerer når arrayet henholdsvis del-arrayet er fyldt med data fra datakilden, eller hvis slutningen af streamen nås (f.eks. eof) eller der opstår en IOException.
metoden
String readUTF()
der bruges til at læse tekst i et modificeret UTF-8 format. Vi skal ikke her se på dette format, men jeg vil anbefale, at man kun bruger det på data der er fremkommet ved den tilsvarende metode
void writeUTF( String s )
i DataOutputs interface.
endelig er der:
int skipBytes( int n )
der er analog til skip i InputStreams interface og derfor er overflødig i den sammenhæng.

 

1.3.1.2 DataOutput

DataOutput har den tilsvarende write-metode for de primitive typer og modificeret UTF-8.
Der findes ingen metoder til byte og short uden fortegn, men disse ville heller ikke være relevante.
Ligeledes er der ingen blokkerende fully-metoder, da de almindelige write-metoder med array henholdsvis del-array af bytes opfylder behovet for denne funktionalitet.

 

1.3.2 DataInputStream

Ud over interfacet fra FilterInputStream (nedarvet uændret fra DataInputStream) og DataInput supplerer DataInputStream kun selv med en enkelt static metode
static String readUTF( DataInput in )
der kan bruges til at læse modificeret UTF-8 fra et DataInput.

 

1.3.3 DataOutputStream

Derimod har DataOutputStream selv en række metoder der rækker ud over de to interfaces fra FilterOutputStream (nedarvet uændret fra DataOutputStream) og DataOutput. Det er
int size()
der fortæller hvor mange bytes der pt. er skrevet ud på streamen, og
void flush()
der tømmer en evt. buffer ved at kalde videre til OutputStream.

 

1.3.2 BufferedInput-/OutputStream

BufferedInputStream og BufferedOutputStream arbejder begge med en intern buffer, men så hører ligheden også op. Motivationen for bufferen er vidt forskellig.

 

1.3.2.1 BufferedInputStream

Idéen med en buffer i BufferedInputStream er at understøtte en InputStream med mærker, hvis den ikke selv gør det.
Mærkerne implementeres ved at man gemmer alle indlæste bytes, siden sidste mærke blev sat, i en buffer så man på den måde kan understøtte reset. Bufferen droppes dog hvis læsningen når ud over den grænse man specificerer i mark-kaldet.
Det hele er rimelig enkelt, men bevirker at BifferInputStream adapter InputStream, så man opnår den øgede funktionalitet (se evt. Adapter pattern).
BufferedInputStream har to konstruktorer:
BufferedInputStream( InputStream in )
BufferedInputStream( InputStream in, int size )
in er den InputStream der skal adaptes, mens size giver mulighed for selv at bestemme den initielle størrelse af bufferen, der internt er et array af bytes. Default er 2 KB.
Hvis bufferen bliver for lille reallokerer den á la Vectors implementation.

 

1.3.2.2 BufferedOutputStream

Idéen med en buffer i BufferedOutputStream er at beskytte en OutputStream mod mange små stykker data og i stedet samle passende portioner i en buffer før OutputStream besværes med dem.
Om det er hensigtsmæssigt at anvende BufferedOutputStream afhænger naturligvis af hvad der gemmer sig bag OutputStreams interface.
BufferedOutputStream har to konstruktorer, der ligner BufferedInputStreams:
BufferedOutputStream( OutputStream in )
BufferedOutputStream( OutputStream in, int size )
out er naturligvis den OutputStream der skal adaptes, mens size er størrelsen på bufferen. Default er ½ KB.
Til forskel fra BufferedInputStream sker der ingen udvidelse af bufferen hvis den løber fuld. I stedet tømmes den ud på OutputStream. En sådan tømning sker naturligvis også hvis man kalder metoden flush.

 

1.4 PipedInput-/OutputStream

PipedInputStream og PipedOutputStream bruges til at sende data mellem threads. Afsenderen har en PipedOutputStream og modtageren har en PipedInputStream. De to streams er associerede. Det er et simplificeret Forwarder-Receiver pattern uden marshalling, hvor bytes sendes mellem processer (se evt. Forwarder-Receiver pattern).

 

1.4.1 PipedInputStream

PipedInputStream har en cirkulær buffer på 1 KB, som  den opbevarer data i efterhånden som de ankommer. Modtagelsen af bytes sker ved at PipedPotputStream kalder en protected metode receive, der skriver i bufferen. Hvis bufferen løber fuld blokkerer denne metoder og venter på at der bliver plads.
PipedInputStream har to konstruktorer:
PipedInputStream()
PipedInputStream( PipedOutputStream src )
Default-konstruktoren initialiserer en PipedInputStream, der andnu ikke er forbundet med nogen PipedOutputStream.
Den anden forbinder ved initialiseringen instansen til den PipedOutputStream, der angives som parameter. Man skal i den forbindelse være opmærksom på at forbindelsen oprettes gensidig. Man skal derfor ikke efterfølgende tilmelde PipedInputStream hos PipedOutputStream, der klarer de selv blot den ene bliver tilmeldt hos den anden.
Hvis man venter med at tilmelde en PipedOutputStream, så gøres det senere med metoden:
void connect( PipedOutputStream src )
Når en PipedInputStream først er blevet forbundet med en PipedOutputStream kan den ikke genbruges i forbindelse med en anden PipedOutputStream. Man kan altså ikke lade connect på PipedInputStreamen igen. Det omvendte gør sig ikke gældende, en PipedOutputStream kan godt genbruges i forbindelse med en anden PipedInputStream, som altså skal være "ubrugt". En PipedOutputStream har flere liv!
Man kan se hvor mange bytes der står og venter i bufferen med et kald af available.
read.metoderne blokkerer alle, hvis der ikke er mindst én byte at læse fra bufferen. Dette gælder altså også de read-metoder der læser til et array henholdsvis del-array.
read-metoderne kaster naturligvis en IOException hvis der opstår en I/O-fejl, men i den forbindelse regnes det også som en I/O-fejl hvis den tråd der har PipedOutputStreamen terminerer uden at have kaldt close, og bufferen løber tom. I denne situation blokkeres altså ikke selvom bufferen løber tom. Bemærk at en thread der har en PipedOutputStream bør kalde close på den før den selv terminerer.

 

1.4.2 PipedOutputStream

PipedOutputStream har analogt konstruktorer svarende til PipedInputStreams
PipedOutputStream()
PipedOutputStream( PipedInputStream src )
og de fungerer fuldstændig analogt, blot set fra sender-siden.
Også connect-metoden er svrende til PipedOutputStreams.
Som nævnt overfor blokkerer receive hvis bufferen løber fund, derfor kan write-metoderne blokkere i denne situation og man har ingen mulighed for at undgå det. I forbindelse med at receive blokkerer kaldet, kalder den med et sekunds mellemrum notifyAll i et forsøg på at vække en eller anden den forhåbentlig vil læse fra bufferen så der bliver plads.
Man kan selv gøre det inden man evt. foretager et blokkerende write-kald ved at kalde flush på PipedOutputStream, der får PipedInputStream i den adnen ende til at udføre notifyAll.

 

1.4.3 Eksempel

Lad os se et eksempel på to threads der kommunikerer med hinanden i det klassiske Producer-Consumer pattern.

import java.io.*;

class Producer extends Thread {

  private PipedOutputStream output;

  public Producer( PipedOutputStream out ) {
    output = out;
  }

  public void run() {
    try {
      for ( byte b=0; b<10; b++ )
        output.write( b );
    }
    catch ( IOException e ) {
      System.out.println( "[Producer] I/O fejl" );
    }
  }
}

import java.io.*;

class Consumer extends Thread {

  private PipedInputStream input;

  public Consumer( PipedInputStream in ) {
    input = in;
  }

  public void run() {
    try {
      while ( true )
        System.out.println( "[Consumer] " + input.read() );
    }
    catch ( IOException e ) {
      System.out.println( "[Consumer] I/O fejl" );
    }
  }
}

import java.io.*;

class TestPipedStreams {

  public static void main( String[] argv ) {
    PipedOutputStream output=null;
    PipedInputStream input=null;

    try {
      output = new PipedOutputStream();
      input = new PipedInputStream( output );
    }
    catch ( IOException e ) {
      System.out.println( "[TestPipedStreams] I/O fejl" );
      System.exit( 1 );
    }

    new Consumer( input ).start();
    new Producer( output ).start();
  }
}

[Consumer] 0
[Consumer] 1
[Consumer] 2
[Consumer] 3
[Consumer] 4
[Consumer] 5
[Consumer] 6
[Consumer] 7
[Consumer] 8
[Consumer] 9
[Consumer] I/O fejl

Bemærk at vi her lidt brutalt anvender den IOException der kastes når tråden med PipedOutputStream terminerer uden at kalde close og bufferen løber tom, til også at stoppe Consumeren.

 

1.5 Specielle InputStreams

Ved specielle InputStreams forstå dem der ikke har en korresponderende OutputStream.

 

1.5.1 SequenceInputStream

Idéen med SequenceInputStream er at sammensætte (concatenere) flere InputStreams. Når en stream slutter, starter den næste osv.
SequenceInputStream har to konstruktorer alt efter om man vil samle to eller flere InputStreams
SequenceInputStream( InputStream s1, InputStream s2 )
SequenceInputStream( Enumeration e )
Den første tager to InputStreams, hvoraf den venstre bliver den første der læses fra.
Den anden tager en Enumeration af InputStreams (Smid f.eks. først InputStreams i en Vector og kald elements-metoden for at få den som en Enumeration).
available er begrænset idet den kun fortæller hvor mange bytes der er til rådighed fra den stream den pr. er igang med. Det giver lidt problemer, betragt følgende eksempel:
SequenceInputStream input = new SequenceInputStream( ... );

while ( input.available() > 0 )
  ...
Her vil while-løkken terminerer når den første InputStream løber tom. Hvis man vil lave en løkke der gennemløber hele SequenceInputStream skal man desværre selv beregne hvor mange elementer der er og bruge en standard for-løkke.
Mht. close-metoden og Enumerations skal man være opmærksom på at alle InputStreams der endnu ikke er færdiglæst vil blive lukket en efter en i forbindelse med close-kaldet..

 

1.5.1.1 Eksempel

Lad os se et eksempel med SequenceInputStream.

import java.io.*;

class TestSequenceInputStream {

  public static void main( String[] argv ) {

    ByteArrayOutputStream out_1 = new ByteArrayOutputStream();
    ByteArrayOutputStream out_2 = new ByteArrayOutputStream();

    for ( int b=0; b<10; b++ )
      out_1.write( b );
    for ( int b=10; b<20; b++ )
      out_2.write( b );

    ByteArrayInputStream in_1 = new ByteArrayInputStream( out_1.toByteArray() );
    ByteArrayInputStream in_2 = new ByteArrayInputStream( out_2.toByteArray() );

    try {
      SequenceInputStream input = new SequenceInputStream( in_1, in_2 );

      for ( int i=0; i<20; i++ )
        System.out.print( input.read() + " " );
      System.out.println();
    }
    catch ( IOException e ) {
      System.out.println( "I/O fejl" );
    }
  }
}

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Der laves to ByteArrayOutputStreams der fyldes med talene fra 0 til 9 henholdsvis fra 11 til 19. Dernæst laves de tilsvarende ByteArrayOutputStreams der skal bruge til SequenceOutputStreamen. I try-blokken er samlet alt med SequenceOutputStream selv om det rent faktisk kun er read der kan kaste en IOException. Dernæst udskrives bytes fra den sammensatte stream og man ser at tallene nu som forventet passer sammen.

 

1.5.2 PushbackInputStream

Idéen med PushbackInputStream er at kunne fortryde læsning fra InputStream. PushbackInputStream er en FilterInputStream og føjer derfor denne undo-funktionalitet til en InputStream.
PushbackInputStream har tre unread-metoder
void unread( int b )
void unread( byte[] buffer )
void unread( byte[] buffer, int offset, int length )
der en for en er en undo-metode for de tre tilsvarende read-metoder.
Bemærk at unread-metoderne giver mulighed for at unreade bytes man ikke har læst, da man selv skal anføre de pågældende bytes som parameter.
Selve unread-funktionaliteten er implementeret med en buffer i form af et byte array. Bufferens størrelse fastlægges ved instantiering og man kan ikke senere udvide den ved reallokering af arrayet. Det betyder at bufferen kan løbe fuld. Hvis det sker vil unread-metoden kaste en IOException og bufferen data vil være uberørte af kaldet (fra bufferen løb fuld).
PushbackInputStream har to konstruktorer:
PushbackInputStream( InputStream in )
PushbackInputStream( InputStream in, int size )
size er størrelsen af bufferen. Default er en!
At default er sølle en byte skyldes primært at der i JDK 1.0 ikke var nogen buffer. På daværende tidspunkt var det kun muligt at unreade én byte. Bufferen, og den anden konstruktor, kom i JDK 1.1.
PushbackInputStream understøtter ikke mærker.
available returnerer summen af hvad der ligger i bufferen og hvad InputStreamen siger den har.

 

1.5.3 Eksempel

Lad os se et eksempel med PushbackInputStream.

import java.io.*;

class TestPushbackInputStream {

  public static void main( String[] argv ) {

    try {
      ByteArrayOutputStream output = new ByteArrayOutputStream();

      for ( byte b=0; b<10; b++ )
        output.write( b );

      PushbackInputStream input =
        new PushbackInputStream(
          new ByteArrayInputStream( output.toByteArray() ), 10 );

      for ( int i=0; i<5; i++ )
        System.out.print( input.read() + " " );
      System.out.println();

      for ( byte b=0; b<5; b++ )
        input.unread( b );

      while ( input.available() > 0 )
        System.out.print( input.read() + " " );
      System.out.println();
    }
    catch ( IOException e ) {
      System.out.println( "I/O fejl ved læsning fra PushbackInputStream" );
    }
  }
}

0 1 2 3 4
4 3 2 1 0 5 6 7 8 9

Vi fylder først tallene 0 til 9 i en ByteArrayOutputStream. Dernæst laver vi en PushbackInputStream med denne stream som InputStream og med en bufferen på 10 bytes (skulle være rigeligt). Dernæst læser vi fem bytes fra PushbackInputStreamen og unreader de samme fem tal igen, men i forkert rækkefølge. Vi ser den forkerte rækkefølge af de første fem tal da vi tømmer PushbackOutputStreamen og udskriver den.

 

1.6 Specielle OutputStreams

Ved specielle OutputStreams forstås, analogt til specielle InputStreams, dem der ikke har en korresponderende InputStream.

 

1.6.1 PrintStream

Idéen med PrintStream er at have en række bekvemme print-metoder der kan skrive forskellige typer som almindelig tekst i form af platformafhængige bytes (typisk en ASCII-variant).
De velkendte System.out.og System.in er instanser af PrintStream og har som bekendt en lang række metoder ved navn print og println.
Metoderne er her vist for print, men findes naturligvis fuldstændig tilsvarende for println:
void print( boolean b )
void print( char c )
void print( char[] s )
void print( double d )
void print( float f )
void print( int i )
void print( long l )
void print( Object obj )
void print( String s )
Ud over disse har println også en parameterløs udgange til linieskift, der derfor ikke findes for print.
En speciel ting ved PrintStream er at den aldrig vil kaste en exception. I stedet sætter den et internt flag der indikerer om der er sket en fejl. Man kan aflæse dette flag med
boolean checkError()
der samtidig flusher.
Flushing er ikke unødvendig, da PrintStream er bufferet. Man kan sætte PrintStream til automatisk at flushe (hvilket ere gjort for System.out og System.err) når man har skrevet et array af chars eller lavet et linieskift. Dette gøres ved instantieringen
PrintStream( OutputStream out )
PrintStream( OutputStream out, int size ) 
hvor man anvender den sidste af disse konstruktorer med true som anden parameter.
[Det er min erfaring at System.out flusher for hvert eneste tegn - jeg er ikke klar over hvorfor!]
Da System.out er så ofte anvendt, skal jeg ikke trætte med et eksempel på PrintStream.

 

1.7 Oversigt

 

1.7.1 InputStreams

Stream Mærker Datakilde JDK
ByteArrayInputStream
Ja
byte[]
1.0
FileInputStream
Nej
file
1.0
FilterInputStream
-
InputStream
1.0
   DataInputStream
-
InputStream
1.0
   PushbackInputStream
Ja
InputStream
1.0
BufferedInputStream
Nej
InputStream
1.0
ObjectInputStream
Nej
InputStream
1.1
PipedInputStream
Nej
PipedOutputStream
1.0
SequenceInputStream
Ja
InputStreams
1.0

 

1.7.2 OutputStreams

Stream Buffer Destination JDK
ByteArrayOutputStream
Nej
byte[]
1.0
FileOutputStream
Nej
file
1.0
FilterOutputStream
-
OutputStream
1.0
   DataOutputStream
-
OutputStream
1.0
   PrintStream
Ja
OutputStream
1.0
BufferedOutputStream
Ja
OutputStream
1.0
ObjectOutputStream
-
OutputStream
1.1
PipedOutputStream
Nej
PipedInputStream
1.0

 

2. Reader og Writers

 

1.X Oversigt

 

1.X.1 Readers

Reader Mærker Datakilde JDK
CharArrayReader
Ja
char[]
1.1
StringReader
Ja
String
1.1
InputStreamReader
Nej
InputStream
1.1
   FileReader
Nej
file
1.1
BufferedReader
Ja
Reader
1.1
   LineNumberReader
Nej
Reader
1.1
FilterReader
-
Reader
1.1
   PushbackReader
Nej
Reader
1.1
PipedReader
Nej
PipedWriter
1.1

 

1.X.1 Writers

Reader Buffer Datakilde JDK
CharArrayWriter
Nej
char[]
1.1
StringWriter
Nej
String
1.1
OutputStreamWriter
-
OutputStream
1.1
   FileWriter
Nej
file
1.1
BufferedWriter
Ja
Writer
1.1
FilterWriter
-
Writer
1.1
PipedWriter
Nej
PipedReader
1.1
PrintWriter
-
Writer/OutputStream
1.1

 

3. StreamTokenizer

StreamTokenizer læser tekst fra en Reader og inddeler det i mindre tekststykker, kaldet tokens. Inspirationen til StreamTokenizers funktionalitet skal man søge i parsning af source-filer i forbindelse med compilering. En StreamTokenizer løser mange af de traktiske problemer i forbindelse med leksikografisk analyse, idet den gør det bekvemt at indlæse en kildetekst i syntaktiske entiteter eller tokens.

Lad os først se et eksempel på anvendelsen af en StreamTokenizer, for at få et indtryk af hvad den mere præcist gør:

import java.io.*;

class TestStreamTokenizer {

  /* En C-kommentar */

  public static void main( String[] argv ) {

    int x=3; // blot for at få et tal med, og samtidig en C++ kommentar

    try {
      StreamTokenizer st =
        new StreamTokenizer(
          new FileReader( "TestStreamTokenizer.java" ) );

      while ( st.nextToken() != StreamTokenizer.TT_EOF ) {
        if ( st.ttype == StreamTokenizer.TT_WORD )
          System.out.println( "Ord: " + st.sval );
        else if ( st.ttype == StreamTokenizer.TT_NUMBER )
          System.out.println( "Tal: " + st.nval );
        else if ( st.ttype == StreamTokenizer.TT_EOL )
          System.out.println( "Linieskift" ); 
      }
    }
    catch ( FileNotFoundException e ) {
      System.out.println( "Source-filen mangler" );
    }
    catch ( IOException e ) {
      System.out.println( "Fejl ved læsning fra source-filen" );
    }
  }
}

Ord: import
Ord: java.io.
Ord: class
Ord: TestStreamTokenizer
Ord: public
Ord: static
Ord: void
Ord: main
Ord: String
Ord: argv
Ord: int
Ord: x
Tal: 3.0
Ord: try
Ord: StreamTokenizer
Ord: st
...

I eksemplet er der indsat lidt ekstra fyld for at illustrere flere egenskaber ved StreamTokenizeren. Som man ser ignorerer den i default-indstillingen alle specielle tegn (pånær punktum) og indlæser kun det man almindeligvis kan betegne som "ord".

Ved instantieringen angiver vi en Reader som bliver datakilden. StreamTokenizer har kun denne ene konstruktor:

StreamTokenizer( Reader r ) 

Eftersom StreamTokenizer ser på datakilden som værende en sekvens af tegn, er det kun naturligt at man ikke kan anføre en InputStream som parameter til konstruktoren (der findes dog en deprecated konstruktor, hvor man netop kan gøre dette).

Dernæst følger en while-løkke der kører så længe vi ikke har nået slutningen af datakilden.

int nextToken() 

Metoden nextToken returnerer ikke det næste token. Den flytter os frem til det næste token, og returnerer hvilken type det har. Tokens kan være en af to typer: tekst (TT_WORD) eller tal (TT_NUMBER). Dette er interger konstanter i klassen. Der findes to andre konstanter til at indikere hvad man kunne kalde specielle "tokens": end of file (TT_EOF) og end of line (TT_EOL). Den første af disse bliver netop brugt i eksemplets kørselsbetingelsen. De tre andre bliver brugt inde i løkkens if-sætning der ved udskrift beskriver de forskellige tokens efterhånden som datakilden itereres.

Adgangen til en indikation af tokentype, samt selve token sker vi public instans-variable. Det er rædselsfuldt grimt, set med objektorienterede øjne, men Gosling, der har lavet StreamTokenizer, vil sikkert forsvare sig med at det virker; hvilket er en ringe trøst. Det er er følgende tre public instans-variable:

int    ttype
String sval
double nval 

ttype indeholder typeangivelsen for det aktuelle token. Såfremt det er en tekst vil token befinde sig i sval, modsat hvis det er et tal, hvor det vil befinde sig i sval.

StreamTokenizers metoder kan inddeles i to grupper: Indstillings-metoder og tre andre metoder. Lad os først gøre de tre andre metoder færdige, og dernæst se på indstillings-mulighederne.

Af de tre har vi allerede set den ene, nemlig nextToken.

Den næste er:

void pushBack() 

StreamTokenizer har altså pushback egenskaber, ligesom PushbackStream og PushbackReader. For StreamTokenizer er det dog lidt mere beskedent, man kan kun undo læsningen af det sidste token. Det betyder at det næste kan af nextToken på sin vis vil være virkningsløst, idet der efter kaldet vil stå nøjagtig det samme i ttype, sval og nval som før kaldet. I forbindelse med syntaktisk analyse er det meget nyttigt da kan på den måde kan kigge et token frem og se om der er tale om en syntaktisk delimiter.

Den tredie metode er:

int lineno() 

Igen en metode der retter sig mod syntaktisk analyse, idet en fejl kan meddeles på skærmen med linie-nummer, idet man til enhver tid kan kalde denne metode for at få linie-nummeret svarende til det aktuelle token.

 

3.1 Indstillinger

Der findes mange metoder (12 stk.) til at indstille en StreamTokenizer. Den normale anvendelse af en StreamTokenizer er at man efter instantieringen foretager en række kald der indstiller den syntaktiske opsætning, hvorefter man lave en iteration over datakilden indtil den er udtømt.

 

3.1.1 Kommentarer

De to kommentarer der optrådte i eksemplet blev ignoreres af StreamTokenizeren. De repræsenterer to forskellige former for kommentarer. Den første slags med /*...*/ betegnes i denne forbindelse som C-kommentarer, fordi det er den eneste type der findes i C. Den anden type med // betegnes tilsvarende som C++-kommenaterer, fordi de i modsætning til C findes i C++. Betegnelser er mere populære end retvisende, da "C-kommentarer" også findes i C++, og de begge fandte i forgængeren til C.

Default er, at både C og C++ kommentarer ignoreres af StreamTokenizer. Man kan ændre på dette med følgende metoder:

void slashSlashComments( boolean b )
void slashStarComments( boolean b ) 

Parameteren indikerer om den pågældende type kommentarer ignoreres (true) eller regnes som almindelig tekst på lige fod med alt andet (false). Metode-navnene kræver måske en forklaring på dansk. Slash er betegnelsen for skråstreg, mens star naturligvis er * (også kaldet asterix).

Metoden

void commentChar( int c ) 

bruges til at anføre specielle kommentarer. Det tegn man anfører som parameter til metoden bliver regnet som starten på en til end of line kommentar, svarende til //. Det bruges nogen gange i forbindelse med visse assemblere, der anvender semikolon som sådan et tegn, hvor alt efter semikolon på en linie ignoreres.

 

3.1.2 Delimiters

Med metoden

void whitespacesChars( int start, int end ) 

kan man angive hvilke tegn der skal regnes som whitespaces og dermed som delimiters mellem tokens. Bemærk at dette er ASCII-orienterede værdier, ikke UniCode værdier; low og high skal ligge i intervallet [0:255]. Der er tale om en tilføjelse af whitespaces. Tegn der tidligere har været angivet som whitespaces vedbliver med at være det, også efter denne metode er kaldt.

Default whitespaces er [0:32], hvor 32 svarer til mellemrum. Dette interval indeholder alle kontrol-tegnene, dvs. tabulering, linieskift osv.

 

3.1.3 Tokens

Ovenfor så vi hvorledes man kunne anføre delimiters, modsat kan man anføre at tegn er almindelige og derfor kan indgå i tokens. Dette gøres med en af følgende metoder:

void wordChars( int start, int end ) 

Her vil tegne i intervallet [start:end] blive regnet som tegn der indgår i et ord. Default for ord-tegn er: 'a' til 'z', 'A' til 'Z' og intervallet [160:255]. [punktum er ud fra eksempelt også et word-tegn men det ligger da ikke i det øverste interval? eller gør det? Jeg skal have checket UniCode!]

Visse tegn betegnes som almindelige (eng.: ordinary). De regnes som tokens i sig selv, og nextToken sætter ttype til deres værdi, når den støder på den. Mao., de er enkelt-tegns-tokens. Man kan angive at tegn skal regnes som sådanne enkelt-tegns-tokens med en af følgende to metoder:

void ordinaryChar( int c )
void ordinaryChars( int start, int end ) 

Der er ingen default enkelt-tegns-tokens.

Skal linieskift regnes som et specielt token eller skal den ignoreres? det kan man bestemme med metoden

void eolIsSignificant( boolean b ) 

true betyder at linieskift regnes som et token, false ej.

Man kan i visse tilfælde ønske at man kan behandle hele sætninger som tokens. At man kan indramme dem i f.eks. anførselstegn. Man kan angive, at man ønsker et tegn behandlet som et sådant citat-tegn med metoden

void quoteChar( int c ) 
Når et token som står mellem sådanne citat-tegn læses, returnerer nextToken tegnet, f.eks. et anførselstegn, og man kan dernæst hente teksten i sval.

Default er anførselstegn " og enkelt-apostrof ', der begge er citat-tegn.

Når ord behandles som tokens har man mulighed for at dem automatisk konverteret til små bogstaver (lowercase) med metoden:

void lowerCaseMode( boolean b ) 

Skal cifre behandles som tal eller er det bare tegn som alle andre? Det bestemmer man med metoden

void parseNumbers() 

der indstiller StreamTokenizeren så den behandler cifre som tal. Default er at cifre behandles som tal, og den eneste mulighed man har for at ændre på dette er ved at anvende følgende metode, der fjerner enhver for syntaks-indstilling i StreamTokenizeren:

void resetSyntax() 

Efter at have kaldt denne metode regnes alle tegn for ordinary, dvs. at nextToken altid returnerer det næste tegn.

 

Opgaver

1 Lav to klasser VectorInputStream og VectorOutputStream, der nedarver fra henholdsvis Input- og OutputStream. Implementer dem således at datakilden er en Vector. VectorInputStream skal have en passende konstruktor, og VectorOutputStream skal have en get-metode, så man kan arbejde videre med Vector'en efter at skrivning til VectorOutputStream er afsluttet.

 

Vejledende løsning til opgaverne

1
...