Using a Pair/Tuple type in Java

ยท

4 min read

At times I find myself wanting to use a type such as tuple in Python (or Scala) or the std::pair from C++ to represent a listing of homogenous or heterogenous values e.g. coordinates (x,y), a value along with its frequency ('b',2) or some other type of state involving multiple values. There's 4 different ways I've found to accomplish this in Java without needing external libraries (mostly).

  1. Custom class

  2. Map.Entry

  3. Records (JDK 14+)

  4. javafx.util.Pair class (JDK 11 and below)

Note: the same simple example will be used throughout - returning the average score along with letter grade from a student's test scores. Also text blocks for printing output, which are included from JDK 15+

Custom Class

Define a class containing the amount of items (2 in this case for a pair) and their types.

// Definition
class Tuple<K,V> {
    private final K first;
    private final V second;

    public Tuple(K first, V second){
        this.first = first;
        this.second = second;
    }

    public K first(){return first;}
    public V second(){return second;}
    // other methods, etc ...
}

// Usage
class Main {
    public static void main(String[] args) {
        var studentId = "123-abc";
        var studentScores = fetchScores(studentId);
        var gradeTuple = calcGrade(studentScores);
        var msg = "Grade for student %s: %s(%.2f)".formatted(studentId, gradeTuple.first(), gradeTuple.second());
        System.out.println(msg);
        // e.g. Grade for student 123-abc: A(95.5)
    }

    public Pair<String, Double> calcGrade(List<Double> studentScores) {
        double avg = studentScores.stream()
            .mapToDouble(Double::doubleValue)
            .average().getAsDouble();
        String letterGrade = getGradeFor(score);
        return new Tuple<String, Double>(letterGrade, avg);
    }
}

Pros ๐Ÿ‘

  • Relatively quick to define a custom class, and you can customize it exactly to your needs.

Cons ๐Ÿ‘Ž

  • Compared to a record, there will be some extra effort needed to implement methods such as equals(), hashcode(), etc (if class instances need to be compared or sorted)

Map.Entry

Utilize the Map.Entry interface (a nested interface of Map). It represents a single entry from a map i.e. a (key, value) "pairing". In the example below I use the static Map.entry(key,value) method to return an implementation.

class Main {
    public static void main(String[] args) {
        var studentId = "123-abc";
        var studentScores = fetchScores(studentId);
        var gradeEntry = calcGrade(studentScores);
        var msg = "Grade for student %s: %s(%.2f)".formatted(studentId, gradeEntry.getKey(), gradeEntry.getValue());
        // e.g. Grade for student 123-abc: A(95.5)
        System.out.println(msg);
    }

    public Map.Entry<String, Double> calcGrade(List<Double> studentScores) {
        double avg = studentScores.stream()
            .mapToDouble(Double::doubleValue)
            .average().getAsDouble();
        String letterGrade = getGradeFor(score);
        return Map.entry(letterGrade, avg);
    }
}

Pros ๐Ÿ‘

  • Map.Entry is included in every JDK and there's no need to create additional classes

Cons ๐Ÿ‘Ž

  • The semantics might be a little "odd" if what you are trying to model does not have a key/value relationship

Records

Use the record class introduced in JDK 14. It allows for the quick creation of immutable classes that include toString(), hashCode(), and equals() "out of the box". They can be declared locally, or as class variables.

class Main {
    record Tuple<K,V>(K first, V second){}

    public static void main(String[] args) {
        var studentId = "123-abc";
        var studentScores = fetchScores(studentId);
        var gradeTuple = calcGrade(studentScores);
        var msg = "Grade for student %s: %s(%.2f)".formatted(studentId, gradeTuple.first(), gradeTuple.second());
        // e.g. Grade for student 123-abc: A(95.5)
        System.out.println(msg);
    }

    public Map.Entry<String, Double> calcGrade(List<Double> studentScores) {
        double avg = studentScores.stream()
            .mapToDouble(Double::doubleValue)
            .average().getAsDouble();
        String letterGrade = getGradeFor(score);
        return new Tuple<String, Double>(letterGrade, avg);
    }
}

Pros ๐Ÿ‘

  • Much less code needs to be written compared to defining a custom class

  • Records are immutable by default (helps avoid making unintended changes)

Cons ๐Ÿ‘Ž

  • Records aren't available in Java versions older than 14

javafx.util.Pair

Use the Pair class from the javafx package, which was included in the JDK until version 11.

class Main {
    public static void main(String[] args) {
        var studentId = "123-abc";
        var studentScores = fetchScores(studentId);
        var gradePair = calcGrade(studentScores);
        var msg = "Grade for student %s: %s(%.2f)".formatted(studentId, gradeTuple.first(), gradeTuple.second());
        // e.g. Grade for student 123-abc: A(95.5)
        System.out.println(msg);
    }

    public Pair<String, Double> calcGrade(List<Double> studentScores) {
        double avg = studentScores.stream()
            .mapToDouble(Double::doubleValue)
            .average().getAsDouble();
        String letterGrade = getGradeFor(score);
        return new Pair<String, Double>(letterGrade, avg);
    }
}

Pros ๐Ÿ‘

  • Functionality provided out of the box

  • Built-in equals(), toString(), etc

Cons ๐Ÿ‘Ž

  • Pair class is not included in JDK versions after 11
ย