Tuples

Runnable example: TupleSample.java

What are Tuples?

Tuple2<A,B>, Tuple3<A,B,C>, and Tuple4<A,B,C,D> are immutable, type-safe groupings of 2, 3, or 4 heterogeneous values without requiring a named class or record.

All three are Java record types, which means:

  • Structural equality (equals/hashCode) is derived automatically.
  • toString produces a readable representation with field names.
  • Fields are exposed via accessor methods _1(), _2(), _3(), _4() — consistent across all tuple arities.

Null policy: Tuple3 and Tuple4 are @NullMarked — null elements throw NullPointerException at construction time. Tuple2 is a plain record without null guards; prefer Tuple3/Tuple4 when null-safety is required.

Use Tuples when:

  • You need to return two or more values from a private or intermediate method without declaring a dedicated type.
  • You are building a stream pipeline and need to carry context (e.g., original value + computed result) through a chain of map steps.
  • The grouping is anonymous and local — it never appears in a public API or crosses a module boundary.

Creating instances

// Tuple2 — a pair
Tuple2<String, Integer> t2 = new Tuple2<>("Alice", 30);
// Tuple3 — a triple (null-guarded factory method)
Tuple3<String, Integer, Boolean> t3 = Tuple3.of("Alice", 30, true);
// Tuple4 — a quadruple (null-guarded factory method)
Tuple4<String, Integer, Boolean, Double> t4 = Tuple4.of("Alice", 30, true, 1.75);
// All fields are @NullMarked — null arguments throw NullPointerException
Tuple3.of("Alice", null, true); // throws NullPointerException

Accessing fields

Fields are accessed via zero-argument methods _1(), _2(), _3(), and _4().

Tuple2<String, Integer> t2 = new Tuple2<>("Alice", 30);
String name = t2._1(); // "Alice"
int age = t2._2(); // 30
Tuple3<String, Integer, Boolean> t3 = Tuple3.of("Alice", 30, true);
String n2 = t3._1(); // "Alice"
int age3 = t3._2(); // 30
boolean active = t3._3(); // true
Tuple4<String, Integer, Boolean, Double> t4 = Tuple4.of("Alice", 30, true, 1.75);
double height = t4._4(); // 1.75

Transforming individual slots

Tuple3 and Tuple4 provide mapFirst, mapSecond, mapThird (and mapFourth for Tuple4). Each returns a new tuple with the targeted slot replaced; the remaining slots are carried through unchanged.

// Tuple3 — transform individual slots; the others are unchanged
Tuple3<String, Integer, Boolean> t3 = Tuple3.of("alice", 30, true);
Tuple3<String, Integer, Boolean> upper = t3.mapFirst(String::toUpperCase); // ("ALICE", 30, true)
Tuple3<String, Integer, Boolean> older = t3.mapSecond(age -> age + 1); // ("alice", 31, true)
Tuple3<String, Integer, Boolean> toggled = t3.mapThird(b -> !b); // ("alice", 30, false)
// Tuple4 — additionally has mapFourth
Tuple4<String, Integer, Boolean, Double> t4 = Tuple4.of("alice", 30, true, 1.75);
Tuple4<String, Integer, Boolean, Double> taller = t4.mapFourth(h -> h + 0.1); // (..., 1.85)

Collapsing to a single value — map

Tuple3.map(TriFunction) and Tuple4.map(QuadFunction) collapse the entire tuple into a single result by applying a function to all slots at once.

// Tuple3.map — collapses three elements into one value via TriFunction
Tuple3<String, Integer, Boolean> t3 = Tuple3.of("Alice", 30, true);
String label = t3.map((name, age, active) ->
name + " (age " + age + ")" + (active ? " ✓" : ""));
// "Alice (age 30) ✓"
// Tuple4.map — collapses four elements into one value via QuadFunction
Tuple4<String, Integer, Boolean, Double> t4 = Tuple4.of("Alice", 30, true, 1.75);
String summary = t4.map((name, age, active, height) ->
name + " | " + age + "y | " + height + "m | " + (active ? "active" : "inactive"));
// "Alice | 30y | 1.75m | active"

TriFunction<A,B,C,R> and QuadFunction<A,B,C,D,R> are functional interfaces provided by the library, analogous to java.util.function.BiFunction.

Equality and toString

// Tuple types are records — equality, hashCode, and toString are derived automatically.
Tuple2<String, Integer> a = new Tuple2<>("Alice", 30);
Tuple2<String, Integer> b = new Tuple2<>("Alice", 30);
boolean same = a.equals(b); // true — structural equality
int hash = a.hashCode(); // consistent with equals
String repr = Tuple3.of("Alice", 30, true).toString();
// "Tuple3[_1=Alice, _2=30, _3=true]"

When to use Tuples vs Records

ScenarioRecommendation
Anonymous intermediate value in a private method or pipelineTuple2 / Tuple3 / Tuple4
Two values returned from a private helperTuple2
Named, stable domain concept used in a public APIJava record with named fields
Type appears in multiple modules or across service boundariesJava record with named fields
More than four elementsJava record with named fields

The key rule: if you would name it, use a record. Tuples are for ephemeral, local groupings where the fields are obvious from context.

Real-world example

Using Tuple2 and Tuple3 to carry multiple values through a stream pipeline without declaring intermediate named types.

// Pair each user with their validation result inside a stream pipeline.
// Tuple2 keeps the original value and its outcome together without a named class.
List<Tuple2<User, ValidationResult>> results = users.stream()
.map(user -> new Tuple2<>(user, validator.validate(user)))
.collect(Collectors.toList());
// Partition into valid and invalid
Map<Boolean, List<Tuple2<User, ValidationResult>>> partitioned = results.stream()
.collect(Collectors.partitioningBy(t -> t._2().isValid()));
List<Tuple2<User, ValidationResult>> valid = partitioned.get(true);
List<Tuple2<User, ValidationResult>> invalid = partitioned.get(false);
// Collapse each pair to a summary line for reporting
List<String> report = invalid.stream()
.map(t -> t.map((user, res) ->
user.email() + ": " + String.join(", ", res.errors()))) // Tuple2 has no map() yet — use fields
.collect(Collectors.toList());
// Simpler with Tuple3: carry user, result, and timestamp together
Tuple3<User, ValidationResult, Instant> snapshot =
Tuple3.of(user, validator.validate(user), Instant.now());
String audit = snapshot.map((u, r, ts) ->
"[" + ts + "] " + u.email() + " → " + (r.isValid() ? "OK" : r.errors()));