Innholdsoversikt for programutvikling

Filbehandling

Innledning

Programmer 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.

Lese tekstfil

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:

  1. Fila leses og skrives tegn for tegn som 16-biters-tegn.
  2. Fila leses og skrives linje for linje.
  3. Fila leses og skrives byte for byte.

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.

Lese tekstfil tegn for tegn

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 ressurser

Som 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.

Eksempel

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.

Lese tekstfil linje for linje — bufret innlesing

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-klassen

File-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.

Bruk av JFileChooser for filvalg

Klassen 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.

Lese fil byte for byte

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   }

Bufring

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.

Eksempel

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.

Skriving til fil og lesing fra fil av javas primitive datatyper, samt String-objekter — strukturerte filer

Hittil 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.

Eksempel

  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.

Eksempel

  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å.

LeseSkriveType
readBooleanwriteBooleanboolean
readCharwriteCharchar
readBytewriteBytebyte
readShortwriteShortshort
readIntwriteIntint
readLongwriteLonglong
readFloatwriteFloatfloat
readDoublewriteDoubledouble
readUTFwriteUTFString (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:

intStringStringdouble intStringStringdouble  . . . 

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.

Test på filslutt

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.

Eksempel

I DataIOTest.java som er gjengitt nedenfor, blir det opprettet en datafil med poster som har denne struktur:

doubleintString

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.

Skrive primitive datatyper til tekstfil: 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.

Eksempel

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 }

Serialisering av objekter

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.

Eksempel

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 }

Merknader

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.

Oppdatering av data i sekvensielle filer

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.

Random-aksess-filer

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

Programeksempel

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.

Mer om File-klassen

Da 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