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. toStringproduces 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
mapsteps. - The grouping is anonymous and local — it never appears in a public API or crosses a module boundary.
Creating instances
// Tuple2 — a pairTuple2<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 NullPointerExceptionTuple3.of("Alice", null, true); // throws NullPointerExceptionAccessing 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(); // 30boolean active = t3._3(); // true
Tuple4<String, Integer, Boolean, Double> t4 = Tuple4.of("Alice", 30, true, 1.75);double height = t4._4(); // 1.75Transforming 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 unchangedTuple3<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 mapFourthTuple4<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 TriFunctionTuple3<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 QuadFunctionTuple4<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 equalityint 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
| Scenario | Recommendation |
|---|---|
| Anonymous intermediate value in a private method or pipeline | Tuple2 / Tuple3 / Tuple4 |
| Two values returned from a private helper | Tuple2 |
| Named, stable domain concept used in a public API | Java record with named fields |
| Type appears in multiple modules or across service boundaries | Java record with named fields |
| More than four elements | Java 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 invalidMap<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 reportingList<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 togetherTuple3<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()));