Innholdsoversikt for programutvikling
try
med ressurserFile
-klassenJFileChooser
for filvalgPrintWriter
File
-klassenProgrammer som foretar filbehandling, det vil si oppretter filer, endrer filer, sletter filer, etc., må lages som applikasjoner, ikke som appletprogrammer. Av sikkerhetsmessige grunner er det nemlig sterkt begrenset hva som er tillatt å gjøre av filbehandling direkte via appletprogrammer.
Java håndterer alle typer input og output på essensielt samme måte: En kilde for input eller et mål for output kalles en strøm (engelsk: stream). En strøm er en ordnet sekvens av data.
Det finnes ingen input- eller output-instruksjoner som tilhører selve
java-språket. All input/output foretas ved hjelp av forhåndsdefinerte
klassebiblioteker som følger med javas programmeringsomgivelser. De fleste av
klassene for I/O er definert i pakkene java.io
og java.nio
.
Vi har imidlertid her valgt å konsentrere oss om pakken java.io
.
Informasjon kan vi dele inn i to hovedtyper: tekstlig informasjon (tegn som
kan leses) og binær informasjon (informasjon om bilder, lyd, etc.).
Grunnenheten for tekstlig informasjon er et enkelttegn. Java representerer
slike i 16-biters Unicode-tegn. For binære data brukes 1 byte (8 biter) som
grunnenhet. På grunn av denne forskjellen er det i java definert to
hovedtyper av strømmer: byte-strømmer og tegn-strømmer. Byte-strømmene
kalles input-strømmer og output-strømmer. De grunnleggende klassene for å
skrive en bytefil og lese en bytefil heter
FileOutputStream
og FileInputStream
. Filnavnet
(eller eventuelt filstien) kan brukes som konstruktørparameter.
Tegnstrømmene kalles på engelsk
readers og writers, altså lesere og skrivere på norsk. De grunnleggende klassene for å
skrive en tegnfil og lese en tegnfil heter
FileWriter
og FileReader
. Filnavnet
(eller eventuelt filstien) kan brukes som konstruktørparameter.
Begge hovedtypene av
strømmer har stort sett det samme settet av metoder. Forskjellen er altså at
det for byte-strømmer leses eller skrives byter, mens det for tegn-strømmene
leses eller skrives 16-biters tegn.
Merknad Egentlig er det slik at det som skrives til fil og leses fra
fil er byter også i tilfelle tegnstrømmer.
Leserne og skriverne er filtre som konverterer
mellom byter og Unicode-tegn. Klassene FileReader
og
FileWriter
er det vi kan kalle bekvemmelighetsklasser som er
definert fordi det er så vanlig å knytte en leser eller en skriver til en fil.
For eksempel er instruksjonen
FileWriter ut = new FileWriter( "tekstfil.txt" );
ekvivalent med
OutputStreamWriter ut = new OutputStreamWriter( new FileOutputStream( "tekstfil.txt" ) );
OutputStreamWriter
-klassen konverterer en strøm av
Unicode-tegn over til en byte-strøm med en bestemt tegnkoding (bestemt av
hvilken koding som blir brukt av systemet som programmet blir kjørt på).
Det er byte-strømmen som skrives til fila.
Tilsvarende så konverterer klassen InputStreamReader
en
byte-strøm med en bestemt tegnkoding over til en strøm av Unicode-tegn.
Instruksjonen
FileReader inn = new FileReader( "tekstfil.txt" );
er ekvivalent med
InputStreamReader inn = new InputStreamReader( new FileInputStream( "tekstfil.txt" ) );
Det går imidlertid an å bruke byte-strømmer også til å lese og skrive tegn. Men da er det bare tegn som tilhører ASCII-tegnsettet (8-biters-tegn) som kan behandles riktig. I java blir det derfor anbefalt at en bruker lesere og skrivere for behandling av tekstinformasjon. For behandling av binære data (bilder, lyd, etc.) må en selvsagt bruke byte-strømmer.
Ved filbehandling kan det oppstå forskjellige typer av unntakssituasjoner.
Systemet finner for eksempel ikke en fil som skal åpnes. Eller det klarer ikke
å lese ifra fila fordi den er skadet. Ved utskrift til fil kan det også gå
galt, for eksempel fordi disken det skal skrives til er skadet eller full. Programmer som
foretar filbehandling bør derfor kunne fange opp eventuelle unntakssituasjoner
som kan oppstå, ved at det legges inn passende
try-catch
-instruksjoner.
Dersom dette ikke gjøres, må en legge inn nødvendige
throws
-setninger
for å få kompilert programmet, siden de unntakssituasjonene som kan oppstå er sjekkede unntak.
Vi skal ta for oss et program som leser en tekstfil samtidig som det skriver ut innholdet i et vindu. Brukeren skal kunne velge hvilken fil som skal leses og vises. Vi skal ta for oss tre forskjellige versjoner av programmet:
Det som skiller de tre versjonene, er hvilken type strøm som brukes for
lesing. Dessuten vil mulige unntakssituasjoner som kan oppstå under fillesingen
bli behandlet på litt forskjellig måte av de tre programmene.
Det tredje programmet vil dessuten vise hvordan vi kan bruke javas
filvelgingsboks JFileChooser
for å velge hvilken fil som
skal leses.
Metoden visFil
som er hentet fra programmet
VisTegnFil.java
leser tegn for tegn
fra den fil som metodeparameteren angir. Hvert tegn blir tilføyd til
tekstområdet output
i programvinduet.
public void visFil( String filnavn ) throws IOException { FileReader inntekst; //tekstfila som skal leses try { //åpner fila som skal leses inntekst = new FileReader( filnavn ); } catch( FileNotFoundException e ) { output.setText( "Finner ikke fil " + filnavn ); return; } output.setText( "Innhold i fil " + filnavn + "\n" ); //leser tegn inntil filslutt int i; do { i = inntekst.read(); //leser et tegn if ( i != -1 ) //-1 betyr filslutt output.append( String.valueOf( (char) i ) ); } while ( i != -1 ); //Alt er lest. Lukker fila. inntekst.close(); //Plasserer tekstmarkøren forrest i tekstområdet: output.setCaretPosition( 0 ); }
Legg spesielt merke til read
-metoden som leser ett tegn fra
fila. Den returnerer ikke dette i form av et tegn, det vil si en
char
-verdi, men i form av en
int
-verdi, nærmere
bestemt Unicode-koden for vedkommende tegn. (Som kjent består en
int
-verdi av fire byter. Det er de to laveste bytene som inneholder
Unicode-verdiene, mens de to øverste bytene bare består av nuller.)
Den returnerte int
-verdien må
programmet konvertere til det tegnet som verdien er kode for, og dette tegnet
må videre konverteres til den strengen som består av nettopp dette tegnet, for
append
-metoden krever en String
-parameter.
Denne doble konverteringen blir gjort ved instruksjonen
String.valueOf( (char) i )
Alternativt kunne vi isteden foretatt konverteringen ved instruksjonen
"" + (char) i
Når en er ferdig med å lese eller skrive en fil, er det viktig å huske på å lukke den. Dette er særlig viktig når en skriver en fil, ellers kan en risikere at data går tapt. For øvrig er det slik at åpne filer krever systemressurser. Derfor bør en ikke holde åpne andre filer enn dem en har bruk for.
Legg for øvrig merke til at lesemetoden fanger opp mulig
FileNotFoundException
i tilfelle programmet ikke finner den fila
vi ønsker å lese. Men derimot så fanger metoden ikke opp mulig
IOException
som kan oppstå både mens fila blir lest og når
programmet prøver å lukke den. På grunn av dette må vi signalisere at
metoden kan kaste ut et slikt unntaksobjekt ved at vi tilføyer
throws IOException
i metodedefinisjonen. Siden unntaksobjekt av denne typen kan bli kastet ut av metoden, må vi
fange det opp på det stedet metoden blir kalt opp. Det er i lytteobjektets
actionPerformed
-metode:
public void actionPerformed( ActionEvent e ) { try { String fil = input.getText(); visFil( fil ); } catch ( IOException ex ) { //kan oppstå ved lesing fra fil eller lukking av fil output.append( "Problem med lesing fra fil." ); } }
Merknad Slik programmet er skrevet, er det meningen at brukeren skal skrive filnavn eller filsti i tekstfeltet for valg av fil. Dersom det bare skrives filnavn, vil virkemåten, det vil si hvor det vil bli lett etter fila, være systemavhengig og avhengig av hvilket verktøy som blir brukt for å kjøre programmet. Det vil dessuten være avhengig av om programmet inngår i en navngitt java-pakke eller ikke. Dersom en skriver fullstendig filsti (fra rotnivå), så skal programmet finne fila. Men husk at java krever vanlige skråstreker som katalogskille, ikke bakoverskråstreker!
Bildet under viser resultatet av å kjøre programmet i NetBeans og bruk av
default-pakken. Kildefila for programmet, som er blitt lest, ligger da i
underkatalogen src
til den katalogen som har samme navn som
det NetBeans-prosjektet som programmet tilhører.
try
med ressurserSom nevnt ovenfor, er det viktig at filer blir lukket. Dette for å hindre at data
går tapt, og dessuten for ikke å bruke unødvendige systemressurser.
Ser vi på lesemetoden til forrige program, så inneholder den instruksjon
for å lukke fila, men dersom det oppstår noe problem under lesingen av fila,
så vil metoden kaste ut en IOException
og avslutte uten at
lukkeinstruksjonen blir nådd.
Fra og med javaversjon 7 er det kommet en ny
type try
-instruksjon, kalt
try
-med-ressurser, som
sikrer at filer blir lukket uten at vi trenger å legge inn noen lukkeinstruksjon.
Denne nye try
-instruksjonen
skal vi bruke heretter, og første eksempel på det kommer i neste program.
Det som blir kalt en ressurs i denne sammenhengen, er et objekt som må lukkes etter
at programmet er ferdig med å bruke det. Reint konkret så gjelder det alle objekter
som implementerer
interface java.lang.AutoCloseable
,
som inkluderer alle objekter som implementerer
java.io.Closeable
.
I praksis betyr det at vi alltid kan bruke denne typen
try
-instruksjon
i forbindelse med filbehandling.
I forrige program brukte vi følgende instruksjoner for å åpne fila som programmet skulle lese:
FileReader inntekst; try { inntekst = new FileReader( filnavn ); ...
Dersom vi skulle gjort det samme ved bruk av mekanismen
try
-med-ressurser, så hadde
det sett ut på denne måten:
try (FileReader inntekst = new FileReader( filnavn )) { ...
Legg merke til at vi i instruksjonen for å opprette filobjektet,
inne i parentesen bak try
,
ikke avslutter med noe semikolon. Det objektet som blir opprettet vil eksistere
og være tilgjengelig inne i blokka som følger etter
try
.
Ved å gjøre det på denne måten sikrer vi oss altså at fila alltid vil bli lukket,
uansett om try
-instruksjonen
blir fullført på normal måte eller om den blir avbrutt av at det blir
kastet ut en IOException
.
Første eksempel på bruk av dette kommer i neste programeksempel.
Det er tillatt å opprette flere ressurser mellom parentesene til
try
-med-ressurser-instruksjonen.
Det kan for eksempel være aktuelt å åpne én fil som det skal leses ifra,
og en annen fil som det skal skrives til.
Vi plasserer i så fall semikolon mellom hver av ressursene, men altså ikke noe semikolon
til slutt i parentesen.
En try
-med-ressurser-instruksjon kan ha
catch
- og
finally
-blokker på samme måte
som en ordinær try
-instruksjon.
I en try
-med-ressurser-instruksjon vil
eventuelle catch
- og
finally
-blokker bli utført
etter at de ressursene som er blitt deklarert er blitt lukket.
FileReader
-klassens read
-metode leser et tegn fra fila
som FileReader
-objektet er knyttet til (via konstruktør-parameter).
Lesing fra en fil er imidlertid en tidkrevende operasjon. Bedre effektivitet
oppnår vi dersom hver leseoperasjon på fila leser en passende "pakke" med
tegn og lagrer disse i et buffer (mellomlager) i maskinens memory, slik at
programmets leseinstruksjoner foretar innlesing fra dette bufferet. Når
bufferet er tomt, foretas en ny innlesing til dette ifra selve fila, og så
videre. Denne type innlesing får vi til ved å "pakke inn"
FileReader
-objektet i et BufferedReader
-objekt på
denne måten:
try (BufferedReader innfil = new BufferedReader( new FileReader( < filnavn > ))) { int i = innfil.read(); //leser ett tegn fra bufferet ...
Her ble det dessuten brukt instruksjon av type
try
-med-ressurser.
BufferedReader
-objekter har også evne til å gjenkjenne
linjeslutt-tegn. De har derfor en readLine
-metode som ved kall
leser den neste tekstlinja fra bufferet og returnerer den i form av et
String
-objekt. Linjeslutt-tegnet blir imidlertid ikke
inkludert i dette String
-objektet. Ved filslutt returnerer
readLine
-metoden null
.
Som medspiller til
BufferedReader
-klassen har vi søsterklassen
BufferedWriter
. Den har en metode newLine()
.
Denne bør brukes for utskrift av linjesluttmerker til fila. En er da sikret
å få filer som virker riktig på alle plattformer.
Metoden
visFil
nedenfor er en modifikasjon av metoden med
samme navn i foregående program. Det blir nå brukt instruksjon av type
try
-med-ressurser.
Den nye versjonen av metoden leser dessuten fila linje for linje.
(Egentlig leses det fra bufferet. Hver gang dette blir tomt, vil det
automatisk bli foretatt en ny innlesing til det fra fila.)
Metoden tilføyer også linje for linje i tekstområdet output
. Legg
merke til at vi da selv må tilføye linjeskift-tegn, siden disse ikke er
inkludert i tekstlinjene som leses fra fila. Metoden er hentet fra
programmet
VisTekstfil.java
.
31 public void visFil( String filnavn ) throws IOException 32 { 33 //Åpner tekstfila som skal leses 34 try (BufferedReader inntekst = 35 new BufferedReader( new FileReader( filnavn ))) 36 { 37 output.setText( "Innhold i fil " + filnavn + "\n" ); 38 //leser linjer inntil filslutt 39 String innlinje = null; 40 do 41 { 42 innlinje = inntekst.readLine(); //leser en linje 43 if ( innlinje != null ) //null betyr filslutt 44 output.append( innlinje + "\n" ); 45 } while ( innlinje != null ); 46 //Alt er lest. Fila blir automatisk lukket. 47 output.setCaretPosition( 0 ); 48 } 49 catch (FileNotFoundException e) 50 { 51 output.setText("Finner ikke fil " + filnavn); 52 } 53 }
Merknad Samme merknad som for foregående eksempel gjelder også for dette programmet. Programvinduet vil også se ut som det gjorde i forrige program.
File
-klassenFile
-objekter brukes ikke i forbindelse med skriving til en
fil eller lesing fra en fil. De inneholder derimot diverse informasjon om en
fil og dens omgivelser, slik som
File
-klassen har derfor et misvisende navn.
File
-objekter representerer ikke filer, men filnavn og deres
omgivelser. Det mest aktuelle i praksis er kanskje å sjekke om en fil med oppgitt
navn eksisterer. Det er nemlig slik at dersom vi åpner en fil ved hjelp av
et FileOutputStream
- eller Writer
-objekt for å
skrive til den, så vil en eventuell eksisterende fil med samme navn bli slettet
(forutsatt selvsagt at den ligger i samme katalog som vi vil skrive ut vår fil til).
Ved først å sjekke om en fil med gitt navn eksisterer, kan vi unngå at data går
tapt uforvarende.
Vi skal se nærmere på bruk av File
-objekter seinere. I denne
omgang skal vi bare ta for oss det som er nødvendig i forbindelse med bruk av
javas dialogvindu JFileChooser
for filvalg.
JFileChooser
for filvalgKlassen JFileChooser
definerer dialogvinduer som kan brukes til å
navigere i filtreet og velge en fil. Det er definert separate dialogvinduer
for åpning av fil (for lesing) og for lagring (skriving) av fil. Når vi skal
åpne et slikt dialogvindu, må vi derfor velge om det skal brukes til å åpne
en fil for lesing, eller til å lagre (skrive) en fil. Et dialogvindu for
åpning av fil kan se ut som vist på følgende bilde.
For å få vist et slikt vindu på skjermen, skriver vi følgende instruksjoner:
JFileChooser filvelger = new JFileChooser(); filvelger.setCurrentDirectory( new File( "." ) ); int resultat = filvelger.showOpenDialog( this );
Som default stiller filvelgeren seg inn på brukerens såkalte hjemmekatalog.
Hvilken dette er, er plattformavhengig og avhengig av hvilke innstillinger
brukeren har gjort. På en Windows-plattform kan for eksempel hjemmekatalogen
oppfattes som katalogen Mine dokumenter
. For å få stilt filvelgeren inn på
katalogen vi er i for øyeblikket, skriver vi, slik det er gjort ovenfor:
filvelger.setCurrentDirectory( new File( "." ) );
"Katalogen som vi er i for øyeblikket", kan imidlertid også tolkes på
forskjellige måter. Dersom vi for eksempel kjører programmet i NetBeans og det
tilhører prosjektet filbehandling
, så tolkes "katalogen som vi er i
for øyeblikket" som katalogen filbehandling
.
Den returnerte int
-verdien (kalt resultat
ovenfor) som vi får når vi har gjort vårt valg i filvelgeren, slik at den har
lukket seg, kan være en av følgende konstanter:
JFileChooser.APPROVE_OPTION //det er klikket på Open-knappen JFileChooser.CANCEL_OPTION //det er klikket på Cancel-knappen JFileChooser.ERROR_OPTION //det er klikket på lukkeknappen //eller har oppstått en feilsituasjon
I tilfelle brukeren har valgt Open, kan vi få tak i valgt fil i form av et
File
-objekt:
File f = filvelger.getSelectedFile();
Husk at File
-objektet ikke representerer en fil, men
inneholder diverse opplysninger om en fil, blant annet dens navn. Navnet
kan vi få tak i ved å skrive
String filnavn = f.getName();
Prøver vi nå å åpne fila ved å bruke dette navnet, forutsetter det at den ligger i samme filkatalog som vi er i. Bedre er det derfor å hente filstien:
String filsti = f.getPath();
Filstien kan vi nå bruke for å åpne fila på vanlig måte.
En filvelger av type JFileChooser
kan for øvrig tilpasses
på mange forskjellige måter. Nærmere opplysninger og eksempler kan man finne
i The Java Tutorials.
Metodene velgFil
og visFil
nedenfor er hentet
fra programmet
VisByteFil.java
. Metoden velgFil
lar brukeren
velge fil ved hjelp av et JFileChooser
-vindu som forklart ovenfor.
Den andre metoden leser den valgte fila byte for byte og viser filinnholdet
i et tekstområde. Siden den valgte fila leses byte for byte, vil det bare være
tegn som tilhører ASCII-tegnsettet som vil bli vist som leselige tegn.
Legg ellers merke til at i denne versjonen av visFil
-metoden så
blir eventuelle unntaksobjekter av type IOException
fanget opp.
Vi trenger derfor ikke å deklarere at metoden kan kaste ut,
throws
, slike objekter,
slik vi måtte i de to foregående programmene. Dermed trenger heller ikke
kallet på metoden ligge inni en
try
-instruksjon.
35 public String velgFil() 36 { 37 JFileChooser filvelger = new JFileChooser(); 38 filvelger.setCurrentDirectory( new File( "." ) ); 39 int resultat = filvelger.showOpenDialog( this ); 40 if ( resultat == JFileChooser.APPROVE_OPTION ) 41 { //bruker har klikket på Open-knappen 42 File fil = filvelger.getSelectedFile(); 43 return fil.getPath(); 44 } 45 else //bruker har klikket på Cancel-knappen eller lukkeknappen 46 return null; 47 } 48 49 public void visFil( String filnavn ) 50 { 51 if ( filnavn == null ) 52 { 53 JOptionPane.showMessageDialog( this, 54 "Du må først velge fil!", "Advarsel", 55 JOptionPane.WARNING_MESSAGE ); 56 return; 57 } 58 59 //åpner tekstfila som skal leses 60 try (FileInputStream innfil = new FileInputStream( filnavn )) 61 { 62 output.setText( "Innhold i fil " + filnavn + "\n" ); 63 //leser byte inntil filslutt 64 int i; 65 do 66 { 67 i = innfil.read(); //leser en byte 68 if ( i != -1 ) //-1 betyr filslutt 69 output.append( String.valueOf( (char) i ) ); 70 } while ( i != -1 ); 71 72 //Alt er lest. Fila vil bli automatisk lukket. 73 output.setCaretPosition( 0 ); 74 } 75 catch(FileNotFoundException nf) 76 { 77 output.setText( "Finner ikke fil " + filnavn ); 78 } 79 catch( IOException e ) 80 { 81 output.setText( "Problem med lesing fra fil " + filnavn ); 82 } 83 }
For å øke effektiviteten ved input og output, bør byte-strømmer bufres, på
tilsvarende måte som tegn-strømmer. Bufringsklassene heter nå
BufferedOutputStream
og BufferedInputStream
.
Klassene inneholder imidlertid ikke, som de tilsvarende klassene for
tegn-strømmer, metoder newLine
og readLine
for
behandling av tekstlinjer. Når en outputstrøm blir lukket, så blir
utskriftsbufferet automatisk flushet, det vil si at dersom det ved lukking er igjen
data i utskriftsbufferet i påvente av at det skal bli fylt opp
før det skjer utskrift til fila, så vil de gjenværende data automatisk bli
skrevet ut til fila før den blir lukket.
Som et lite eksempel på bruk av bufringsklassene for byte-strømmer tar vi for oss
et program som leser fila farrago.txt
byte for byte og kopierer den byte for byte til en ny fil. Det forutsettes
at fila farrago.txt ligger i samme filkatalog som programmets java-fil.
Brukeren får velge navn på den nye fila ved hjelp av et
JFileChooser
-vindu for lagring av fil. Dette vinduet ser ut som
vist på følgende bilde.
Når det i dette vinduet er skrevet et navn for fila som skal lagres (og
klikket Save eller trykket returtast), blir det sjekket om det
allerede eksisterer en fil med det valgte navn. I så fall må brukeren velge
et nytt filnavn. Både lesing av eksisterende fil og skriving av kopifil
gjøres bufret. Programmet, som ligger i fila
Bytebufring.java
,
er gjengitt nedenfor. Legg merke til at tegnene både leses fra fil og skrives til ny
fil i form av int
-verdier! Legg også merke til at det i
try
-med-ressurser-instruksjonen
til dette programmet blir opprettet to ressurser, fila det skal leses fra og
fila det skal skrives til. De to ressursene er atskilt med semikolon.
1 import java.io.*; 2 import javax.swing.*; 3 4 public class Bytebufring 5 { 6 public static void main(String[] args) 7 { 8 File f = null; 9 boolean okfil = false; 10 //ber først brukeren velge fil det skal skrives til 11 do 12 { 13 JFileChooser filvelger = new JFileChooser(); 14 filvelger.setCurrentDirectory( new File( "." ) ); 15 int resultat = filvelger.showSaveDialog( null ); 16 if ( resultat == JFileChooser.APPROVE_OPTION ) 17 { 18 f = filvelger.getSelectedFile(); 19 if ( !f.exists() ) 20 okfil = true; 21 else 22 JOptionPane.showMessageDialog( null, 23 "Fila eksisterer allerede!\n" + 24 "Du må velge et annet navn.", 25 "Advarsel", JOptionPane.WARNING_MESSAGE ); 26 } 27 else 28 { 29 JOptionPane.showMessageDialog( null, 30 "Du har ikke valgt utfil!\n" + 31 "Programmet vil bli avsluttet.", 32 "Advarsel", JOptionPane.WARNING_MESSAGE ); 33 System.exit( 0 ); 34 } 35 } while ( !okfil ); 36 //åpner fil det skal leses fra og fil det skal skrives til 37 try (BufferedInputStream in = new BufferedInputStream( 38 new FileInputStream("src/farrago.txt")); 39 BufferedOutputStream out = new BufferedOutputStream( 40 new FileOutputStream(f))) 41 { 42 int c; 43 44 while ((c = in.read()) != -1) 45 out.write(c); 46 } 47 catch ( FileNotFoundException fnfe ) 48 { 49 System.out.println( "Finner ikke fil det skal leses fra." ); 50 } 51 catch ( IOException ioe ) 52 { 53 System.out.println( "Problem med fillesing eller skriving." ); 54 } 55 JOptionPane.showMessageDialog( null, "Du kan nå åpne og lese " + 56 "fila " + f.getName() + " som ble skrevet ut." ); 57 } 58 }
Merknad I programmet ovenfor er
"src/farrago.txt"
brukt som konstruktørparameter ved åpning av fil
for lesing. I tektsten foran programmet er det skrevet at fila må ligge i
samme filkatalog som programmets java-fil. For at dette skal stemme,
må konstruktørparameteren tilpasses det systemet som programmet blir kjørt på.
Slik det står ovenfor, passer det når programmet blir kjørt på NetBeans og
bruk av default-pakken. Programmets javafil ligger da i underkatalogen src
til
prosjektkatalogen. I denne underkatalogen er også fila farrago.txt
lagt inn.
String
-objekter — strukturerte filerHittil har vi bare tatt for oss overføring til og fra fil av
enkelt-byter, 16-biters-tegn og av hele tekstlinjer. Vi har også lært
hvordan vi kan bufre en strøm for å oppnå bedre effektivitet. Bufringen
kan vi oppfatte figurlig på den måten at vi sender den opprinnelige
strømmen gjennom et filter som omformer strømmen. I klassebibliotekets java.io
-pakke
finnes det også klasser som definerer andre typer av filtre som vi kan sende
strømmer igjennom for å omforme dem på forskjellige måter.
En av disse filtertypene konverterer mellom byte-strømmer
og javas primitive datatyper. Slike filtre må vi derfor bruke når vi vil
overføre verdier av de primitive datatypene til og fra fil. De aktuelle
filterklassene heter DataOutputStream
og
DataInputStream
. For at de skal virke som filtre, må vi bruke
dem som "innpakning" rundt henholdsvis FileOutputStream
og
FileInputStream
, på tilsvarende måte som vi gjorde i tilfelle
bufring.
DataOutputStream output = new DataOutputStream( new FileOutputStream( < filnavn > ) );
Dersom vi
i tillegg ønsker bufring, må vi sende strømmen gjennom et bufferfilter
før vi sender den gjennom DataInputStream
- eller
DataOutputStream
-filteret, siden vi ønsker å bruke bufrede
lese- og skrivemetoder fra disse.
DataInputStream innfil = new DataInputStream( new BufferedInputStream( new FileInputStream( < filnavn > ) ) );
Tabellen nedenfor viser oversikt over lese- og skrivemetodene til
DataInputStream
og DataOutputStream
.
Skrivemetodene må som parameter ha den verdi som skal skrives ut, og den må
selvsagt ha riktig datatype i forhold til den metode som brukes. Lesemetodene
er uten parametre og returnerer den innleste verdien.
Merk deg at metodene for å skrive og lese String
-objekter
heter writeUTF
og readUTF
. UTF er forkortelse for
Unicode Text Format og er en bestemt måte å kode tegn på.
Lese | Skrive | Type |
---|---|---|
readBoolean | writeBoolean | boolean |
readChar | writeChar | char |
readByte | writeByte | byte |
readShort | writeShort | short |
readInt | writeInt | int |
readLong | writeLong | long |
readFloat | writeFloat | float |
readDouble | writeDouble | double |
readUTF | writeUTF | String (i UTF-format) |
Når data skrives til fil ved hjelp av et DataOutputStream
-objekt,
vil de forskjellige datatypene bli skrevet til fila i en bestemt rekkefølge,
bestemt av hvilken rekkefølge skriveinstruksjonene blir utført i. Fila får
dermed en bestemt struktur, som i praksis er bestemt av programmereren.
Strukturen kan for eksempel se ut slik, beskrevet ved rekkefølgen til datatypene:
int | String | String | double |
int | String | String | double |
. . . |
Det vil altså (som regel) være bestemte sekvenser av datatyper som gjentar seg. En slik sekvens blir ofte kalt for en "post" på fila. Det ligger ikke mer i dette ordet enn at det er en gruppe av data som logisk hører sammen på en eller annen måte.
Data som skal leses fra fil som en DataInputStream
, må
på forhånd være skrevet til fila som en DataOutputStream
.
Ellers vil ikke kodingen av data være riktig. Dessuten må vi kjenne
filstrukturen, slik at de forskjellige lesemetodene blir kalt opp i riktig
rekkefølge.
I våre tidligere eksempler, der filer er lest tegn for tegn eller byte for
byte, og disse er blitt returnert i form av en int
-verdi, er det
verdien -1 som har indikert filslutt. I tilfelle lesing av tekstfil linje for
linje i form av et String
-objekt, indikerte verdien
null
filslutt. Når vi leser en fil som en
DataInputStream
, kan -1, null
og hvilke som helst
andre dataverdier tenkes å være gyldige data. Filslutt må derfor indikeres
på en annen måte. Det blir gjort ved at det blir kastet ut en
EOFException
. Denne må derfor programmet fange opp.
I DataIOTest.java
som
er gjengitt nedenfor, blir det opprettet en datafil med poster som har denne
struktur:
double | int | String |
Det blir skrevet noen poster av denne type til fila, som så blir lukket. Deretter blir fila åpnet og lest post for post. De leste data blir tilføyd tekst og skrevet ut i et tekstområde.
1 import java.io.*; 2 import javax.swing.*; 3 import java.text.DecimalFormat; 4 5 public class DataIOTest extends JFrame 6 { 7 private JTextArea utskrift; 8 9 public DataIOTest() 10 { 11 super("Test av DataOutputStream og DataInputStream"); 12 utskrift = new JTextArea(10, 40); 13 add(new JScrollPane(utskrift)); 14 pack(); 15 setVisible(true); 16 } 17 18 public void skrivDatafil(String filnavn) 19 { 20 try (DataOutputStream ut = 21 new DataOutputStream(new FileOutputStream(filnavn))) 22 { 23 //data som skal skrives til fil 24 double[] priser = {99.00, 149.90, 75.50, 223.90, 145.90}; 25 int[] enheter = {12, 8, 13, 29, 50}; 26 String[] bok = {"Sandemose: Varulven", 27 "Borgen: Lillelord", 28 "Camus: La peste", 29 "Hugo: Notre-Dame de Paris", 30 "Pasternak: Dr. Zhivago"}; 31 32 //utskrift 33 for ( int i = 0; i < priser.length; i ++ ) 34 { 35 ut.writeDouble( priser[i] ); 36 ut.writeInt( enheter[i] ); 37 ut.writeUTF( bok[i] ); 38 } 39 } 40 catch ( IOException e ) 41 { 42 System.out.println("Filproblem."); 43 } 44 } 45 46 public void visDatafil( String filnavn ) 47 { 48 double pris; 49 int enhet; 50 String bok; 51 double total = 0.0; 52 DecimalFormat formatterer = new DecimalFormat("0.00"); 53 utskrift.setText("Faktura\n"); 54 //leser data inn igjen fra fila og skriver ut i 55 //tekstområde utskrift 56 try (DataInputStream inn = new DataInputStream( 57 new FileInputStream(filnavn))) 58 { 59 while (true) 60 { 61 pris = inn.readDouble(); 62 enhet = inn.readInt(); 63 bok = inn.readUTF(); 64 utskrift.append( "Du har bestilt " + 65 enhet + " enheter av " + 66 bok + " til pris kr. " + 67 formatterer.format( pris ) + "\n" ); 68 total = total + enhet * pris; 69 } 70 } 71 catch (FileNotFoundException fnfe) 72 { 73 System.out.println("Finner ikke fil " + filnavn); 74 return; 75 } 76 catch (EOFException e) 77 { 78 //hele fila er lest, går videre 79 utskrift.append("Totalt: kr. " + formatterer.format(total)); 80 } 81 catch (IOException e) 82 { 83 System.out.println("Problem med lesing fra fil."); 84 } 85 } 86 87 public static void main(String[] args) 88 { 89 DataIOTest tester = new DataIOTest(); 90 tester.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 91 tester.skrivDatafil("faktura1.dta"); 92 tester.visDatafil("faktura1.dta"); 93 } 94 }
En kjøring av programmet ga vinduet som det er bilde av under.
PrintWriter
DataOutputStream
-objekter skriver til fil en binær
representasjon av verdier tilhørende javas primitive datatyper. Slike
filer kan derfor ikke leses som tekstfiler, men må leses ved hjelp av et
DataInputStream
-objekt. Ønsker vi å skrive verdier tilhørende
de primitive datatypene til en tekstfil, må vi bruke et
PrintWriter
-objekt. Det oppretter vi på følgende måte:
PrintWriter utfil = new PrintWriter( new FileWriter( < filnavn > ) );
PrintWriter
'e er alltid bufret.
PrintWriter
-klassen inneholder print
- og
println
-metoder som tar parameter av alle de primitive
datatypene, samt String
. De forskjellige versjonene av
println
-metoden tilføyer alle sammen linjeskifttegn i tillegg
til verdien som er parameter. Dette gjøres på en slik måte at det virker
riktig på alle plattformer når fila skal leses inn igjen. Det er også en
println
-metode uten parameter for avslutning av en linje.
Den resulterende tekstfil kan leses med en hvilken som helst teksteditor.
Dessuten kan den selvsagt leses inn igjen til et javaprogram ved bruk av de
forskjellige typene av Reader
-objekter. Det finnes i java
ingen analog klasse til PrintWriter
for innlesing av data i
tekstlig format. Ved innlesing kan vi lese linje for linje ved hjelp av en
BufferedReader
. Så må vi selv splitte opp teksten ved hjelp av en
Scanner
.
Tidligere ble det til dette formål brukt en StringTokenizer
, og så foretatt
eventuell konvertering til relevant datatype ved hjelp av
Integer.parseInt, Double.parseDouble
etc. Dette blir nå ansett
som en foreldet framgangsmåte. Det er også mulig å knytte en Scanner
direkte
til en fil, uten å gå veien om en BufferedReader
.
En liten introduksjon til bruk av Scanner
'e
kan finnes på følgende link:
Splitte opp input ved hjelp av en Scanner
.
Programmet
PrintWritertest.java
som er gjenngitt nedenfor skriver til tekstfila faktura1.txt
de samme data som ble skrevet
til en binær fil av programmet DataIOTest
som vi tok for oss
ovenfor. Tekstfila kan etterpå leses inn igjen med en teksteditor, for eksempel
med TextPad. Hvor den vil havne hen er imidlertid avhengig av plattform, innstillinger
og av hvilket verktøy som blir brukt til å kjøre programmet. Dersom det kjøres
med NetBeans og defaultpakken, vil fila havne i prosjektkatalogen til vedkommende
NetBeansprosjekt. Programmet avslutter med å vise en meldingsboks om at
data er skrevet ut til tekstfila faktura1.txt.
1 import java.io.*; 2 import javax.swing.*; 3 import java.text.DecimalFormat; 4 5 public class PrintWritertest 6 { 7 public static void main(String[] args) 8 { 9 PrintWritertest tester = new PrintWritertest(); 10 tester.skrivTekstfil( "faktura1.txt" ); 11 JOptionPane.showMessageDialog( null, 12 "Data er skrevet ut til tekstfil faktura1.txt" ); 13 } 14 15 public void skrivTekstfil( String filnavn ) 16 { 17 //data som skal skrives til fil 18 double[] priser = { 99.00, 149.90, 75.50, 223.90, 145.90 }; 19 int[] enheter = { 12, 8, 13, 29, 50 }; 20 String[] bok = {"Sandemose: Varulven", 21 "Borgen: Lillelord", 22 "Camus: La peste", 23 "Hugo: Notre-Dame de Paris", 24 "Pasternak: Dr. Zhivago"}; 25 26 //skriver til fil 27 try (PrintWriter ut = 28 new PrintWriter( new FileWriter( filnavn ) )) 29 { 30 double total = 0.0; 31 DecimalFormat formatterer = new DecimalFormat( "0.00" ); 32 for ( int i = 0; i < priser.length; i ++ ) 33 { 34 ut.print( "Du har bestilt " ); 35 ut.print( enheter[i] ); 36 ut.print( " enheter av " ); 37 ut.print( bok[i] ); 38 ut.print( " til pris kr. " ); 39 ut.println( formatterer.format( priser[i] ) ); 40 total = total + enheter[ i ] * priser[ i ]; 41 } 42 ut.println( "Totalt: kr. " + formatterer.format( total ) ); 43 } 44 catch (IOException e) 45 { 46 System.out.println( "Filproblem." ); 47 } 48 } 49 }
Dersom vi ønsker å lagre objekter på fil, må objektenes datafelter
(instansvariable) skrives til fil i en eller annen bestemt rekkefølge. En
mulighet er å skrive programkode for dette selv og benytte seg av en
DataOutputStream
. Ved innlesing (ved hjelp av en
DataInputStream
) må objektene opprettes igjen
(ved bruk av new
-operatoren)
etter hvert som datafeltene leses
inn. I den tidligste java-versjonen var denne framgangsmåten den eneste
muligheten for input/output av objekter til fil. I de seinere java-versjonene
er det definert byte-strømmer ObjectOutputStream
og
ObjectInputStream
for overføring av objekter til og fra fil.
Disse forutsetter imidlertid at objektene som skal overføres er
serialisert i den forstand at klassene som definerer objektene
implementerer interface Serializable
.
For å implementere dette
interface er det ingen metode som må implementeres, det er nok å skrive
class Eksempel implements Serializable { < klassedefinisjon på vanlig måte > }
der Eksempel
her bare er brukt som navn på vedkommende klasse.
Objekter oppretter vi på vanlig måte:
Eksempel eks = new Eksempel();
For å få skrevet dette objektet til fil, kan vi nå skrive
ObjectOutputStream utfil = new ObjectOutputStream( new FileOutputStream( < filnavn > ) ); utfil.writeObject( eks );
I tillegg må vi selvsagt legge inn passende
try-catch
-instruksjoner for å fange opp eventuelle
IOException
-objekter. Når objektene skal leses inn igjen, er
det nødvendig å foreta typekonvertering til riktig klassetype. Med eksemplet
ovenfor vil det bli slik:
ObjectInputStream innfil = new ObjectInputStream( new FileInputStream( < filnavn > ) ); Eksempel e = (Eksempel) innfil.readObject();
Dersom objektet som skrives ut til fil inneholder peker(e) til andre objekter, vil også disse objektene automatisk bli skrevet til fil (uten at vi har noen ekstra utskriftsinstruksjoner), forutsatt at objektene er serialisert som beskrevet ovenfor. Og ved innlesing vil alt sammen bli lest inn igjen som følge av én eneste leseinstruksjon. Reint teknisk blir dette gjort på den måten at hvert objekt som skrives til fil blir tildelt et bestemt serienummer (derfor betegnelsen serialisering) som skrives til fil sammen med objektets data. Dersom flere objekter har referanser (pekere) til ett og samme objekt, blir dette objektet ikke skrevet til fil flere ganger. Etter at det er skrevet til fil én gang, blir det i tilfelle seinere referanser bare serienummeret som blir skrevet til fil.
Vi skal se på løsningsforslaget til en øvingsoppgave som har vært brukt i kurset Programutvikling.
Oppgaven gikk ut på å implementere en sammenkjedet liste av
hele tall. Det fullstendige programmet finnes i filene
Heltallsnode.java
,
Heltallsliste.java
,
Listevindu.java
og Listetest.java
.
Nedenfor er det hentet ut fra disse filene den koden som inneholder de
nødvendige endringene som må gjøres i programmet for å få lagret lista på fil
når programmet blir avsluttet, og lest denne fila inn igjen når programmet
blir startet opp på nytt. For at vinduets lukkeknapp skal virke på den måten
at programmet foretar lagring på fil før det avslutter, er det nødvendig å
registrere en vinduslytter for vinduet. Hvordan dette kan gjøres, er beskrevet
i notatet Programmering av vinduslytter.
Klassen Heltallsliste
inneholder peker til første node i lista.
Denne er et objekt av type Heltallsnode
. Klassedefinisjonene for
Heltallsliste
og Heltallsnode
er nå endret ved at det bak
klassenavnet er tilføyd
implements Serializable
Ellers er klassene som før. I vindusklassen Listevindu
er det
definert en metode skrivTilFil
som skriver
Heltallsliste
-objektet til fil og en metode lesFil
som
leser Heltallsliste
-objektet inn igjen. Legg merke til at det er nok
med én instruksjon for å skrive hele lista til fil, og nok med
én instruksjon for å lese hele lista inn igjen! Ved kjøring av
programmet vil vi nå se at tallene som er satt inn i lista blir lagret i fila.
Det oppnås ved at skrivTilFil
-metoden blir kalt opp i vinduslytteren før
programmet avsluttes og lesFil
-metoden blir kalt opp av
konstruktøren for vindusklassen Listevindu
. Dersom det ikke finnes
noen fil, eller oppstår noen problemer ved innlesing, blir det opprettet en
tom liste.
1 import java.io.*; 2 3 public class Heltallsnode implements Serializable 4 { 5 private int info; 6 Heltallsnode neste; 7 8 public Heltallsnode( int data ) 9 { 10 info = data; 11 neste = null; 12 } 13 14 public int getInfo() 15 { 16 return info; 17 } 18 19 public void setInfo( int nyVerdi ) 20 { 21 info = nyVerdi; 22 } 23 } // slutt på class Heltallsnode import javax.swing.JTextArea; import java.io.*; class Heltallsliste implements Serializable { private Heltallsnode hode; public Heltallsliste() { hode = null; } /* Samme klasseinnhold som tidligere. */ } } // slutt på class Heltallsliste 1 import javax.swing.*; 2 import java.awt.*; 3 import java.awt.event.*; 4 import java.io.*; 5 6 public class Listevindu extends JFrame 7 { 8 private JTextField input, inputb, inputs, input2, input3; 9 private JTextArea lista, kopien; 10 private JButton dobbel, revkopi, slette, reverser,sorter; 11 private Heltallsliste heltallsliste; 12 private Kommandolytter lytteren; 13 14 public Listevindu() 15 { 16 super("Test av heltallsliste"); 17 lytteren = new Kommandolytter(); /* Samme innhold som tidligere i denne delen av kostruktøren, men uten instruksjon for å opprette heltallsliste. */ 68 lesFil(); 69 skrivListe(); 70 setSize(550, 400); 71 setVisible(true); 72 } 73 74 private void visFeilmelding(String melding) 75 { 76 JOptionPane.showMessageDialog(this, melding, 77 "Problem", JOptionPane.ERROR_MESSAGE); 78 } 79 80 private void lesFil() 81 { 82 try (ObjectInputStream innfil = new ObjectInputStream( 83 new FileInputStream( "src/liste.data" ))) 84 { 85 heltallsliste = (Heltallsliste) innfil.readObject(); 86 } 87 catch(ClassNotFoundException cnfe) 88 { 89 lista.setText(cnfe.getMessage()); 90 lista.append("\nOppretter tom liste.\n"); 91 heltallsliste = new Heltallsliste(); 92 } 93 catch(FileNotFoundException fne) 94 { 95 lista.setText("Finner ikke datafil. Oppretter tom liste.\n"); 96 heltallsliste = new Heltallsliste(); 97 } 98 catch(IOException ioe) 99 { 100 lista.setText("Innlesingsfeil. Oppretter tom liste.\n"); 101 heltallsliste = new Heltallsliste(); 102 } 103 } 104 105 public void skrivTilFil() 106 { 107 try (ObjectOutputStream utfil = new ObjectOutputStream( 108 new FileOutputStream("src/liste.data"))) 109 { 110 utfil.writeObject(heltallsliste); 111 } 112 catch( NotSerializableException nse ) 113 { 114 visFeilmelding("Objektet er ikke serialisert!"); 115 } 116 catch( IOException ioe ) 117 { 118 visFeilmelding("Problem med utskrift til fil."); 119 } 120 } 121 /* Samme innhold som tidligere i resten av klassen. */ 280 } // slutt på class Lisevindu 1 import java.awt.event.*; 2 3 //Testklasse for Heltallsliste-klassen 4 public class Listetest 5 { 6 public static void main(String[] args) 7 { 8 final Listevindu vindu = new Listevindu(); 9 //final for å kunne gjøre aksess på lokal variabel 10 //fra anonym indre klasse 11 vindu.addWindowListener( 12 new WindowAdapter() { 13 public void windowClosing(WindowEvent e) 14 { 15 vindu.skrivTilFil(); 16 System.exit(0); 17 } 18 } ); 19 } 20 }
Arrayer og String
-objekter
I java er verdier av den forhåndsdefinerte datatypen String
, samt
av type array, uansett hvilken type elementer arrayen inneholder,
også objekttyper. Det innebærer at dersom
readObject
-metoden brukes til innlesing av slike, så må det
også for disse foretas eksplisitt typekonvertering til riktig type. Men
når disse typene inngår som datafelter i andre objekter, og disse objektene
er serialisert, er det ikke nødvendig å foreta seg noe spesielt i tilfelle
innlesing.
static
-datafelter
Det er grunn til å merke seg at datafelter som er
static
ikke blir skrevet til fil når det brukes
serialisering. Dersom en ønsker slike verdier skrevet til fil, må det gjøres
i form av separate utskriftsinstruksjoner. Tilsvarende gjelder ved innlesing.
Ved innlesing må en dessuten passe på at innlesingsinstruksjonene kommer i
riktig rekkefølge i forhold til rekkefølgen ved utskrift, slik at datatypene
stemmer.
For øvrig er det slik at man for ObjectOutputStream
og ObjectInputStream
kan bruke de samme skrive- og lesemetodene
som for henholdsvis DataOutputStream
og
DataInputStream
. Det er nemlig slik at disse metodene er
spesifisert i interface
'ene
DataOutput
og DataInput
. Det første av disse
blir implementert av både DataOutputStream
og av
ObjectOutputStream
, mens det siste blir implementert av både
DataInputStream
og av ObjectInputStream
.
En objektfil kan derfor inneholde en blanding av objekter og verdier av
primitiv datatype.
Versjonsnumre
Til hver klasse som er serialisert ved at den implementerer
interface Serializable
, blir det knyttet et versjonsnummer, kalt
serialVersionUID
. Ved lagring på fil blir dette versjonsnummeret
skrevet til fil sammen med andre data for objektet, som del av selve serialiseringen.
Ved deserialisering, det vil si ved typekonvertering
til riktig type etter innlesing av et objekt fra fil, blir det først sjekket om
kjøresystemet har tilgang til en klasse som definerer den typen som man forsøker
å konvertere til. Dersom dette ikke er tilfelle, blir det kastet ut en
ClassNotFoundException
.
Finnes det en slik klasse, blir det sjekket
om den har samme versjonsnummer som det nummeret som ble lest inn fra fil.
Dersom det ikke er tilfelle, blir det kastet ut en
InvalidClassException
.
Dersom det i klassedefinisjonen for den serilaliserte klassen ikke eksplisitt er skrevet et datafelt som har dette mønster:
<aksesstype> static final long serialVersionUID = <long-verdi>;
så vil javas kjøresystem automatisk generere og skrive til fil et slikt versjonsnummer.
Dets verdi blir generert på grunnlag av klassens datafelter og metodesignaturer.
Ved innlesing og deserialisering blir et tilsvarende nummer generert på grunnlag av
klassedefinisjonen for den typen som man prøver å konvertere til. Dersom disse da ikke
er lik hverandre, får vi altså en InvalidClassException
.
Problemet er imidlertid at dersom vi etter at en objektfil er skrevet, for eksempel
har oppgradert til en ny java-versjon eller -kompilator, så kan vi risikere ikke å få
lest objektene inn igjen, selv om vi ikke har foretatt noen endringer i klassene som
definerte objektene den gang de ble skrevet til fil. På grunn av dette blir det nå
sterkt anbefalt at det i alle klasser som implementerer
interface Serializable
eksplisitt blir deklarert et datafelt etter det mønster som er beskrevet ovenfor.
Det blir videre sterkt anbefalt at dette datafeltet blir gitt
private
aksess,
for versjonsnummeret skal bare identifisere denne spesielle klassen. Eventuelle
subklasser må få sitt eget versjonsnummer. Husk ellers på at når vi angir en
long
-verdi, så må vi avslutte
selve tallverdien med bokstaven L
.
Ellers vil tallverdien bli tolket som en int
-verdi.
Det er for øvrig ingen bestemte krav til hvordan verdien til versjonsnummeret skal være.
Men vi må ikke bruke verdien 0L
, for den brukes som versjonsnummer for
klasser som ikke er serialiserte.
Et konkret eksempel
på deklarasjon av versjonsnummer kan derfor se ut slik:
private static final long serialVersionUID = 123L;
For øvrig er det selvsagt viktig at vi bruker versjonsnumre på en konsistent
og konsekvent måte, slik at de virker etter hensikten. For arrayer er det ikke
mulig å deklarere versjonsnumre som beskrevet ovenfor. For dem må vi derfor
bruke de versjonsnumrene som javas kjøresystem selv genererer. Det samme er tilfelle
for String
-objekter.
Dersom vi tidligere har definert en klasse Eksempel
som implementerer
interface Serializable
, men ikke eksplisitt har deklarert noe
versjonsnummer i den, slik det nettopp er beskrevet, så lurer vi kanskje på hvilket
versjonsnummer som javas kjøresystem har generert for denne klassen. Det kan vi få tak
i på følgende måte:
ObjectStreamClass klasse = ObjectStreamClass.lookup(Eksempel.class); long id = klasse.getSerialVersionUID();
Hva om vi ønsker å lese gamle objektfiler til et program med oppdaterte objektdefinisjoner?
Etter hvert som et program blir utviklet, gjør vi ofte modifikasjoner i klassene som definerer
objektene våre: Vi legger til eller fjerner datafelter, endrer datatyper, endrer
metodedefinisjoner, og legger til nye metoder. Dette kan resultere i at vi ikke får lest
inn igjen gamle objektfiler. Men vi har likevel muligheter for å få det til, forutsatt
at vi programmerer riktig. Den første betingelsen er at de nye objektdefinisjonene gir samme
versjonsnumre som de har de gamle objektene som er lagret på fil. Baserer vi oss på automatisk
generering av versjonsnumre, vil dette vanligvis ikke skje. Vi må derfor, iallfall i de oppdaterte
klassedefinisjonene, selv bestemme versjonsnumre slik det er forklart ovenfor. Vi må sørge
for at versjonsnumrene stemmer med de som er lagret på fil. Disse kan vi få tak i som beskrevet ovenfor.
Dersom vi bare har endret på klassens metoder, vil det ikke være noe problem å lese inn igjen
objektdata, forutsatt altså at versjonsnumrene stemmer. Men dersom det er endringer i datafeltene,
kan vi få problemer. Ved innlesing vil datafeltene som leses inn fra fil bli sammenliknet med
dem som finnes i nåværende klassedefinisjon. Dersom to felter har samme navn, men forskjellig
datatype, vil det ikke bli gjort noe forsøk på å konvertere den innleste typen til den nye typen.
Objektene vil da bli betraktet som inkompatible og det oppstår feilsituasjon i samsvar med det.
Dersom innleste objekter har datafelter som ikke finnes i den nåværende klassedefinisjon, så vil
disse kort og godt bli ignorert. Har derimot nåværende klassedefinisjon datafelter som ikke finnes
i objektene som leses inn, så vil verdien til de nye datafeltene bli satt til default startverdi,
det vil si null
for objekter,
tallverdi null for numeriske felter, og false
for logiske felter.
Multiple referanser — single objekter
Du lurer kanskje på hva som skjer dersom to forskjellige objekter som er skrevet til fil
begge inneholder referanse til ett og samme objekt: Vil da de to objektene
fortsatt referere til ett og samme objekt etter at de er lest inn igjen fra fil?
Svaret på det er "ja". En fil kan bare inneholde én kopi av hvert enkelt objekt,
men den kan inneholde et vilkårlig antall referanser til dette objektet.
Dersom man eksplisitt skriver samme objekt til fil to ganger, så vil det bare
være referansen til objektet som blir skrevet til fil to ganger.
Men dersom ett enkelt objekt blir skrevet til to forskjellige filer, så blir
det dublisert. Et program som leser inn igjen begge filene vil se to distinkte
objekter.
De filer vi har tatt for oss hittil har vært såkalte sekvensielle filer. Det innebærer at de ved lesing og skriving alltid må prosesseres fra begynnelsen og videre byte for byte utover i fila, på liknende måte som når et lydbånd spilles av eller tas opp.
Ønsker vi å endre innholdet i en sekvensiell fil, kan det bare skje på følgende måte:
Vi må da huske på at når vi åpner en fil for skriving, så vil en eventuell eksisterende fil med samme navn bli slettet. Den nye fila må derfor i første omgang tildeles et annet navn.
Endring av sekvensielle filer er altså en omstendelig og tidkrevende prosess, iallfall når det er litt størrelse på filene. Sekvensielle filer egner seg derfor dårlig i situasjoner der det er hyppige endringer av filinnholdet. I slike tilfeller er det bedre å bruke filer der det er mulig å gå direkte inn i en gitt posisjon i fila, lese det som står der, og eventuelt oppdatere dataene, uten at resten av fila gjøres noe med. Dette er det mulig å gjøre i filer med såkalt random, eller direkte aksess.
Med en RandomAccessFile
kan vi foreta både input og output på én og samme fil vekselvis
mens fila er åpen. Klassen RandomAccessFile
implementerer både DataOutput
og DataInput
som spesifiserer skrive- og lesemetodene for binær behandling av verdier av javas
primitive datatyper, samt av type String
, se
foran. En fil av denne typen
kan derfor behandle både byter og 16-biters Unicode-tegn.
Klassen er ikke subklasse til verken InputStream, OutputStream,
Reader
eller Writer
. Den er direkte subklasse til klassen
Object
.
Konstruktøren til RandomAccessFile
har en
String
-parameter som indikerer om det skal foretas bare lesing
(parameterverdi "r"
for read),
eller både lesing og skriving (parameterverdi
"rw"
for read og write).
Posisjonen for en skrive- eller leseoperasjon for fil er bestemt av en
såkalt filpeker. Det er en long
-verdi som angir antall byter
fra starten av fila. Den økes automatisk hver gang en skrive- eller
leseoperasjon utføres. For eksempel vil den ved en
readInt
-operasjon økes med 4, siden en
int
-verdi
består av 4 byter.
I en random-aksess-fil, eller direkte-fil som det også kalles,
det vil si en fil av type
RandomAccessFile
, er det, i motsetning til i sekvensielle filer,
mulig for programmereren å plassere filpekeren i ønsket posisjon. Til dette
formål inneholder RandomAccessFile
-klassen følgende metoder:
public long getFilePointer() // Returnerer aktuell posisjon // for filpekeren public void seek( long pos ) // Setter filpekeren til spesifisert // posisjon. Neste lese- eller skrive-operasjon // gjøres fra denne posisjon. public long length() // Returnerer fillengde i antall byter.
Alle disse metodene kan kaste ut en IOException
.
For å manøvrere filpekeren til en ønsket posisjon i fila, er det nødvendig
å kjenne postenes lengde, slik at vi kan regne oss fram til posisjonen.
Enklest er dette når alle postene har samme lengde, men dette er ikke noe
absolutt krav. For å regne ut postenes lengde, må vi bruke det vi vet om de
forskjellige datatypenes størrelse i antall byter. (Vi vet at
int
har 4 byter,
double
har 8 byter etc. I
læreboka til Deitel & Deitel, 9. utgave, finnes det en tabell over dette
på side 1405 (Appendiks D). Du kan også finne en tabell på nettadressen
http://download.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html.)
Som eksempel på bruk av direkte-fil (random-aksess-fil) skal vi ta for oss
et program som først trekker 100 slumptall og skriver dem til direkte-fila
"slumptall.dta". Deretter blir fila gjennomgått på den måten at hvert tredje
tall blir lest og lagt inn i en array. I fila blir det skrevet inn tallet 3
istedenfor det tallet som stod der. De andre tallene i fila blir værende som
de var. Filinnholdet blir skrevet ut i programvinduet før og etter filendring.
Arrayen med tallene som ble fjernet fra fila blir også skrevet ut.
Fila
Direktefileksempel.java
inneholder de metodene som behandler
fila "slumptall.dta" med de nevnte slumptallene. Fila
Direktefildemo.java
definerer programvinduet og bruker et
objekt av type Direktefileksempel
for å foreta filbehandlingen.
Nedenfor ser vi nærmere på de metodene som utfører filbehandlingen.
Vi ser først på metoden som trekker de 100 slumptallene og skriver dem til fil.
11 //oppretter fil og skriver inn 100 slumptall mellom 0 og 99 12 public void lagDirektefil() 13 { 14 try (RandomAccessFile output = new RandomAccessFile( 15 "src/slumptall.dta", "rw" )) 16 { 17 //skriver 100 slumptall til fila 18 for ( int i = 0; i < ANTALL; i++ ) 19 { 20 int slump = (int) (Math.random() * 100); 21 output.writeInt( slump ); 22 } 23 } 24 catch( IOException e ) 25 { 26 System.err.println( e.getMessage() ); 27 System.exit( 1 ); 28 } 29 }
Som tidligere nevnt, kan en RandomAccessFile
åpnes enten for
bare lesing, eller for både skriving og lesing. Her skal vi opprette og skrive
fila, og må derfor åpne den for både lesing og skriving, selv om vi bare skal
skrive til den i denne omgang. Altså bruker vi konstruktørparameteren
"rw"
i
tillegg til filnavnet. For å skrive en
int
-verdi til fila bruker
vi metoden writeInt
, som vi kjenner igjen fra den gang vi tok
for oss DataOutputStream
-klassen. Som nevnt ovenfor, kommer dette
av at både DataOutputStream
og RandomAccessFile
implementerer interface DataOutput
,
der denne metoden blir spesifisert.
Metoden visFil
, som er gjengitt nedenfor, åpner fila
"slumptall.dta" for lesing, derfor konstruktørparameter
"r"
i tillegg til
filnavnet når fila blir åpnet. Metoden leser tall for tall fra fila og tilføyer
dem til det tekstområdet som metoden mottar som parameter. Siden vi ønsker
15 tall per linje i dette tekstområdet, brukes det en tellevariabel for å finne
ut når vi skal tilføye linjeskifttegn. For øvrig vil det for
RandomAccessFile
være slik som det var for en
DataOutputStream
, at det på fila kan ligge en hvilken som helst
verdi. Derfor er det ingen spesiell verdi som kan brukes til å indikere
filslutt. Ved filslutt blir det isteden kastet ut en Exception
av
type EOFException
. Denne må metoden fange opp slik at filslutt
blir registrert på riktig måte. Det er når denne blir kastet ut at
innlesingsløkka blir avsluttet. Det eneste som da gjenstår er å lukke fila,
noe som vil bli gjort automatisk siden vi har åpnet den ved bruk av instruksjon av type
try
-med-ressurser.
For øvrig blir visFil
-metoden kalt opp to ganger av programmet:
før og etter at fila "slumptall.dta" er blitt modifisert ved kall på metoden
modifiserFil
.
31 //leser fila og dataene inn i tekstområdet som er parameter, 32 //15 tall pr. linje 33 public void visFil( JTextArea utskrift ) 34 { 35 try (RandomAccessFile input = new RandomAccessFile( 36 "src/slumptall.dta", "r" )) 37 { 38 int ant = 0; 39 int tallPrLinje = 15; 40 boolean mer = true; 41 while ( mer ) 42 { 43 try 44 { 45 int nesteTall = input.readInt(); 46 ant++; 47 utskrift.append( nesteTall + " " ); 48 if ( ant % tallPrLinje == 0 ) 49 utskrift.append( "\n" ); 50 } 51 catch( EOFException e ) 52 { 53 mer = false; 54 } 55 } 56 utskrift.append( "\n\n" ); 57 } 58 catch( IOException e ) 59 { 60 System.err.println( e.getMessage() ); 61 System.exit( 1 ); 62 } 63 }
Metoden modifiserFil
som er gjengitt nedenfor har som oppgave
å foreta den modifisering av fila "slumptall.dta" som ble nevnt ovenfor.
Hvert tredje tall i fila skal erstattes med tallet 3. De tallene som blir
erstattet skal lagres i en egen array. Fila må derfor i dette tilfelle åpnes
for både lesing og skriving. For å få plassert filpekeren foran hvert tredje
tall bruker vi metoden seek
. For angivelse av filposisjon (det
vil si filpekerens verdi), bruker vi long
-variabelen
pos
. Vi vet at hver int
-verdi er på 4 byter. For å
komme til det tredje tallet forfra i fila, gir vi derfor pos
verdien 8. Nå skal to ting gjøres: Vi skal lese den verdi som ligger på fila
og lagre den i arrayen kalt vraktabell
. I tillegg skal vi
på fila erstatte den verdien vi nå leste med verdien 3. For å gjøre dette,
må vi flytte filpekeren tilbake igjen 4 plasser, for da vi leste inn
tallet som var på fila, ble den flyttet 4 plasser fram. Nåværende plassering
av filpekeren kan vi få tak i ved kall på metoden
getFilePointer()
. For å flytte filpekeren 4 plasser tilbake fra
den posisjon vi er i, kan vi derfor skrive
fil.seek( fil.getFilePointer() - 4 );
Når det er gjort, kan vi skrive ut tallet 3 til fila, som erstatning for det som skulle fjernes. Da vil igjen filpekeren flytte seg 4 plasser framover. For å komme til det neste tallet som skal leses og erstattes med 3, må vi derfor flytte filpekeren 8 plasser framover ved at vi skriver
pos = fil.getFilePointer() + 8; //hopper over de to neste tall
fil.seek( pos );
Den fullstendige metoden modifiserFil
ser ut som følger:
65 //leser fila og bytter ut hvert tredje tall med 3 66 public void modifiserFil() 67 { 68 try (RandomAccessFile fil = new RandomAccessFile( 69 "src/slumptall.dta", "rw" )) 70 { 71 long lengde = fil.length(); //fillengde i antall byter 72 long pos = 8; //plasserer filpekeren ved beg. av tall nr. 3 73 fil.seek( pos ); 74 75 while ( pos < lengde ) 76 { 77 //leser fra fil tall som skal byttes ut og 78 //legger det inn i tabell: 79 vraktabell[ antVrak++ ] = fil.readInt(); 80 //setter tilbake filpekeren: 81 fil.seek( fil.getFilePointer() - 4 ); 82 fil.writeInt( 3 ); //bytter ut det eksisterende tall med 3 83 pos = fil.getFilePointer() + 8; //hopper over de to neste tall 84 fil.seek( pos ); 85 } 86 } 87 catch( IOException e ) 88 { 89 System.err.println( e.getMessage() ); 90 System.exit( 1 ); 91 } 92 }
Bildet under viser resultatet av en kjøring av programmet. Siden det trekkes slumptall, vil utskriften bli forskjellig fra kjøring til kjøring. Det som skal være likt hver gang, er at hvert tredje tall blir byttet ut med tallet 3.
File
-klassenDa vi skulle bruke dialogvinduer av type JFileChooser
for å
velge navn og plassering for den fil som skulle leses eller skrives, trengte
vi å ha kjennskap til File
-klassen. For vi fikk returnert den valgte
fila i form av et
File
-objekt, se
ovenfor. Men som der nevnt, representerer et
File
-objekt ikke noen fil som det kan leses fra eller
skrives til, men derimot diverse opplysninger om en slik og om dens omgivelser.
Vi skal nå se nærmere på hvilke opplysninger det her er snakk om.
Programmet Filtest.java
med driverklasse Filsjekking
lar brukeren skrive inn i et tekstfelt en streng som normalt skal være et
filnavn, en filsti eller et katalognavn (mappenavn). Metoden
sjekkFil
som er gjengitt nedenfor mottar den innleste strengen
som parameter. Metoden oppretter et File
-objekt med den innleste
strengen som konstruktørparameter. Det blir så sjekket om det i den aktuelle
filkatalog eksisterer
noe File
-objekt svarende til den innleste strengen.
For spørsmålet om hvor programmet vil starte å lete etter det fil- eller
katalognavn som er lest inn, gjelder tilsvarende kommentarer som er
skrevet til tidligere eksempler der filnavn er lest inn, se
ovenfor.
Dersom det eksisterer et slikt File
-objekt,
blir dets navn skrevet ut; det blir skrevet ut om det er en fil
eller en filkatalog. Videre blir det sjekket om det som ble skrevet inn er
en absolutt eller relativ filsti, det vil si om den starter på rotnivå eller
der vi er for øyeblikket. Det blir også vist når fila eller filkatalogen sist
ble modifisert, hvor lang den er (antall byte), filsti, absolutt filsti og
forelder-katalog. Dersom det var et filnavn som ble skrevet inn, blir denne
åpnet og lest, innholdet vises på skjermen. Var det derimot et katalognavn
som ble skrevet inn, så blir det listet opp hvilke underkataloger og filer
denne inneholder.
30 public void sjekkFil(String fil) 31 { 32 output.setText(""); 33 File navn = new File(fil); 34 35 if (navn.exists()) 36 { 37 output.append( 38 navn.getName() + " eksisterer\n" + 39 (navn.isFile() ? "er en fil\n" : "er ikke en fil\n") + 40 (navn.isDirectory() ? "er en katalog\n" : 41 "er ikke en katalog\n") + 42 (navn.isAbsolute() ? "er absolutt sti\n" : 43 "er ikke absolutt sti\n") + 44 "Sist modifisert: "); 45 long millisek = navn.lastModified(); 46 Date dato = new Date(millisek); 47 DateFormat formatterer = DateFormat.getInstance(); 48 String tidspunkt = formatterer.format(dato); 49 output.append(tidspunkt + 50 "\nLengde: " + navn.length() + 51 "\nSti: " + navn.getPath() + 52 "\nAbsolutt sti: " + navn.getAbsolutePath() + 53 "\nForelder: " + navn.getParent()); 54 55 if (navn.isFile()) 56 { // leser og viser fila 57 try (BufferedReader innfil = new BufferedReader( 58 new FileReader(navn))) 59 { 60 output.append("\n\n"); 61 String innlinje = null; 62 63 do 64 { 65 innlinje = innfil.readLine(); 66 if (innlinje != null) 67 output.append(innlinje + "\n"); 68 } 69 while (innlinje != null); 70 } 71 catch (IOException e2) 72 { 73 } 74 } 75 else if (navn.isDirectory()) 76 { // viser fillista 77 String[] dir = navn.list(); 78 output.append("\n\nKatalog inneholder:\n"); 79 80 for (int i = 0; i < dir.length; i++) 81 output.append(dir[i] + "\n"); 82 } 83 } 84 else 85 output.setText(fil + " eksisterer ikke.\n"); 86 output.setCaretPosition(0); 87 }
Bildet under viser et kjøreresultat for programmet.
Innholdsoversikt for programutvikling
Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2015