functional-reactive
Made in the European Union

Tuples (Pair…Sept)

Type-safe N-ary tuples — both as legacy nested classes and as modern Java records.

functional-reactive ships two parallel tuple flavours so you can pick whichever fits your JDK target and style:

  • Records under com.svenruppert.functional.tuple — proper Java 14+ records, component accessors t1()/t2()/…
  • Classic classes nested in com.svenruppert.functional.model.DataRecords — JDK-8-compatible classes with getT1()/getT2()/… getters.

Pick records for new code on JDK 14+. Pick DataRecords when targeting JDK 8/11 or when you need to extend a tuple with domain methods (records cannot be extended).

Tuple sizes

Both packages expose: Single, Pair, Triple, Quad, Quint, Sext, Sept — 1- through 7-tuple.

Records (modern)

import com.svenruppert.functional.tuple.Pair;
import com.svenruppert.functional.tuple.Triple;

Pair<String, Integer> entry = new Pair<>("answer", 42);
entry.t1();   // "answer"
entry.t2();   // 42

Triple<String, Integer, Boolean> row = new Triple<>("id", 7, true);
row.t1();     // "id"

Because they are records, you also get equals, hashCode, toString, and full destructuring with Java 21+ pattern matching for free:

if (entry instanceof Pair<String, Integer>(var key, var value)) {
    // use key, value directly
}

DataRecords (classic)

import com.svenruppert.functional.model.DataRecords;

DataRecords.Pair<String, Integer> entry =
    new DataRecords.Pair<>("answer", 42);

entry.getT1();   // "answer"
entry.getT2();   // 42

Each class also offers a static next(...) factory if you prefer that style:

DataRecords.Pair.next("answer", 42);
DataRecords.Triple.next("id", 7, true);

equals, hashCode and toString are implemented manually inside the classes, so behaviour matches the record variant.

Serializable variants

For tuples that need to cross a serialization boundary (RMI, GWT, Vaadin push, etc.), the classic flavour also exists in a Serializable form:

import com.svenruppert.functional.model.DataRecordsSerializable;

DataRecordsSerializable.Pair<String, Integer> p =
    new DataRecordsSerializable.Pair<>("k", 1);

The type parameters are bounded by extends Serializable, so the compiler enforces that everything inside can actually travel.

When to use which

Decision tree
  • JDK 14+, no inheritance needed → records (functional.tuple.*).
  • Need to extend a tuple with domain getters (e.g. class Car extends Quint<…>) → classic (DataRecords.*).
  • Crossing a serialization boundary → DataRecordsSerializable.*.

Extending tuples for domain semantics

A common pattern with the classic flavour is to extend a tuple to give it meaningful accessors:

public class Car extends DataRecords.Quint<String, Colour, Integer, Integer, Float> {
    public Car(String brand, Colour colour, Integer speed, Integer year, Float price) {
        super(brand, colour, speed, year, price);
    }
    public String  brand()  { return getT1(); }
    public Colour  colour() { return getT2(); }
    public Integer speed()  { return getT3(); }
    public Integer year()   { return getT4(); }
    public Float   price()  { return getT5(); }
}

Records do not allow this — they are implicitly final. If you need a domain-typed accessor with a record, write a wrapper record instead.