A tuple is a fixed-size, ordered group of values that can have different types, such as a name and the years of experience kept together as (“Sajal”, 12). Java has no built-in tuple type, so we write a record (Java 16 and later) for a tuple with named parts, call Map.entry() for a quick pair, use Pair or Triple from Apache Commons Lang, or add the javatuples library for the classes Pair to Decade.
We use a tuple when a method must return two or more values, or when we need a compound key for a Map. For example, a method that scans a list of prices returns both the lowest and the highest price.
The following example creates the same tuple with and without a library. The result of each line is in the comment.
// 1. Record (no library)
record Employee(String name, int experience) {}
Employee employee = new Employee("Sajal", 12); // Employee[name=Sajal, experience=12]
String name = employee.name(); // "Sajal"
// 2. Map.entry() for a pair (no library)
Map.Entry<String, Integer> entry = Map.entry("Sajal", 12); // Sajal=12
Integer exp = entry.getValue(); // 12
// 3. javatuples library
Pair<String, Integer> pair = Pair.with("Sajal", 12); // [Sajal, 12]
String first = pair.getValue0(); // "Sajal"
Triplet<String, Integer, String> triplet = pair.add("IT Professional"); // [Sajal, 12, IT Professional]
Pair<String, Integer> changed = pair.setAt0("Kajal"); // [Kajal, 12] (pair stays [Sajal, 12])
Notice that none of the three tuples can change. The method setAt0() returns a new Pair and leaves the original one as it was.
Next, we compare the built-in options with a custom class. After that, we go through the javatuples operations one by one, from creating a tuple to converting it into a List, and we close with a comparison table.
1. What is a Tuple?
A tuple is an immutable object that holds a fixed number of values, and the values can have different types. The pieces of contained information do not necessarily relate to each other in any way, but collectively these will have some meaning.
For example, [“Sajal Chakraborty”, “IT Professional”, 32] can be a tuple where each value inside the tuple does not have any relation, but this whole set of values can represent an actor in the application.
Each line of the Sample Tuples block is one tuple. The values in a tuple have a position, so the first value of [“Alex”, 32, “New York”, true] is always the name.
["Java", 1.8, "Windows"]
["Alex", 32, "New York", true]
[3, "Alexa", "howtodoinjava.com", 37000]
A tuple differs from a List in two ways. A List can grow and shrink, whereas a tuple has a fixed size. A List<String> holds values of one type, whereas a tuple keeps the type of each position, so the compiler knows that position 0 is a String and position 1 is an Integer.
2. Tuples in Java
By default, Java doesn’t have any built-in data structure to support tuples. The JDK has no Tuple or Pair class in java.util, so we choose between a class that we write, a record, a Map.Entry or a library.
2.1. A Custom Tuple Class
Before records, we wrote a small generic class that stores two values of generic types. The class has no setter methods, and its fields are final, so a tuple cannot change after we create it.
class Tuple<A, B> {
private final A first;
private final B second;
public Tuple(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() {
return first;
}
public B getSecond() {
return second;
}
}
We can use this class to store any two unrelated values, such as an Integer and a String.
Tuple<Integer, String> myTuple = new Tuple<>(1, "apple");
Integer number = myTuple.getFirst(); // 1
String fruit = myTuple.getSecond(); // "apple"
The class works, but it does not override equals() and hashCode(). So two tuples with the same values are not equal, and a Tuple used as a HashMap key never finds its entry again. We would have to write both methods ourselves, together with toString(), and a record generates all of them for us.
2.2. Records as Named Tuples
A record is a class that holds a fixed set of values, and the compiler generates the constructor, the accessor methods, equals(), hashCode() and toString(). Records became final in Java 16. In current Java, a record is the best choice for a tuple, because each value has a name and a type, and no library is needed.
record Employee(String name, int experience) {}
Employee sajal = new Employee("Sajal", 12);
String name = sajal.name(); // "Sajal"
int experience = sajal.experience(); // 12
String text = sajal.toString(); // "Employee[name=Sajal, experience=12]"
boolean same = sajal.equals(new Employee("Sajal", 12)); // true
A record is the cleanest way to return more than one value from a method. For example, a price report needs both the lowest and the highest value of a list. The method returns a MinMax record, so the caller reads min() and max() by name instead of getValue0() and getValue1().
record MinMax(int min, int max) {}
static MinMax minMax(List<Integer> values) {
int min = values.stream().mapToInt(Integer::intValue).min().orElseThrow();
int max = values.stream().mapToInt(Integer::intValue).max().orElseThrow();
return new MinMax(min, max);
}
MinMax range = minMax(List.of(5, 3, 8)); // MinMax[min=3, max=8]
For an empty list, orElseThrow() throws a NoSuchElementException, so we check values.isEmpty() before the call when the list can be empty.
Since Java 21, a record pattern takes a record apart into variables in one step, in the same way that tuples are unpacked in other languages.
Object obj = sajal;
if (obj instanceof Employee(String n, int e)) {
System.out.println(n + " " + e); // Sajal 12
}
Records have equals() and hashCode() based on all their values, so a record works as a compound HashMap key.
Map<Employee, String> teams = new HashMap<>();
teams.put(new Employee("Sajal", 12), "Payments");
String team = teams.get(new Employee("Sajal", 12)); // "Payments"
2.3. Map.entry() for a Quick Pair
When we need a pair only for a moment, for example as an element of a stream, the static method Map.entry()) (Java 9 and later) creates an unmodifiable key-value pair.
Map.Entry<String, Integer> entry = Map.entry("Sajal", 12); // Sajal=12
String key = entry.getKey(); // "Sajal"
Integer value = entry.getValue(); // 12
Integer old = entry.setValue(15); // UnsupportedOperationException
Map.Entry<String, Integer> noValue = Map.entry("Sajal", null); // NullPointerException
The method Map.entry() rejects null for both the key and the value. When a value can be null, we create an AbstractMap.SimpleImmutableEntry instead.
Map.Entry<String, Integer> entry = new AbstractMap.SimpleImmutableEntry<>("Sajal", null); // Sajal=null
2.4. Pair and Triple in Apache Commons Lang
Projects that already use Apache Commons Lang get the classes Pair and Triple in the package org.apache.commons.lang3.tuple. The method of() returns an immutable instance. We read the values with getLeft() and getRight(), and a Triple also has getMiddle().
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.21.0</version>
</dependency>
Pair<String, Integer> pair = Pair.of("Sajal", 12); // (Sajal,12)
String left = pair.getLeft(); // "Sajal"
Triple<String, Integer, String> triple = Triple.of("Sajal", 12, "Pune"); // (Sajal,12,Pune)
Integer middle = triple.getMiddle(); // 12
Commons Lang stops at three values. For tuples with up to ten values and operations such as adding a value, we use javatuples.
3. Javatuples Library
The javatuples library has one class per tuple size. Its last release, 1.2, is from 2011, so the library gets no new features or fixes. The API is small and stable, and it runs without problems on Java 25.
3.1. Maven
The javatuples library is on Maven Central, and we add this dependency to use it.
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
3.2. Core Classes
Javatuples supports tuples of size up to 10, and for each size, it has one class in the package org.javatuples.
| Class | Number of values |
|---|---|
| Unit | 1 |
| Pair | 2 |
| Triplet | 3 |
| Quartet | 4 |
| Quintet | 5 |
| Sextet | 6 |
| Septet | 7 |
| Octet | 8 |
| Ennead | 9 |
| Decade | 10 |
The library has two more classes with two values, KeyValue and LabelValue. They work like Pair, but their getters have names that say what each value means, as we see in section 4.2.2.
4. Operations On Tuples
All javatuples operations return values or new tuples, because a javatuples object never changes. The type of each position is part of the generic type, e.g. Pair<String, Integer>.
4.1. Creating a Tuple
4.1.1. Factory Methods
The static factory method with() of each tuple class creates a tuple from its values. The compiler infers the generic types from the arguments.
Pair<String, Integer> pair = Pair.with("Sajal", 12); // [Sajal, 12]
Quartet<String, Integer, String, Double> quartet1 = Quartet.with("A1", 1, "A3", 2.3); // [A1, 1, A3, 2.3]
4.1.2. Constructor
We can also use the constructor of Pair. It creates the same tuple as with(), so the two pairs are equal.
Pair<String, Integer> person = new Pair<>("Sajal", 12); // [Sajal, 12]
boolean equal = person.equals(pair); // true
4.1.3. From Collection or Iterable
We can create tuples from a Collection or an Iterable. The method fromCollection() needs a collection with the exact number of values of the tuple. The method fromIterable(iterable, index) starts at the given index and takes as many values as the tuple needs. When fewer values are left, the missing positions are null, so Pair.fromIterable(listOf4Names, 3) returns [A4, null].
All values come from one collection, so all positions get the same type, e.g. Quartet<String, String, String, String>.
List<String> listOf4Names = Arrays.asList("A1", "A2", "A3", "A4");
Quartet<String, String, String, String> quartet = Quartet.fromCollection(listOf4Names); // [A1, A2, A3, A4]
Pair<String, String> pair1 = Pair.fromIterable(listOf4Names, 2); // [A3, A4]
Pair<String, String> wrongSize = Pair.fromCollection(listOf4Names);
// IllegalArgumentException: Iterable must have exactly 2 available elements in order to create a Pair.
4.2. Getting Values from Tuple
4.2.1. getValueX() Methods
We can get the values from the tuples by using the indexed getValueX() methods, where X is the position of the value inside the tuple, starting at 0. For example, getValue0() and getValue1() exist on a Pair.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
String name = pair.getValue0(); // "Sajal"
Integer exp = pair.getValue1(); // 12
Please note that these getValueX() methods are type-safe. It means the compiler already knows the method return type based on the element values used to initialize the tuple.
4.2.2. getValue(int index) Method
Tuples have another method, getValue(int index), which is not type-safe. It returns an Object, so we cast the value to the expected type. A wrong cast throws a ClassCastException at runtime, and a position outside the tuple throws an IllegalArgumentException.
Object first = pair.getValue(0); // "Sajal"
String name = (String) pair.getValue(0); // "Sajal"
Integer wrong = (Integer) pair.getValue(0); // ClassCastException
Object none = pair.getValue(2); // IllegalArgumentException: Cannot retrieve position 2 in Pair. ...
So we use getValue(int) only in generic code that loops over positions, and getValueX() everywhere else.
The classes KeyValue and LabelValue have the methods getKey()/getValue() and getLabel()/getValue().
KeyValue<String, Integer> kv = KeyValue.with("Sajal", 12);
String key = kv.getKey(); // "Sajal"
LabelValue<String, Integer> lv = LabelValue.with("Exp", 12);
String label = lv.getLabel(); // "Exp"
4.3. Setting Values in Tuple
The setAtX() methods replace the value at position X. Tuples are immutable, so setAtX() returns a new tuple of the same type with the new value, and the original tuple is unchanged.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Pair<String, Integer> modifiedPair = pair.setAt0("Kajal");
System.out.println(pair); // [Sajal, 12]
System.out.println(modifiedPair); // [Kajal, 12]
A call to setAt0() without using the returned tuple does nothing, because the original tuple never changes. The same rule applies to add() and removeFromX() in the next section.
4.4. Adding and Removing Values
4.4.1. add() Method
The method add() returns a new tuple with one more position. For example, adding a value to a Pair returns a Triplet. The new element goes at the end of the tuple.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Triplet<String, Integer, String> triplet = pair.add("IT Professional"); // [Sajal, 12, IT Professional]
We can add one tuple object to another tuple as well. The result type depends on the total number of values, e.g. a Quartet plus a Triplet gives a Septet.
Triplet<String, String, String> triplet = Triplet.with("Java", "C", "C++"); // [Java, C, C++]
Quartet<String, String, String, String> quartet = triplet.addAt1("Python"); // [Java, Python, C, C++]
Septet<String, String, String, String, String, String, String> septet = quartet.add(triplet);
// [Java, Python, C, C++, Java, C, C++]
4.4.2. addAtX() Method
By default, new elements are added at the end of the tuple. The addAtX() methods insert the value at position X instead, and the values after it move one position to the right. In the previous snippet, addAt1(“Python”) put “Python” at position 1.
Triplet<String, String, String> triplet = Triplet.with("Java", "C", "C++");
Quartet<String, String, String, String> quartet = triplet.addAt1("Python"); // [Java, Python, C, C++]
4.4.3. removeFromX() Method
The removeFromX() methods return a smaller tuple without the value at position X. For example, removing a value from a Triplet returns a Pair.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Triplet<String, Integer, String> triplet = pair.add("IT Professional"); // [Sajal, 12, IT Professional]
Unit<Integer> onlyExp = pair.removeFrom0(); // [12]
Pair<String, Integer> back = triplet.removeFrom2(); // [Sajal, 12]
4.5. Converting Tuple to Collection or Array
Each tuple class has the methods toList() and toArray(), which return a List<Object> and an Object[]. The result type uses Object, because a tuple can hold values of different types.
Quartet<String, Integer, String, Double> quartet1 = Quartet.with("A1", 1, "A3", 2.3);
List<Object> quartetList = quartet1.toList(); // [A1, 1, A3, 2.3]
Object[] quartetArr = quartet1.toArray(); // [A1, 1, A3, 2.3]
String text = Arrays.toString(quartetArr); // "[A1, 1, A3, 2.3]"
4.6. Iterating over Tuple Values
All tuple classes implement the Iterable<Object> interface, so a for-each loop goes over the values in position order, the same as with a collection.
Quartet<String, Integer, String, Double> quartet1 = Quartet.with("A1", 1, "A3", 2.3);
for (Object obj : quartet1) {
System.out.println(obj);
}
The program prints one value per line.
A1
1
A3
2.3
4.7. More Tuple Operations
All tuple classes have utility methods that work like their counterparts in List.
- The method contains(value) returns true if the tuple contains the value.
- The method containsAll(values) returns true if the tuple contains all the given values.
- The method indexOf(value) returns the position of the first occurrence of the value, or -1.
- The method lastIndexOf(value) returns the position of the last occurrence of the value, or -1.
- The method getSize() returns the number of values.
boolean hasA3 = quartet1.contains("A3"); // true
boolean hasBoth = quartet1.containsAll("A1", 1); // true
int position = quartet1.indexOf(2.3); // 3
int missing = quartet1.lastIndexOf("X"); // -1
int size = quartet1.getSize(); // 4
Tuples also implement equals() and hashCode(). The method compareTo() compares the values position by position, so every value must be Comparable, such as a String or an Integer.
boolean equal = Pair.with("Sajal", 12).equals(Pair.with("Sajal", 12)); // true
int order = Pair.with("Sajal", 12).compareTo(Pair.with("Sajal", 15)); // -1
5. Benefits of Tuples
Every javatuples class extends the abstract class Tuple, so all of them have the same features.
- They are type safe, because each position has its own generic type.
- They are immutable.
- They are Iterable.
- They are serializable.
- They are comparable, because they implement the Comparable interface.
- They implement equals() and hashCode(), so a tuple works as a HashMap key.
- They implement toString(), which prints the values in square brackets.
A tuple also allows null values, e.g. Pair.with(null, 12) gives [null, 12].
6. Comparison of Tuples vs Lists/Arrays
A tuple is often compared with a List or an array, because all three hold several values in order. The table shows where they differ, together with the record and Map.Entry options from section 2.
| Record | Map.Entry | javatuples | List / array | |
|---|---|---|---|---|
| Values | named, any number | 2 | 1 to 10, by position | any number |
| Types | one type per value | one type per value | one type per position | one type for all elements |
| Can change | no | no (Map.entry()) | no | yes (except List.of()) |
| equals() / hashCode() | yes | yes | yes | yes for List, no for arrays |
| Needs a library | no (Java 16+) | no (Java 9+) | yes | no |
A tuple is not a faster kind of array, and it does not replace arrays or lists. We pick a tuple, or better a record, when the number of values and the type of each value are fixed. We pick a List when the number of values changes.
In practice, the choice depends on who reads the values and how long the tuple lives.
- For a public method that returns several values, we write a record, because the caller reads the values by name.
- For a short-lived pair inside a method or a stream, we call Map.entry().
- For code that works with tuples of different sizes, or when the project already uses javatuples, we use javatuples.
- For a fixed number of values of one type, such as RGB colors, an array is enough.
7. Java Tuple FAQs
7.1. Does Java Have a Built-in Tuple Type?
No. The JDK has no class named Tuple or Pair, apart from javafx.util.Pair in JavaFX, which is not part of the JDK since Java 11. A record, as we saw in section 2.2, is the built-in way to group values since Java 16.
7.2. Is a Java Record a Tuple?
Yes, a record is a named tuple. It holds a fixed number of values in a fixed order, and it is immutable. Unlike a javatuples Pair, every value has a name, so the caller writes employee.experience() instead of pair.getValue1().
7.3. How Do We Return Multiple Values From a Method in Java?
We return a record that holds the values, such as MinMax in section 2.2. An Object[] or a List<Object> also works, but the caller needs casts and has to know the position of each value.
8. Conclusion
A tuple holds a fixed number of values with different types. Java has no built-in tuple class, so we use a record for named values and Map.entry() for a quick pair. Apache Commons Lang adds Pair and Triple, which make sense when the project already has the library.
The javatuples library covers tuples from Unit to Decade. Its tuples are immutable, so methods such as setAtX() and add() return new tuples, and we always use the returned value. The library has had no release since 2011, but it runs on Java 25.
For new code, a record is the clearest choice, because each value has a name and the compiler generates the equals() and hashCode() methods for us.
9. References
- Record JavaDoc (Java 25)
- Map.entry() JavaDoc (Java 25))
- JEP 395: Records
- javatuples
- Commons Lang tuple package JavaDoc
Happy Learning !!