Java Nio: Effektiv input/output

Javas nye input/output-klasser, Nio, er den største forskel imellem den seneste version 1.4 og forgængeren. De nye i/o-biblioteker giver væsentlig bedre ydelse og gør det muligt at skabe kode, hvor programudførslen ikke hænger, mens data-bufferne fyldes op. I denne artikel ser vi på anvendelsen af Nio i forbindelse med filer, og det er slet ikke så svært at udnytte de nye biblioteker endda.

Hvorfor Nio

Blandt de største nyskabelser i Java 1.4, som kom på gaden for et år siden, er input/output-klasserne Nio, som på trods af at de er et år gamle stadig betyder New input-output.

Det nye i bibliotekerne består i to forhold: En implementering af læsning og skrivning, som ligger tættere på den måde, styresystemer typisk implementerer i/o på, samt muligheden for at læse og skrive, uden at programafviklingen stopper, indtil en buffer er fyldt op.

Det har ikke så stor betydning ved læsning og skrivning til disk, hvor overførselshastigheden er høj, men ved dataoverførsel via netværk er programmets afviklingshastighed som regel mange gange større end netværkets hastighed.

Her kan man opleve, at en programdel blot står og venter på nye data i bufferen, mens programmet egentligt kunne foretage sig noget mere fornuftigt.

De fordele, som Nio har, kommer selvfølgelig med en pris. Nio-klasserne er mere system-nære end de gamle input/output-klasser, og derfor er der også flere detaljer at holde styr på.

Hit med gaffeltrucken
Nio til brug med filer er en del nemmere at gå til end Nio til netværksbrug, så det er det, vi kigger nærmere på i denne artikel.

Nios grundlæggende virkemåde er heldigvis simpel. De to centrale begreber er Channels og Buffers. Channels, kanaler, repræsenterer data. Humlen er, at man ikke kan tilgå kanalerne direkte: man skal i stedet benytte en buffer. Bufferen benyttes at skrive til eller læse fra kanalen.

Man kan sammenligne med et lager og en gaffeltruck. Varerne på lagret repræsenterer data, og gaffeltrucken er bufferen.

Programmøren må pænt vente ved porten til lagret, og benytte instruktioner til gaffeltrucken som en måde at hente og gemme data til og fra lageret.

Den buffer, som snakker direkte med kanalerne, er objektet java.nio.ByteBuffer. Det er ByteBufferen, som kommunikerer med kanalen, ved at transportere data frem og tilbage i mellem kanalen og programmørens kode.

Kanalerne, som befinder sig i pakken java.nio.channels, tilgås ved hjælp af nye metoder i de gamle input- og outputstream-biblioteker.

Læs og skriv med buffere

Lad os se på et gennemkommenteret eksempel på læsning og skrivning fra en fil ved hjælp af Nio.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class NioEksempel1 {
  private static final int BUFFER_SIZE = 1024;

  public static void main(String[] args) throws Exception {
    // Nio skriver:
    // En channel hentes fra en FileOutputStream-instans:
    FileChannel filechannel =
      new FileOutputStream("test.txt").getChannel();

    // Vi tager en streng, og omskaber den til et byte-array
    byte[] stringAsByte = "Vi skriver med Nio. ".getBytes();

    // Vi skaber en ByteBuffer-instans, og fylder
    // byte-arrayet ind i bufferen    
    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);    
    buffer.put(stringAsByte);
          
    // Efter at bufferen er blevet fyldt med vores bytes,
    // skal bufferen "klargøres" til skriving med metoden flip
    buffer.flip();
    
    // Derefter skrives bufferen til kanalen
    filechannel.write(buffer);
    filechannel.close();
    
    
    // Nio læser:
    // Her får vi filechannel-instansen fra en
    // FileInputStream-instans
    filechannel =
      new FileInputStream("test.txt").getChannel();
    filechannel.read(buffer);

    // Efter at bufferen er blevet fyldt op af
    // read-metoden i forrige linie, skal bufferen "klargøres"
    // til læsning med metoden flip
    buffer.flip();

    // Her udskriver vi de enkelte bytes i bufferen
    // ved at caste dem om til char's
    while (buffer.hasRemaining())
      System.out.print((char) buffer.get());
  }
}

Eksemplet er forhåbentlig ikke så slemt at se på. Det eneste, som kan virke lidt mystisk, er ByteBufferens metode flip.

Flip sætter ByteBufferets øvre grænse (limit) til den aktuelle værdi af den interne pegepind (position), og dernæst sættes pegepinden til nul. Næste gang, ByteBufferen anvendes, læses der altså fra starten, og til og med den sidste byte, som blev puttet ind i bufferen.

Metoden flip skal anvendes efter at put-metoden eller læsning af kanalen er anvendt, og før get-metoden eller skrivning til kanalen anvendes.

ByteBuffer har, udover flip-metoden, en række andre metoder til at manipulere de interne pegepinde, som ByteBufferen anvender.

I stedet for at instantiere ByteByfferen med den statiske metode allocate(BUFFER_SIZE), kan man også benytte den statiske metode wrap(byte[] bytearray), som tager et bytearray som argument, og derefter benytter dette bytearray som det underliggende lager for de enkelte bytes i ByteBufferen.

En hel fil

Ovenstående eksempel viser kun, hvorledes bufferen skrives til og læses fra kanalen. Hvis man skal læse en hel fil eller overføre data fra en strøm til en anden strøm, skal der lidt flere boller på suppen.

Læsningen af en hel fil er nemt nok. FileChannel-metoden read kaldes successivt, indtil metoden returnerer -1, som markerer slutning på strømmen.

Det kan se sådan ud:

public class NioEksempel2 {
  private static final int BUFFER_SIZE = 1024;

  public static void main(String[] args) throws Exception {
    FileChannel filechannel =
      new FileInputStream("C:\\data.txt").getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

    while (filechannel.read(buffer) != -1) {
      buffer.flip();
      while (buffer.hasRemaining())
        System.out.print((char) buffer.get());
    }
  }
}

Hvis man skal læse fra en inputstrøm og skrive til outputstrøm, kan man selvfølgelig bare læse en stak bytes i en ByteBuffer fra den ene strøm, og så skrive ByteBufferen til output-strømmen, men Nio har en indbygget metode til at overføre data direkte fra en kanal til en anden kanal.

Det kan se sådan ud:

public class NioEksempel3 {
  private static final int BUFFER_SIZE = 1024;

  public static void main(String[] args) throws Exception {
    FileChannel inFilechannel =
      new FileInputStream("data.txt").getChannel();
    FileChannel outFilechannel =
      new FileOutputStream("kopi.txt").getChannel();
    
    outFilechannel.transferFrom(
      inFilechannel,
      0,
      inFilechannel.size());
  }
}

Her overføres data imellem to fil-kanaler ved hjælp af FileChannel-metoden transferFrom, men operationen kan udføres mellem alle kanaler som er læsbare, til alle kanaler som er skrivbare.

Som argumenter tager transferFrom-metoden først den kanal, som der skal læses fra, og derefter den start- og slut-position, som den skal læse, målt i bytes. I eksemplet ovenfor læses hele filen.

Kodning af data

Kodning af data
I disse eksempler benytter vi tekstfiler, og den går egentlig ikke. Man kan nemlig ikke regne med, at konverteringen imellem char, String-metoden getBytes, og byte går problemfrit. Tekst er jo indkodet, som Unicode, ASCII, eller noget helt andet.

De gamle input/output-klasser til formålet - InputStreamReader og OutputStreamWriter - kan det med indkodning, og Nio har tilsvarende klassen CharBuffer, som er en søster-klasse til ByteBuffer. Man skal dog selv holde styr på indkodningen. Nio har pakken java.nio.charset til at oversætte imellem bytes og bestemte tegntabeller.

Tegn er ikke det eneste, som skal kodes på en eller anden facon. Der er buffere til andre primitive typer, som int, double, og så videre.

Views
Men det er kun ByteBuffer, som kan benyttes til at læse eller skrive fra en kanal med, og derfor skal CharBuffer, IntBuffer og de andre type-specifikke buffere svejses sammen med et ByteBuffer. Det gøres ved at benytte ByteBuffer-metoderne asCharBuffer, asIntBuffer med videre, som returnerer en CharBuffer- eller IntBuffer-instans, eller hvad det nu er for en type, det drejer sig om.

Denne CharBuffer benyttes så som et såkaldt view, hvilket vil sige, at operationer på instansen forplanter sig til det underliggende ByteBuffer, som holder styr på de underliggende bytes, der repræsenterer data. Man kan så skrive int-variabler til IntBufferen, og den instans omkoder den bagvedliggende repræsentation af int-variablen til bytes, som placeres i ByteBufferen, som så kan skrives til kanalen.

Endian
Endnu en slem ting, som man ikke altid kan ignorere, er det underliggende styresystems måde at gemme data på. Nogle systemer, "big endian", gemmer data med den mest signifikante byte på den laveste tilgængelige hukommelsesadresse. Andre systemer benytter den højeste hukommelsesadresse. Det sidste kaldes "little endian". Ved transmission over netværk benyttes altid big endian.

Man kan sætte et ByteBuffer til at være big endian eller little endian ved hjælp af hjælpe-klassen java.nio.ByteOrder, og man kan også benytte en statisk metode i objektet, ByteOrder.nativeOrder(), som fortæller hvilken orden, det aktuelle system anvender.

Læs mere
Et godt sted at læse mere om anvendelsen af Nio til filer er Java-guruen Bruce Eckels bog, Thinking in Java, 3. udgave, nærmere bestemt kapitel 12. Den kan, ganske generøst, downloades gratis fra Eckels hjemmeside i et læsevenligt HTML-format.

Mere om samme emne

Computerworld Events

Vi samler hvert år mere end 6.000 deltagere på mere end 70 events for it-professionelle.

Ekspertindsigt – Lyt til førende specialister og virksomheder, der deler viden om den nyeste teknologi og de bedste løsninger.
Netværk – Mød beslutningstagere, kolleger og samarbejdspartnere på tværs af brancher.
Praktisk viden – Få konkrete cases, værktøjer og inspiration, som du kan tage direkte med hjem i organisationen.
Aktuelle tendenser – Bliv opdateret på de vigtigste dagsordener inden for cloud, sikkerhed, data, AI og digital forretning.

Jura | København Ø

Compliance Day 2025

Få de nyeste indsigter fra eksperter om, hvordan du navigerer i et komplekst compliance-landskab, når vi samler viden om alt fra NIS2, AI Act, CRA, DORA til GDPR og SCHREMS2.

Sikkerhed | Klampenborg

Årets CISO 2025

Danmarks stærkeste program om cybersikkerhed. Mød finalisterne til Årets CISO 2025, hør aktuelle oplæg og få skarpe indsigter i sikkerhed, systemer og ledelse. Tilmeld dig og bliv opdateret på it-sikkerhed i praksis.

Sikkerhed | Klampenborg

Digitaliseringen skaber muligheder – og sårbarheder. Beredskab er løsningen.

Digitalisering skaber både muligheder og sårbarheder. Hele Danmark Øver styrker virksomhedernes beredskab gennem praktiske øvelser, indsigt og samarbejde. Deltag og lær, hvordan din organisation står stærkere, når cyberangrebet rammer.

Se alle vores events inden for it

Navnenyt fra it-Danmark

Netip A/S har pr. 19. august 2025 ansat Jacob Vildbæk Jensen som Datateknikerelev ved afd. Herning og afd. Rødekro. Han har tidligere beskæftiget sig med tjenerfaget,. Nyt job
Netip A/S har pr. 19. august 2025 ansat Carl Severin Degn Nørlyng som IT-Supporterelev ved afd. Thisted og afd. Herlev. Nyt job
Norriq Danmark A/S har pr. 1. september 2025 ansat Søren Vindfelt Røn som Data & AI Consultant. Han skal især beskæftige sig med at effektivisere, planlægge og implementere innovative, digitale løsninger for Norriqs kunder. Han kommer fra en stilling som Co-founder & CMO hos DrinkSaver. Han er uddannet Masters of science på Københavns IT-Universitet. Nyt job

Søren Vindfelt Røn

Norriq Danmark A/S

Sentia har pr. 1. oktober 2025 ansat Morten Jørgensen som Chief Commercial Officer. Han skal især beskæftige sig med udbygning af Sentias markedsposition og forretningsområder med det overordnede ansvar for den kommercielle organisation. Han kommer fra en stilling som Forretningsdirektør hos Emagine. Nyt job