Generiske typer

I offisiell tutorial: Generics


Klientkode angir typeargumenter

Vi har allerede sett og brukt klasser med generiske typer i rollen som klient. For eksempel lister:

// Klientkode
List<Integer> a = new ArrayList<>(); // Typeargumentet er `Integer`
a.add(1);
a.add(2);
Integer i = a.get(0);

List<String> b = new ArrayList<>(); // Typeargumentet er `String`
b.add("Foo");
b.add("Bar");
String s = b.get(0);

Som vi ser, kan vi lage List-variabler både for heltall og for strenger. Vi kunne også angitt at listen skal inneholde objekter i en av våre egne hjemmelagde klasser. Når vi har angitt et typeargument mellom krokodilleklammene <>, vil liste-objektet fremover kreve at vi kun legger til denne typen, og garanterer at vi kun vil få hente ut elementer av denne typen. Kildekoden som implementerer List og ArrayList er helt vanlig Java-kode, og kan nødvendigvis ikke vite hvilken type dette er. Under panseret i disse klassene benyttes det i stedet generiske typer.

Første eksempel: en boks <T>

La oss si at vi ønsker å lage en «liste» som har plass til maksimalt ett element. La oss kalle det en boks. Vi ønsker at boksen skal fungere med elementer av den typen klient-koden angir. Eksempel på klient-kode kan være:

// Klientkode
MyBox<Integer> boxA = new MyBox<>();
boxA.set(1);
Integer i = boxA.get();

MyBox<String> boxB = new MyBox<>();
boxB.set("Foo");
String d = boxB.get();

Vi kan implementere denne klassen ved å bruke en generisk type slik:

// Implementasjonskode
public class MyBox<T> {
  private T element = null;

  public void set(T element) {
    this.element = element;
  }

  public T get() {
    return element;
  }
}

I implementasjonskoden over angir <T> at T for denne klassen er en typeparameter. Den er en plassholder for en ukjent type. Hvis vi i klientkoden angir at typeargumentet er String, kan vi litt forenklet forestille oss at alle forekomster av T blir erstattet med String i implementasjonskoden når koden kjører.

Det er vanlig at typeparameter er navngitt med én enkelt stor bokstav, som regel T (for type), E (for element), K (for key), V (for value) eller andre mer eller mindre beskrivende enkeltbokstaver hvis man trenger flere enn én generisk type. Dette er ikke noe Java krever, og typeparameteren kan teknisk sett kalles FunnyType eller noe annet mer selvbeskrivende hvis vi ønsker.

Flere generiske typer på én gang <A, B>

Det er mulig å ha flere generiske typer i spill på én gang.

// Klientkode
Pair<String, Integer> p1 = new Pair<>("Foo", 42);
String s = p1.getFirst();
Integer i = p1.getSecond();

Pair<Double, Boolean> p2 = new Pair<>(0.5, true);
Double d = p2.getFirst();
Boolean b = p2.getSecond();
// Implementasjonskode
public class Pair<A, B> {
  private A first;
  private B second;

  public Pair(A first, B second) {
    this.first = first;
    this.second = second;
  }

  public A getFirst() {
    return this.first;
  }

  public B getSecond() {
    return this.second;
  }
}

Comparable og generiske typer i grensesnitt

Man kan benytte generiske typer i grensesnitt. Dette gjøres også i Java-biblioteket. Et eksempel på dette er grensesnittet Comparable i Java sitt standardbibliotek, som angir at det alltid må finnes en compareTo-metode i de klassene som implementerer grensesnittet. Hva slags type parameteren til denne metoden skal ha, er opp til klassen som implementerer grensesnittet å angi i sin egen klassedeklarasjon.

public interface Comparable <T> {
  /**
  * Compares this object with another, and returns a numerical result based
  * on the comparison. If this object is considered to sort before the other
  * object, this method returns -1 (or another negative value). If this
  * object is considered to sort equal to the other object, the method
  * returns 0. If this object is considered to sort after the other object,
  * the method returns 1 (or another positive value).

  * @param other the object to be compared
  * @return an integer describing the comparison
  */
  int compareTo(T other);
}

Dette grensesnittet implementeres for eksempel av klassene Integer og Double (også dem fra Java sitt standardbibliotek).

Klasser som implementerer generiske grensesnittet angir i sin egen klassedeklarasjon hvilken type parameter grensesnittet skal ha. For eksempel kan vi lage en klasse House som implementerer grensesnittet Comparable<House>. Da må compareTo-metoden ha House som parametertype:

public class House implements Comparable<House> {
  private double squareMeters;

  public House(double squareMeters) {
    this.squareMeters = squareMeters;
  }

  @Override
  public int compareTo(House that) {
    if (this.squareMeters == that.squareMeters) {
      return 0;
    } else if (this.squareMeters < that.squareMeters) {
      return -1;
    } else {
      return 1;
    }
  }
}

Siden House implementerer grensesnittet Comparable<House>, betyr det at vi kan benytte oss av følgende klientkode for å sammenligne to House-objekter:

// Klientkode
House a = new House(100);
House b = new House(120);
int result = a.compareTo(b); // 0 hvis a == b, -1 hvis a < b, og 1 hvis a > b

Generiske typer i metoder

I stedet for at den generiske typen gjelder for en hel klasse, er det også mulig at den gjelder kun for en metode. Da angis typeparameterne med <> mellom metode-modifikatorene (private/public/static etc) og returtypen. Minst en av parameterne til metoden må benytte seg av typeparameteren i typen sin. Typeargumentet, bestemt av klientkoden som kaller på metoden, vil tolkes automatisk basert på hvilken type argumentet har.

import java.util.HashMap;
import java.util.List;
import java.util.Arrays;

public class Main {
  // Klientkode
  public static void main(String[] args) {
    // String
    List<String> a = Arrays.asList("Foo", "Bar", "Foo", "Baz");
    String mostCommonString = mostCommonElement(a);
    System.out.println(mostCommonString);

    // Integer
    List<Integer> b = Arrays.asList(1, 2, 2, 2, 3);
    Integer mostCommonInt = mostCommonElement(b);
    System.out.println(mostCommonInt);
  }

  // Implementasjonskode - generisk type T kun tilgjengelig i denne metoden
  public static <T> T mostCommonElement(List<T> a) {
    HashMap<T, Integer> counts = new HashMap<>();
    int maxCount = 0;
    T maxElement = null;
    for (T x : a) {
      if (counts.containsKey(x)) {
        counts.put(x, counts.get(x) + 1);
      } else {
        counts.put(x, 1);
      }
      if (counts.get(x) > maxCount) {
        maxCount = counts.get(x);
        maxElement = x;
      }
    }
    return maxElement;
  }
}

Kalle metoder på generisk typede variabler <T extends Foo>

Av og til kan man ønske å kalle en metode på en variabel med en generisk type. Problemet er at vi ikke vet hvilken klasse objektet egentlig er, og dermed aner vi ikke hvilke metoder som er tilgjengelige. Vi kan løse dette ved å legge til noen ekstra begrensninger på den generiske typen, for eksempel at den må implementere et gitt grensesnitt.

La oss si at vi har et grensesnitt HasArea som har en metode double area(), samt to klasser House og Circle som implementerer grensesnittet. Vi ønsker å skrive kode med en generisk type som virker for både House og Circle -objekter, men vi har behov for å kalle på area() -metoden. For eksempel en metode som tar inn en liste med objekter og returnerer elementet med største areal:

// Klientkode
List<House> houses = Arrays.asList(
    new House(100), new House(120), new House(80));
House biggestHouse = largestArea(houses);

List<Circle> circles = Arrays.asList(
    new Circle(10.5), new Circle(20), new Circle(5));
Circle biggestCircle = largestArea(circles);
// Implementasjonskode: bruker generisk type <T extends HasArea>
public <T extends HasArea> T largestArea(List<T> a) {
  T largest = a.get(0);
  for (T x : a) {
    if (x.area() > largest.area()) {  // Vi kaller area()-metoden på typen T!
      largest = x;
    }
  }
  return largest;
}

Dersom vi i krokodilleklammene for typeparameteren angir <T extends HasArea>, innebærer dette at T må være en undertype av HasArea i typehierarkiet. Typen T kan altså være HasArea-typen selv, eller en annen type som direkte eller indirekte arver eller implementerer grensesnittet HasArea. I implementasjonskoden gir det oss større muligheter ved at vi kan benytte T som om det var en HasArea-type, mens i klientkoden gir det oss større begresninger ved at kun undertyper av HasArea kan brukes som typeparameter.

import java.util.List;
import java.util.Arrays;

interface HasArea {
  double area();
}

class House implements HasArea {
  private double squareMeters;

  public House(double squareMeters) {
    this.squareMeters = squareMeters;
  }

  @Override
  public double area() {
    return squareMeters;
  }
}

class Circle implements HasArea {
  private double radius;

  public Circle(double radius) {
    this.radius = radius;
  }

  @Override
  public double area() {
    return Math.PI * radius * radius;
  }
}

public class Main {

  // Klientkode
  public static void main(String[] args) {
    List<House> houses = Arrays.asList(
        new House(100), new House(120), new House(80));
    House biggestHouse = largestArea(houses);

    List<Circle> circles = Arrays.asList(
        new Circle(10.5), new Circle(20), new Circle(5));
    Circle biggestCircle = largestArea(circles);
  }

  // Implementasjonskode: bruker generisk type <T extends HasArea>
  public static <T extends HasArea> T largestArea(List<T> a) {
    T largest = a.get(0);
    for (T x : a) {
      if (x.area() > largest.area()) { // Vi kaller area()-metoden på typen T!
        largest = x;
      }
    }
    return largest;
  }
}

Eksempel: største element i en liste <T extends Comparable<T>>

La oss si at vi skriver en metode for å finne det største heltallet i en liste:

public Integer largest(List<Integer> a) {
  Integer largest = a.get(0);
  for (Integer x : a) {
    if (x.compareTo(largest) > 0) {
      largest = x;
    }
  }
  return largest;
}

Så ønsker vi å finne det største tallet i en liste med flyttall. Det hadde vært fint om vi kunne benytte metoden vi allerede hadde skrevet, men alas, den fungerer ikke for andre typer enn Integer. Vi skriver en ny metode:

public Double largest(List<Double> a) {
  Double largest = a.get(0);
  for (Double x : a) {
    if (x.compareTo(largest) > 0) {
      largest = x;
    }
  }
  return largest;
}

Den eneste forskjellen mellom metodene er at den ene tar inn en liste med Integer og den andre en liste med Double. Finnes det en mulighet for å gjenbruke koden uten å skrive den to ganger? Kan koden brukes for å finne «den største» for enhver type det er mulig å sortere?

Med en litt fiffig bruk av restriksjoner i typeparameteren kan vi begrense den generiske typen slik at den må kunne sammenlignes med seg selv: <T extends Comparable<T>> innebærer altså at T kan være hvilken som helst type så lenge den implementerer Comparable mot seg selv:

import java.util.List;
import java.util.Arrays;

public class Main {
  // Klientkode
  public static void main(String[] args) {
    List<Integer> a = Arrays.asList(1, 2, 3, 4, 5);
    List<Double> b = Arrays.asList(0.5, 0.4, 0.3, 0.2, 0.1);
    System.out.println(largest(a));
    System.out.println(largest(b));
  }

  // Implementasjonskode
  public static <T extends Comparable<T>> T largest(List<T> a) {
    T largest = a.get(0);
    for (T x : a) {
      if (x.compareTo(largest) > 0) {
        largest = x;
      }
    }
    return largest;
  }
}

Generiske subtyper <? extends Foo>

I Java er Integer og Double subtyper av Number. Derfor kan vi legge til både Integer og Double -objekter i en liste av Number-objekter:

List<Number> a = new ArrayList<>();
Integer x = 42;
a.add(x);
Double pi = 3.14;
a.add(pi);

Det er derimot ikke tilfelle at List<Integer> og List<Double> er subtyper av List<Number>. For eksempel vil eksempelet under ikke fungere:

import java.util.ArrayList;
import java.util.List;

public class Main {
  public static void main(String[] args) {
    List<Integer> a = new ArrayList<>();
    a.add(42);
    a.add(95);
    Number largestA = getLargest(a);
    System.out.println(largestA);

    List<Double> b = new ArrayList<>();
    b.add(0.42);
    b.add(0.95);
    Number largestB = getLargest(b);
    System.out.println(largestB);
  }

  public static Number getLargest(List<Number> a) {
    Number largest = null;
    for (Number num : a) {
      if (largest == null || largest.doubleValue() < num.doubleValue()) {
        largest = num;
      }
    }
    return largest;
  }
}

Grunnen til at koden ikke fungerer, er fordi List<Integer> og List<Double> ikke er en subtyper av List<Number>. Når man tenker litt nærmere over det, gir dette fullstendig mening:

At samme liste har begge disse egenskapene samtidig er umulig.

I og med at getLargest -metoden over aldri legger til nye objekter i listen, er vi riktignok ikke i den umulige situasjonen akkurat her. Lykkelig nok er det faktisk mulig å endre typeargumentet til parameteren i getLargest slik at den aksepterer både List<Integer> og List<Double> -typer som argument. Dette gjøres med et wildcard ?:

public static Number getLargest(List<? extends Number> a) {
  // ...
}

Utsagnet <? extends Foo> kan tolkes til noe sånt som «hvilken type som helst, bare typen er en undertype av Foo». Denne tolkningen minner mistenkelig om tolkningen av <T extends Foo> i avsnittet om å kalle metoder på generiske typer; men wildcardet ? brukes for i klientkoden å angi et typeargument, ikke for å angi en typeparameter i implementasjonskoden.