Att läsa och skriva i ett program

Sven-Olof Nyström
OOP med Java våren -25
Informationsteknologi
Uppsala Universitet

Skansholm: Kapitel 16

Mål med detta avsnitt.

Du hittar alla programexempel här.

En liten önskelista

Jag listar några saker som man vill kunna göra med strömmar och IO.

Vi vill

Vi vill också

Javas lösning

Så, man vill kunna läsa och skriva alla tänkbara olika typer av data. Dessutom vill man ha olika destinationer för läsning och skrivning: filer, terminal, interna datastrukturer, andra datorer på internet.

Javas lösning är en liten bygglåda för IO där man själv plockar ihop de bitar man behöver. Nackdelen är uppenbar; även ganska enkla saker blir krångliga att göra i Java (att läsa input från användaren till exempel). Fördelen är att man kan återanvända funktionaliteten i andra sammanhang. Man kan också ta fram egna komponenter som kan kombineras med de befintliga bitarna i bygglådan.

Strategin för att kombinera objekt på olika sätt beroende på vilken funktionalitet man vill ha kallas decorator pattern. Googla gärna.

Enkelt exempel

import java.io.*;
public class IO1 {
  public static void main(String[] args)
  throws IOException {
    String s = "";
    InputStream istream = System.in;
    Reader r = new InputStreamReader(istream);
    BufferedReader br = new BufferedReader(r);
    s = br.readLine();
    System.out.println("You typed: "+s);
  }
}

System.in är det som i andra sammanhang kallas standard input; den ström av tecken som normalt ges av användaren.

System.in är ett objekt av klassen InputStream. InputStream representerar en ström av bytes.

Reader är en ström av Char (Unicode). Vi konverterar en ström av bytes till en ström av unicode med konstruktorn InputStreamReader.

(Om vi ska vara lite mer noggranna: Vi skapar ett objekt av klassen InputStreamReader som procucerar Char genom att läsa och konvertera bytes.)

Om man vill läsa en hel rad måste man använda en buffrad läsare. På nästa rad konverterar vi vår Reader till en buffrad läsare.

Till sist läser vi en rad från användaren och skriver ut den.

Körning

$ javac IO1.java
$ java IO1
kaka
You typed: kaka
$

Läs från fil

public class LasFil1 {

    public static void main(String [] arg)
           throws IOException {
	InputStream is = new FileInputStream(arg[0]);
	OutputStream os = System.out;

	int i = 0;
	while ((i = is.read()) != -1) {
	    os.write(i);
	}
	is.close();
        os.close();
    }
}

Här läser vi från en fil, med klassen FileInputStream. Filnamnet ges som argument på kommandoraden. Utskrift sker till standard output, System.out.

Kopiera en fil

Här kopierar vi en fil. Namnen på den existerande filen och den nya filen ges på kommandoraden.

public class Copy {

    public static void main(String [] arg)
           throws IOException {
	InputStream is = new FileInputStream(arg[0]);
	OutputStream os = new FileOutputStream(arg[1]);

	int i = 0;
	while ((i = is.read()) != -1) {
	    os.write(i);
	}
	is.close();
	os.close();
    }
}

Notera att FileInputStream och FileOutputStream är subklasser till InputStream och OutputStream.

De fyra grundläggande klasserna för IO-strömmar

Javas standardbibliotek definierar fyra abstrakta klasser som representerar olika typer av strömmar.

Vi har två strömmar för läsning och skrivning av bytes:

samt två strömmar för läsning och skrivning av char (Unicode):

De abstrakta klasserna ingår förstås i Javas api: https://docs.oracle.com/en/java/javase/17/docs/api/index.html

Klasserna definierar operationer för läsning och skrivning (naturligtvis).

Andra ström-klasser ärver någon av dessa klasser.

Olika typer av strömmar

Javas standardbibliotek definierar konkreta klasser som representerar olika typer av strömmar. Vi har till exempel

Läsa och skriva mot byte-arrayer

Vid testning kan det vara praktiskt att styra output till en array av byte. På samma sätt kan man även låta ett program läsa från en array av byte.

Följande program visar hur en array av byte kan användas som en ström, både vid läsning och vid skrivning.

Skriva och läsa binärt data

Här läser och skriver vi binärt data som en ström av bytes. En int är fyra byte, tex.

Filter (Reader&Writer)

I Java skapar man ofta strömmar från andra strömmar.

Exempel på konstruktorer som tar andra strömmar som argument.

Filter (InputStream&OutputStream)

Definiera egna filter

Man kan också skapa egna filter.

Exempel: FilterReader

Det är en abstrakt klass som ärver klassen Reader. Den har en konstruktor, som tar en Reader som argument. Den har också en "protected" instansvariabel in (den ström som filtreras). För övrigt har den samma metoder som Reader.

För att konvertera från stora till små bokstäver räcker det om man definierar ett filter som överskuggar (overrides) metoderna för läsning.

Exempel: ToLowerCaseReader.java, FilterDemo.java

Generellt

Om vi har ett antal filter kan de kombineras enligt våra önskemål.

Egna filter kan kombineras med existerande,

Kan samma idé tillämpas i andra sammanhang?

Serialisering

Klasserna ObjectInputStream och ObjectOutputStream kan användas för att läsa och skriva (nästan) vilka objekt som helst.

Vi har följande operationer:

readObject och writeObject kräver att objektet tillhör en klass som implementerar interfacet Serializable.

Om in och out är av klasserna InputStream och OutputStream, skriv

ObjectInputStream sin = new ObjectInputStream(in);
ObjectOutputStream sout = new ObjectOutputStream(out);

för att skapa strömmar som kan läsa och skriva objekt.

Programmen nedan testar att skapa ett objekt av klassen Person, skriver det till fil, och läser sen tillbaka det.

SeriWrite.java, SeriRead.java