Input, output og filer


Output til terminal

For å skrive ut tekst til terminalen kan vi bruke System.out.println()-metoden eller System.out.print()-metoden. Begge metodene tar en streng som parameter, og skriver ut strengen til terminalen. Forskjellen er at println()-metoden skriver ut et linjeskift etter strengen, mens print()-metoden ikke gjør det.

System.out.println("Hei, verden!");
System.out.print("Skrive ut uten linjeskift.");
System.out.println("Her fortsetter det bare.");

// Skriver ut:
// Hei, verden!
// Skrive ut uten linjeskift.Her fortsetter det bare.

Det er i tillegg mulig til å skrive ut feilmeldinger. Avhengig av hvilket terminal-program du bruker, kan det være slike feilmeldinger skrives ut med en annen farge (for eksempel rød). Slike feilmeldinger skrives ut til System.err i stedet for System.out.

System.out.println("Dette er en vanlig beskjed.");
System.err.println("Dette er en feilmelding.");

Input

Det er flere måter å lese inn data fra terminalen på. Vi skal se på to av dem her, Scanner og BufferedReader. Både Scanner og BufferedReader kan lese inn data både fra terminalen og fra filer. Scanner er enkel og egnet for interaktive applikasjoner i terminalen, men er irriterende treig dersom man skal lese mye. Derfor er det ikke vanlig å bruke Scanner for å lese store filer. BufferedReader er mye raskere, men krever at at vi håndterer mer selv.

Scanner

En mulighet for å lese inn data fra terminalen er å bruke et Scanner-objekt. Scanner må importeres fra java.util-pakken.

Fordeler med Scanner:

Ulemper med Scanner:

Dersom noe går galt med Scanner, vil den kaste en InputMismatchException. For eksempel dersom du bruker nextInt() i koden, mens det ikke er et heltall som faktisk blir lest. Scanner krever ikke at vi håndterer dette, men det vil krasje programmet vårt hvis vi ikke gjør det.

import java.util.Scanner;

public class Main {
  public static void main(String[] args) {    
    Scanner scanner = new Scanner(System.in);

    // Lese tall
    System.out.print("Skriv inn et heltall: ");
    int num = scanner.nextInt();
    System.out.println("Du skrev inn tallet " + num);

    System.out.print("Skriv inn et flyttall: ");
    double floatnum = scanner.nextDouble();
    System.out.println("Du skrev inn tallet " + floatnum);

    // Lese ord og setninger som strenger
    System.out.print("Skriv inn en setning med minst fire ord: ");
    String word1 = scanner.next();
    System.out.println("Det første ordet var: \"" + word1 + "\"");

    String word2 = scanner.next();
    System.out.println("Det andre ordet var: \"" + word2 + "\"");

    String rest = scanner.nextLine();
    System.out.println("Resten av setningen var: \"" + rest + "\"");

    // Lese helt til EOF er mottat. EOF sendes vanligvis til terminal
    // med ctrl+d (mac/linux) eller ctrl+z (windows). Din IDE kan ha egne
    // måter å sende EOF på.
    System.out.print("Skriv så mye du vil, avslutt med EOF:");
    while (scanner.hasNext()) {
      String word = scanner.next();
      System.out.println("Scanner got: \"" + word + "\"");
    }

    // Når vi er ferdig å bruke scanneren må vi lukke den, ellers
    // lekker vi minne
    scanner.close();
  }
}

BufferedReader

En annen måte å lese inn data fra terminalen på er å bruke et BufferedReader-objekt. BufferedReader og InputStreamReader må importeres fra java.io-pakken.

Fordeler med BufferedReader:

Ulemper med BufferedReader:

BufferedReader vil kaste IOException dersom noe går galt, og krever at vi er bevisst vår håndtering av dette. Dersom vi for eksempel vil at programmet skal krasje dersom vi får IOException, kan vi legge til throws IOException i metodesignaturen til metoden hvor vi bruker BufferedReader (for eksempel: public static void main(String[] args) throws IOException {...).

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
  public static void main(String[] args) throws IOException {
    BufferedReader reader = new BufferedReader(
        new InputStreamReader(System.in)
    );

    // Lese inn tall: vi må konvertere dem til riktig datatype selv
    System.out.print("Skriv inn ett heltall på én linje: ");
    int num = Integer.parseInt(reader.readLine());
    System.out.println("Du skrev inn tallet " + num);

    System.out.print("Skriv inn flere flyttall på samme linje: ");
    double sum = 0;
    for (String floatnum : reader.readLine().split(" ")) {
      sum += Double.parseDouble(floatnum);
    }
    System.out.println("Summen av tallene er: " + sum);

    // Lese inn linjer som strenger
    System.out.print("Skriv inn en linje: ");
    String sentence = reader.readLine();
    System.out.println("Du skrev inn: \"" + sentence + "\"");

    // Lese helt til EOF er mottat. EOF sendes vanligvis til terminal
    // med ctrl+d (mac/linux) eller ctrl+z (windows). Din IDE kan ha egne
    // måter å sende EOF på.
    System.out.print("Skriv så mange linjer du vil, avslutt med EOF:");
    String s;
    while ((s = reader.readLine()) != null) {
      System.out.println("BufferdReader leste: \"" + s + "\"");
    }

    // Når vi er ferdig å bruke reader må vi lukke den (ellers
    // lekker vi minne)
    reader.close();
  }
}

Filer

For å kunne lese eller skrive til en fil, er det vanlig å først opprette et File-objekt. Dette objektet representerer en fil sin plassering i filsystemet, og har egentlig ingenting med filens innhold å gjøre. Filens innhold kan man derimot få tilgang til via en stream (eller de mer brukervennlige abstraksjonene av dette som ofte kalles reader og writer). Når vi oppretter en slik stream, kan vi bruker et File-objekt for å angi hvilken fil vi ønsker tilgang til.

En stream har en retning. For eksempel vil en PrintStream gi oss anledning til å skrive ut data fra programmet vårt og inn i en fil, mens en InputStreamReader gir oss i stedet anledning til å lese data fra filen inn i programmet.

Skrive til en fil

Her et eksempel på å skrive til en fil:

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;

public class Main {
  public static void main(String[] args) throws IOException {
    File file = new File("foo.txt");
    // Oppretter en tom fil i filsystemet hvis den ikke finnes fra før
    file.createNewFile();

    // For å skrive til filen, bruker vi et PrintStream -objekt
    PrintStream writer = new PrintStream(file, StandardCharsets.UTF_8);
    writer.println("En linje med linjeskift på slutten");
    writer.print("Skriver noe uten å slutte med linjeskift.");
    writer.print("Det bare fortsetter på samme linje.");
    writer.println(); // Legger til et linjeskift
    writer.close();
    // Husk a kalle .close(), hvis ikke er det ikke sikkert at Java ble
    // ferdig med å skrive til filen før programmet ble avsluttet.

    System.out.println("PS: System.out er et PrintStream -objekt");
    System.out.println(System.out instanceof PrintStream); // true
  }
}

Lese fra en fil

Akkurat som når vi leser input fra terminalen, kan vi bruke BufferedReader også når vi leser fra en fil. Da vil lines().toList() gi oss alle linjene i filen som en liste, eller vi kan bruke readLine() for å lese én og én linje om gangen.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class Main {
  public static void main(String[] args) throws IOException {
    File file = new File("foo.txt");
    BufferedReader reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream(file),
            StandardCharsets.UTF_8
        )
    );
    List<String> lines = reader.lines().toList();
    reader.close();

    // Skriver ut alle linjene i filen
    for (String line : lines) {
      System.out.println(line);
    }
  }
}

Det er også mulig å bruke Scanner i stedet, den kan eventuelt opprettes med new Scanner(file). Scanner er såpass treigt at den ikke egentlig egner seg for å lese store filer.

Current working directory

Når vi oppretter et File-objekt, oppgir vi et filnavn, eller en sti til en fil. Dersom vi oppgir kun et filnavn tolkes dette som at filen ligger i current working directory (cwd), og dersom vi oppgir en relativ sti tolkes dette som at stien er relativ til cwd.

Hvilken mappe dette er bestemmes av den som starter programmet, og er helt uavhengig av hvor kildekoden eller den kompilerte koden ligger lagret på datamaskinen. Vi kan se hva cwd er ved å skrive ut verdien av systemvariabelen user.dir:

// Viser current working directory
System.out.println("cwd: " + System.getProperty("user.dir"));

I motsetning til andre programmeringsspråk, er det ikke mulig å endre cwd programmatisk fra «innsiden» av et Java-program på en pålitelig måte.

Mapper

I Java -verden representeres både filer og mapper som File -objekter. Det er mulig å sjekke om et File -objekt er en mappe eller en fil ved å bruke isDirectory() og isFile().

File cwd = new File("."); // "." representerer her cwd -- som er en mappe
System.out.println(cwd.isDirectory()); // true
System.out.println(cwd.isFile()); // false
System.out.println(cwd.getCanonicalPath()); // viser stien til cwd

// Skriv ut filene som er i cwd
// Returverdien av listFiles() er null hvis File-objektet ikke er en mappe
// Returverdien av listFiles() er en tom array hvis det er en tom mappe
for (File file : cwd.listFiles()) {
  System.out.println(file.getName());
}

Vi kan opprette mapper ved å bruke mkdir() eller mkdirs(). mkdir() vil opprette mappen hvis den ikke finnes fra før, men forutsetter at alle mappene frem til den siste leddet i stien eksisterer fra før. mkdirs() vil derimot opprette alle mappene og undermappene i den angitte stien hvis de ikke finnes fra før.

File dir = new File("foo/bar/baz");
dir.mkdirs(); // Oppretter alle mappene i stien hvis de ikke finnes fra før

Å opprette en tom fil i en mappe som ikke finnes fra før

File file = new File("foo/bar/baz.txt");
file.getParentFile().mkdirs();
file.createNewFile();

Siden vi ikke kan endre på den faktiske cwd, kan vi i stedet lage oss «vår egen cwd» som vi faktisk kan endre og dermed navigere med. File-klassen har støtte for et slikt opplegg.

// Oppretter vår egen "falske" cwd som vi kaller myCwd
File myCwd = new File("./my/main/directory").getCanonicalFile();
System.out.println(myCwd.getCanonicalPath()); // Viser stien til myCwd
myCwd.mkdirs(); 

// Oppretter en fil i myCwd -mappen
File file = new File(myCwd, "foo.txt");
file.createNewFile();

// Endre myCwd til å peke på foreldremappen
myCwd = myCwd.getParentFile();
System.out.println(myCwd.getCanonicalPath());

// Navigere tilbake til "directory" -mappen med myCwd
myCwd = new File(myCwd, "directory");
System.out.println(myCwd.getCanonicalPath());

Ressurser

Noen filer kan være en integrert del av programmet vi skriver, og ikke en del av den input vi forventer fra bruken av programmet. Eksempler på slike filer er bilder eller lydfiler vi bruker i programmet vårt, eller en konfigurasjonsfil som på en eller annen måte bestemmer hvordan programmet er bygget opp. Det kan også være filer som inneholder databaser som er en del av programmet, for eksempel en ordliste i et ordspill.

Disse filene må vi kunne åpne uansett hva cwd er når programmet starter, eller hvilken datamaskin programmet kjører på.

For Maven-prosjekter løser vi dette ved å legge slike ressurs-file i src/main/resources -mappen. Maven vil automatisk kopiere disse filene til riktig sted når vi bygger prosjektet vårt, og de kan leses med spesielle metoder. Dersom ressursen hører til en bestemt pakke, kan vi legge den i en undermappe med samme navn som pakken.

For eksempel, anta at vi har en tekstfil wordlist.txt vi ønsker å bruke i klassen MyClass som er i pakken no.uib. Vi legger den da i mappen src/main/resources/no/uib. Vi kan så lese inn filen i programmet med følgende kode:

package no.uib;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class MyClass {
  public static void main(String[] args) throws IOException {
    BufferedReader reader = new BufferedReader(
        new InputStreamReader(
            MyClass.class.getResourceAsStream("wordlist.txt"),
            StandardCharsets.UTF_8
        )
    );

    List<String> words = reader.lines().toList();
    reader.close();

    System.out.println("Ordene i wordlist.txt er: ");
    for (String word : words) {
      System.out.println(word);
    }
  }
}

Magien skjer på linjen MyClass.class.getResourceAsStream("wordlist.txt"). Her leter den etter filen wordlist.txt i samme pakke som klassen MyClass ligger i. Det gir ikke mening at dette skal returnere et File -objekt, siden det for ressurs-filer nettopp ikke er relevant hvor filen ligger i filsystemet. I stedet gir den oss innholdet i filen som en såkalt InputStream. Fordi dette innholdet er plaintext, pakker vi den videre inn i en InputStreamReader som tolker filen med UTF-8. For å lese innholdet raskt, pakker vi den videre inn i en BufferedReader før vi leser.