Överlagring av metodnamn

Precis som man kan skapa flera olika konstruerare så länge de har olika antal eller typer av argument så kan man skapa flera metoder med samma namn. Detta kallas överlagring (overloading).

Matematiska funktioner

En annan viktig klass i Java:s api är Math. Denna skapar man inga objekt av. Den har inga konstruerare. Klassen samlar ett antal statiska metoder och konstanter som har att göra med matematiska beräkningar.

Math i Oracles dokumentation

Primitiva värden som objekt

För de primitiva typerna finns klasser som motsvarar dem.

primitiv typ klass
int Integer
boolean Boolean
double Double
m.fl.

Notera att Integer inte kan lagra godtyckligt stora tal som typen med samma namn i Haskell. Det är bara en klass med en instansvariabel som är av typen int.

Dessa klasser har en del statiska metoder, t.ex. parseInt och parseDouble som vi sett. Man kan skapa objekt av dessa klasser:

Integer x = new Integer(25);

Man har nytta av dessa klasser när man vill kunna skicka värden som referenser eller kunna ge variabler värdet null. Mest har man nytta av det i vissa sammanhang (som vi kommer senare) där det inte är möjligt att använda de primitiva typerna.

Java gör automatisk omvandling mellan primitiva värden och motsvarande objekt. Detta kallas boxing/unboxing.

Integer x = new Integer(25);
int y = x;  // Detta går.
x = y + 12;  // Detta också.

Arv

Arv (inheritance) är en viktig mekanisk för att gruppera och återanvända kod. En klass som ärver en annan har automatiskt alla variabler och metoder som förälderklassen har. Förälderklassen kallas superklass och klassen som ärver subklass. I klassen som ärver kan ytterligare variabler och metoder läggas till.

Alla klasser i Java ärver implicit en grundklass som heter Object.

Om en klass B ärver en klass A så är ett objekt av klassen B också ett objekt av klassen A. Antag att vi har en klass Däggdjur som representerar ett djur och flera klasser som ärver denna, t.ex. Människa och Ko. Människor och kor är också däggdjur. Man kan skriva:

Människa m = new Människa(...);

Däggdjur d = m;

Detta fungerar för ett objekt av typen Människa har alla variabler och metoder som Däggdjur har.

Vill man göra tvärtom så klagar kompilatorn.

Människa m = new Människa(...);

Däggdjur d = m;

Människa m2 = d;

Det är i allmänhet inte säkert att ett Däggdjur är just en Människa. Är man, som i fallet ovan, säker på det så kan man göra en explicit typomvandling (type cast).

Människa m2 = (Människa)d;

Kompilatorn accepterar då det, men när programmet körs kommer det utföras en kontroll av att d verkligen är ett objekt av klassen Människa.

Om man vill skriva en klass som ärver, utvidgar en ennen klass så använder man nyckelordet extends.

public class Människa extends Däggdjur {
  ...
}

Vi kommer återkomma till arv senare. Då kommer vi prata om hur man skriver klasser som ärver varandra. Det väsentliga nu är att känna till att denna mekanism finns eftersom klassernas i Javas API utgör en hierarki där klasser ärver varandra på flera nivåer.

Gränssnitt

Gränssnitt (interface) i Java är besläktade med klasser. De kan ses som abstrakta klasser på det viset att de bara säger vilka metoder som ska finnas. Metoderna definieras inte och inga klassvariabler anges. Ett interface specificerar alltså bara gränssnittet, inte hur representationen av objekten ska implementeras. För att kunna använda ett gränssnitt måste det finnas en klass som implementerar det.

Interface är det som liknar klass-mekanismen i Haskell mest. Om vi tar klasserna Show och Eq i Haskell som exempel så skulle vi kunna definiera två interface i Java med samma namn. Interfacet Show skulle innehålla signaturen för en metod som skapar en sträng som återspeglar objektets värde. Interfacet Eq skulle innehålla signaturen för en metod som jämför objekt med varandra. En klass som sedan vill implementera någon av dessa interface behöver bara definiera vad motsvarande funktion ska göra.

En klass kan implementera flera interface.

Notera att grundklassen Object innehåller metoderna toString och equals så interface motsvarande Show och Eq i Java är i praktiken överflödiga.

Detta kommer vi också återkomma till och nämns nu för att interface i Javas API är vanligt förekommande.

For each-loopar

Det finns en variant på for-loopar där man på ett smidigt sätt kan utföra något för varje element i en array.

int[] a = ...;

for (int e : a) {
  ...
}

Loopen upprepas en gång för varje element i a och för varje iteration är x värdet på aktuellt element. Man slipper använda ett index, ange vilka tal det ska löpa mellan och slå rätt element i arrayen.

Exempel:

public static boolean member(int n, int[] a) {
  for (int e : a) {
    if (e == x) return true;
  }
  return false;
}

Switch-satser

Låt oss säga att vi använder helta för att representera veckodagar. Låt 0 vara måndag, 1 tisdag, … och 6 vara söndag. Om vi sedan vill ha en metod som skriver ut öppettider för en affär för en viss veckodag så kan den se ut så här:

public static void skrivÖppettider(int veckodag) {
  // 0 <= veckodag <= 6
  String s;
  if (veckodag <= 3) {  // måndag till torsdag
    s = "10:00 - 20:00";
  } else if (veckodag == 4) {  // fredag
    s = "10:00 - 19:00";
  } else if (veckodag == 5) {  // lördag
    s = "12:00 - 18:00";
  } else {  // söndag
    s = "12:00 - 16:00";
  }
  System.out.println(s);
}

Istället för att använda en massa if-satser kan man använda switch-satsen:

public static void skrivÖppettider(int veckodag) {
  String s;
  switch (veckodag) {
    case 0:
    case 1:
    case 2:
    case 3:
      s = "10:00 - 20:00";
      break;
    case 4:
      s = "10:00 - 19:00";
      break;
    case 5:
      s = "12:00 - 18:00";
      break;
    case 6:
      s = "12:00 - 16:00";
      break;
  }
  System.out.println(s);
}

Notera att utan en break-sats fortsätter exekveringen förbi nästa case.

Det finns också något som motsvarar else. Det heter default och utförs om värdet ej motsvarar någon av fallen. Föregående switch-sats kan alltså också skrivas så här:

public static void skrivÖppettider(int veckodag) {
  String s;
  switch (veckodag) {
    case 4:
      s = "10:00 - 19:00";
      break;
    case 5:
      s = "12:00 - 18:00";
      break;
    case 6:
      s = "12:00 - 16:00";
      break;
    default:  // måndag - torsdag
      s = "10:00 - 20:00";
      break;
  }
  System.out.println(s);
}

Switch satser kan användas för värden av de primitiva typerna byte, short, char och int. De fungerar också med de boxade varianterna av dessa typer (Byte, Short,, Character, Integer) och med String.

Enumereringar

Enumereringar kan man användas för att göra betydelsen av olika variabelvärden tydligare och värdena anpassade till situationen.

Att som i förra avsnittet representera veckodagar med heltal kan lätt leda till kryptisk kod. Då är det bättre att definiera en enumerering:

public enum Veckodag {
  MÅNDAG, TISDAG, ONSDAG, TORSDAG, FREDAG, LÖRDAG, SÖNDAG
}

Man kan deklarera variabler av denna typ och de olika värderna kan man hänvisa till genom att kvalificera dem med enumereringens namn:

Veckodag dag = Veckodag.MÅNDAG;

Switch-satser kan, förutom de typer som nämndes i förra avsnittet, användas med enumereringar. Den första switch-satsen i förra avsnitten blir då så här:

public static void skrivÖppettider(Veckodag veckodag) {
  String s;
  switch (veckodag) {
    case MÅNDAG:
    case TISDAG:
    case ONSDAG:
    case TORSDAG:
      s = "10:00 - 20:00";
      break;
    case FREDAG:
      s = "10:00 - 19:00";
      break;
    case LÖRDAG:
      s = "12:00 - 18:00";
      break;
    case SÖNDAG:
      s = "12:00 - 16:00";
      break;
  }
  System.out.println(s);
}

Notera att i casen måste man inte skriva Veckodag.MÅNDAG e.t.c.

Man kan också använda == för att avgöra om två värden av en enumerering är lika.

Undantag

Undantag (exceptions) är en mekanism i Java för att hantera onormala situationer och resultat. Vad man väljer att se som onormal är förstås inte uppenbart.

Metoder som har att göra med input och output till filer har man valt att hantera I/O-problem (som att filen inte finns) med hjälpa av undantag. Det skulle också kunna vara så att dessa metoder returnerar ett speciellt värder när fel uppstår, t.ex. null där det är lämpligt.

Närmsta motsvarigheten till undantag i Haskell är error.

Undantag är en hierarki med klasser där Exception är grundklassen.

För att skapa (kasta, throw) ett undantag använder man nyckelorder throw.

throw new Exception();

Objektet man skapar måste vara en Exception eller en underklass till denna. Exception har flera konstruerare. Vanligt är att använda en som låter dig beskriva felet som uppstått med en sträng.

throw new Exception("felmeddelande");

Om man vill kunna lagra information av specifik typ kan man själv skapa en underklass till Exception. Detta görs också ofta enbart för att ge undantagstypen ett beskrivande namn, t.ex. IllegalArgumentException.

Precis som i Haskell där man kan fånga errors så kan man fånga undantag så att de inte orsakar programstopp och felmeddelanden. Detta gör man genom att omge koden som kan orsaka undantaget med följande konstruktion:

try {
  koden som kan kasta undantag
} catch (Exception e) {
  koden som hanterar undantaget
}

Koden som kan kasta undantag kan innehålla flera ställen som kan kasta undantag. En vinst med undantag är att, istället för att behöva kontrollera för t.ex. varje läs- eller skrivoperation om något gick snett, kunna lägga flera sådana operationer i ett try-block och bara behöva hantera att något gått snett en gång.

Om man anger klassen Exception efter catch så fångas alla undantag eftersom alla undantag är en Exception. Om man anger t.ex. klassen IllegalArgumentException så fångas undantag av denna typ (och de som tillhör en underklass). Andra undantag som kastas i try-blocket skickas vidare uppåt i stacken av anropande metoder. Om inget try-catch-block fångar ett undantag så avbryts programexekveringen och undantaget skrivs ut.

Det finns två sorters undantag, checked och unchecked. Den första sorten måste man för varje metod som inte hanterar dem tala om att de kan komma att kastas. I labb 1 används följande signatur för main-metoden:

public static void main(String[] args) throws Exception

Detta för att inläsning av en fil, som man ombeds utföra, kan kasta undantag som är av typen checked. Meningen är att det tydligt ska synas, precis som det tydligt syns vilken sorts värde som returneras, att metoden kan kasta ett undantag.

För den andra sorten, unchecked, behöver man inte tala om att en metod kan kasta dem. Alla undantag som hör till klassen RuntimeException eller dess underklasser är unchecked. Alla undantag som inte gör det är checked.

I en övningsuppgift första veckan så skrev vi en metod som hittar maxvärdet i en array av flyttal:

public static double maxDoubles(double[] a) {
  double max = a[0];
  for (int i = 1; i < a.length; i++) {
    if (a[i] > max) max = a[i];
  }
  return max;
}

Metoden fungerar bara då a har längd 1 eller mer. Om man anropar metoden med en array av längden 0 som argument så kommer första raden orsaka ett undantag, ett IndexOutOfBoundsException. Detta undantag är underklass till RuntimeException. Därför måste man inte skriva throws IndexOutOfBoundsException i metodens signatur.

För en användare av metoden kan det vara bättre att få en förklaring till varför argumentet inte är giltigt. Vi kan skriva om den så här:

public static double maxDoubles(double[] a) {
  if (a.length == 0) {
    throw new IllegalArgumentException(
     "maxDoubles: argument a must contain at least one element");
  }
  double max = a[0];
  for (int i = 1; i < a.length; i++) {
    if (a[i] > max) max = a[i];
  }
  return max;
}

Ett alternativ är att låta IndexOutOfBoundsException uppstå, fånga det och kasta ett IllegalArgumentException.

public static double maxDoubles(double[] a) {
  try {
    double max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (a[i] > max) max = a[i];
    }
    return max;
  } catch (IndexOutOfBoundsException e) {
    throw new IllegalArgumentException(
     "maxDoubles: argument a must contain at least one element");
  }
}

När ett undantag inträffer i try-blocket avbryts exekveringen direkt av blocket. Men programmet kan befinna sig i ett tillstånd där den har resurser vars användning bör avslutas på korrekt sätt. Det kan t. ex. röra sig om öppna filer som behöver stängas. För att kunna hantera detta kan try-catch-satsen ha finally-block i slutet. Detta block exekveras oavsett om ett undantag uppstått.

Om man vill skilja på hanteringen av olika typer av undantag så kan man inkludera flera catch-block.

Input/Output

Vi har sett exempel på att läsa från en fil genom att använda java.nio.file.Files.readAllBytes. java.nio är ett paket för filhantering på låg nivå. Vi ska nu titta på några klasser och metoder i paketet java.io som tillåter filåtkomst på högre nivå.

Klassen File är en abstrakt representation av sökvägar till filer och kataloger, liknande Path i java.nio.

File infil = new File("in.txt");

Om man vill läsa in från eller skriva till filer binärt så kan man använda klasserna FileInputStream, FileOutputStream. Med dessa kan man läsa och skriva sekvenser av bytes.

Här ska vi fokusera på att läsa och skriva textfiler. För att läsa textfiler använder man lämpligen klassen Scanner, som finns i paketet java.util.

Scanner scanner = new Scanner(infil);

Denna konstruera kastar undantaget FileNotFoundException om filen inte finns. Vi måste hantera undantaget på något sätt. Låt oss göra det med try-catch.

try {
    File infil = new File("in.txt");
    Scanner scanner = new Scanner(infil);
    System.out.println("Filen är öppen");
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
}

Inläsning via Scanner bygger på att läsa token i taget och vad som är ett token definieras av en avgränsare (delimiter). Förvald delimiter är mellanrum. Man kan avgöra om det finns fler tokens med metoden hasNext. Man kan läsa in olika typer av värden med metoderns next..., t.ex. nextDouble. Man kan läsa in en hel rad med nextLine.

Låt oss läsa in flyttal så länge filen inte är slut.

try {
    File infil = new File("in.txt");
    Scanner scanner = new Scanner(infil);
    
    while (scanner.hasNext()) {
        double x = scanner.nextDouble();
        System.out.println(Math.sqrt(x));
    }
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
}

Prövar vi detta när in.txt innehåller annat flyttal så får vi ett runtime-fel, ett ohanterat undantag av typen InputMismatchException. Denna härrör från RuntimeException och därför fick vi ingen kompileringsvarning. Låt oss hantera detta undantag.

try {
    File infil = new File("in.txt");
    Scanner scanner = new Scanner(infil);
    
    while (scanner.hasNext()) {
        double x = scanner.nextDouble();
        System.out.println(Math.sqrt(x));
    }
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
} catch (InputMismatchException e) {
    System.out.println("Indata måste vara flyttal.");
}

Ofta uppstår inget problem om man glömmer att stänga en fil, men det är säkrast och snyggast att göra det explicit så fort man är klar med filen.

try {
    File infil = new File("in.txt");
    Scanner scanner = new Scanner(infil);
    
    while (scanner.hasNext()) {
        double x = scanner.nextDouble();
        System.out.println(Math.sqrt(x));
    }
    
    scanner.close();
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
} catch (InputMismatchException e) {
    System.out.println("Indata måste vara flyttal.");
}

Men om det uppstår ett undantag när filen är öppen så stängs den aldrig explicit eftersom exekveringen av try-blocked då avbryts. Vi kan använda ett finally-block för att hantera detta.

Scanner scanner = null;
try {
    File infil = new File("in.txt");
    scanner = new Scanner(infil);
    
    while (scanner.hasNext()) {
        double x = scanner.nextDouble();
        System.out.println(Math.sqrt(x));
    }
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
} catch (InputMismatchException e) {
    System.out.println("Indata måste vara flyttal.");
} finally {
  if (scanner != null) scanner.close();
}

Om ett undantag uppstod vid öppnandet av filen så är inte filen öppen och kan heller inte stängas. Denna situation motsvaras av att scanner är null.

Om vi vill skriva resultatet till en textfil istället för standard output så kan vi använda klassen PrintWriter.

Scanner scanner = null;
PrintWriter writer = null;
try {
    File infil = new File("in.txt");
    scanner = new Scanner(infil);

    File utfil = new File("ut.txt");
    writer = new PrintWriter(utfil);
    
    while (scanner.hasNext()) {
        double x = scanner.nextDouble();
        writer.println(Math.sqrt(x));
    }
} catch (FileNotFoundException e) {
    System.out.println(e.getMessage());
} catch (InputMismatchException e) {
    System.out.println("Indata måste vara flyttal.");
} finally {
  if (scanner != null) scanner.close();
  if (writer != null) writer.close();
}