A Java tuple is a small, fixed-size object that holds a few values of different types in a set order, such as a name, an age and a city. Java has no built-in tuple type, so we either write a record (Java 16 and later) or use the javatuples library, which offers ready-made classes from Unit (one value) to Decade (ten values).
We use tuples to return two or three values from one method, to use a composite key in a map, or to carry an intermediate result through a stream pipeline without writing a new class for it.
The following example creates a two-value tuple with javatuples and reads, replaces and adds values. Each line shows the result as a comment.
Pair<String, Integer> pair = Pair.with("Sajal", 12); // [Sajal, 12]
String name = pair.getValue0(); // "Sajal"
Integer years = pair.getValue1(); // 12
Pair<String, Integer> renamed = pair.setAt0("Kajal"); // [Kajal, 12]
Triplet<String, Integer, String> triplet = pair.add("Admin"); // [Sajal, 12, Admin]
Notice that setAt0() and add() return new tuples and leave pair unchanged, because javatuples classes are immutable. We start with what a tuple is and how records cover the same need in plain Java, and go through the javatuples operations and compare tuples with lists, arrays and records.
1. What is a Tuple?
A tuple is an ordered group of values where each position has its own type. The values do not need to relate to each other in any way, but together they describe one thing. For example, [“Sajal Chakraborty”, “IT Professional”, 32] can be a tuple that represents a person in an HR app, with a name, a job title and an age.

Tuples can hold any mix of types. Each one has a fixed number of values, and each position always holds the same kind of value.
["Java", 25, "Linux"]
["Alex", 32, "New York", true]
[3, "Alexa", "howtodoinjava.com", 37000]
Languages such as Python, Kotlin and C# have tuple syntax built in. In Java, we model the same idea with a class, and the question is only whether that class has positional names such as getValue0() or real names such as name(), as the diagram shows.
2. Tuples in Java
By default, Java does not have a tuple data type, and since Java 16 the standard way to group a few values is a record. A record declares its components in one line, and the compiler adds the constructor, the accessor methods, equals(), hashCode() and toString().
2.1. A Generic Tuple Record
Before Java 16, we wrote a generic class with two final fields, a constructor and two getters, and we left out the setters to keep it immutable. A generic record does the same in one line and also gets value-based equals() and hashCode(), which the hand-written class did not have.
record Tuple<A, B>(A first, B second) {}
We can store any two values (primitives are boxed, objects are stored as references) and read them with the accessor methods. The record has no setters, so the values cannot be changed once it is created.
Tuple<Integer, String> fruit = new Tuple<>(1, "apple"); // Tuple[first=1, second=apple]
Integer number = fruit.first(); // 1
String label = fruit.second(); // "apple"
boolean same = fruit.equals(new Tuple<>(1, "apple")); // true
2.2. Records as Named Tuples
A generic tuple still hides what first and second mean. A record with real component names fixes that, and it costs one line per use case.
Consider a shop app where a product page shows the lowest and the highest price of a product across sellers. The method that computes both values returns one PriceRange instead of an array or two separate calls.
record PriceRange(double min, double max) {}
static PriceRange priceRange(List<Double> prices) {
DoubleSummaryStatistics stats = prices.stream()
.mapToDouble(Double::doubleValue)
.summaryStatistics();
return new PriceRange(stats.getMin(), stats.getMax());
}
PriceRange range = priceRange(List.of(4.5, 2.0, 9.25)); // PriceRange[min=2.0, max=9.25]
double lowest = range.min(); // 2.0
double highest = range.max(); // 9.25
The caller reads range.min() instead of getValue0(), so the code explains itself. Records also work with record patterns, so if (range instanceof PriceRange(var min, var max)) unpacks both values in one step, much like tuple unpacking in other languages.
2.3. Map.entry() for Two Values
For a quick key-value pair inside a method, the JDK already has Map.entry() (Java 9), which returns an unmodifiable Map.Entry. It rejects null keys and values, and the pairs in Java guide compares it with the other two-value classes.
Map.Entry<String, Integer> entry = Map.entry("Sajal", 12); // Sajal=12
String key = entry.getKey(); // "Sajal"
Map.Entry<String, Integer> bad = Map.entry("Sajal", null); // NullPointerException
A List<Object> or an Object[] can also hold values of different types, but the compiler forgets the type of each position, so every read needs a cast that can fail at runtime with a ClassCastException.
3. Javatuples Library
The javatuples library gives us generic tuple classes with positional getters, so we do not declare a record for every combination. It is a small jar with no dependencies, and it runs on Java 25 from the classpath.
3.1. Adding javatuples to a Maven Project
The library is in Maven Central. Version 1.2 from 2011 is the latest and last release, so the project receives no fixes or updates, which is the main reason to prefer records in new code.
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
3.2. Tuple Classes from Unit to Decade
Javatuples supports tuples of up to 10 values, with one class for each size. All of them extend the abstract class org.javatuples.Tuple.
| Class | Values | Class | Values |
|---|---|---|---|
| Unit | 1 | Sextet | 6 |
| Pair | 2 | Septet | 7 |
| Triplet | 3 | Octet | 8 |
| Quartet | 4 | Ennead | 9 |
| Quintet | 5 | Decade | 10 |
On top of these classes, the library has KeyValue and LabelValue. Both hold two values like Pair, but their getters are getKey()/getValue() and getLabel()/getValue(), which read better for that kind of data. A KeyValue is never equal to a Pair with the same values, because equals() compares the classes too.
4. Operations On Tuples
Every javatuples class has the same set of operations, and each one that changes the content returns a new tuple. The examples use Pair, Triplet and Quartet, and the other sizes work the same way.
4.1. Creating a Tuple
There are three ways to create a tuple, and the factory method is the shortest one.
4.1.1. Factory Methods
Each tuple class has a static with() factory method that infers the type parameters from the arguments.
Pair<String, Integer> pair = Pair.with("Sajal", 12); // [Sajal, 12]
Quartet<String, Integer, String, Double> quartet = Quartet.with("A1", 1, "A3", 2.3); // [A1, 1, A3, 2.3]
4.1.2. Constructor
The public constructor does the same job, and the diamond operator keeps the line short.
Pair<String, Integer> person = new Pair<>("Sajal", 12); // [Sajal, 12]
4.1.3. From Collection or Iterable
The fromCollection() method needs a collection with the same number of elements as the tuple size, otherwise it throws an IllegalArgumentException. The fromIterable(iterable, index) variant starts reading at the given index.
List<String> names = List.of("A1", "A2", "A3", "A4");
Quartet<String, String, String, String> all = Quartet.fromCollection(names); // [A1, A2, A3, A4]
Pair<String, String> lastTwo = Pair.fromIterable(names, 2); // [A3, A4]
Pair<String, String> wrongSize = Pair.fromCollection(names); // IllegalArgumentException
Be careful with fromIterable() near the end of the list. When fewer elements remain than the tuple needs, it fills the missing positions with null instead of failing, so Pair.fromIterable(names, 3) returns [A4, null].
4.2. Getting Values from Tuple
We read values either with the typed getValueX() methods or with the untyped getValue(int) method.
4.2.1. getValue() Methods
The indexed getValueX() methods, where X is the position starting at 0, are type-safe. The compiler knows the return type from the type parameters, so no cast is needed.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
String name = pair.getValue0(); // "Sajal"
Integer experience = pair.getValue1(); // 12
4.2.2. getValue(int index) Method
The getValue(int index) method returns Object, so we cast the value ourselves, and a wrong cast fails at runtime. We use it only when the position comes from a variable, for example in a loop over all positions.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Object first = pair.getValue(0); // "Sajal"
String name = (String) pair.getValue(0); // "Sajal"
String wrong = (String) pair.getValue(1); // ClassCastException
The KeyValue and LabelValue classes use getKey()/getValue() and getLabel()/getValue() instead.
4.3. Setting Values in Tuple
The setAtX() methods replace the value at position X. Because the tuples are immutable, setAtX() returns a new tuple of the same size and leaves the original unchanged.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Pair<String, Integer> modified = pair.setAt0("Kajal"); // [Kajal, 12]
Pair<String, Integer> original = pair; // [Sajal, 12]
A setAtX() call can also change the type of that position, for example pair.setAt1(“twelve”) returns a Pair<String, String>.
4.4. Adding and Removing Values
Adding or removing a value changes the size, so the result is a tuple of a different class.
4.4.1. add() Method
The add() method appends a value at the end. Adding one value to a Pair returns a Triplet, and adding another tuple appends all of its values.
Pair<String, Integer> pair = Pair.with("Sajal", 12);
Triplet<String, Integer, String> triplet = pair.add("IT Professional"); // [Sajal, 12, IT Professional]
Triplet<String, String, String> langs = Triplet.with("Java", "C", "C++");
Quintet<String, Integer, String, String, String> five = pair.add(langs); // [Sajal, 12, Java, C, C++]
4.4.2. addAt() Method
By default, new elements go to the end of the tuple. The addAtX() methods insert the value at position X instead and shift the rest to the right.
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. removeFrom() Method
The removeFromX() methods drop the value at position X and return the next smaller tuple class.
Triplet<String, String, String> triplet = Triplet.with("Java", "C", "C++");
Pair<String, String> withoutC = triplet.removeFrom1(); // [Java, C++]
4.5. Converting Tuple to Collection or Array
Each tuple class has toList() and toArray() methods. A tuple can hold values of different types, so the result is a List<Object> or an Object[], and the list is unmodifiable.
Quartet<String, Integer, String, Double> quartet = Quartet.with("A1", 1, "A3", 2.3);
List<Object> list = quartet.toList(); // [A1, 1, A3, 2.3]
Object[] array = quartet.toArray(); // [A1, 1, A3, 2.3]
boolean added = list.add("A5"); // UnsupportedOperationException
4.6. Iterating over Tuple Values
All tuple classes implement Iterable<Object>, so a for-each loop visits the values in position order.
Quartet<String, Integer, String, Double> quartet = Quartet.with("A1", 1, "A3", 2.3);
StringBuilder values = new StringBuilder();
for (Object value : quartet) {
values.append(value).append(' ');
}
String joined = values.toString().strip(); // "A1 1 A3 2.3"
4.7. More Tuple Operations
All tuple classes also have the search methods we know from collections.
- contains(value) returns true if the tuple holds the value.
- containsAll(collection) returns true if the tuple holds all the values.
- indexOf(value) returns the position of the first match, or -1.
- lastIndexOf(value) returns the position of the last match, or -1.
Triplet<String, Integer, String> dev = Triplet.with("Sajal", 12, "Sajal");
boolean hasName = dev.contains("Sajal"); // true
boolean hasBoth = dev.containsAll(List.of("Sajal", 12)); // true
int firstIndex = dev.indexOf("Sajal"); // 0
int lastIndex = dev.lastIndexOf("Sajal"); // 2
Tuples also implement equals(), hashCode() and compareTo() from their values, so we can use them as HashMap keys and sort them. The compareTo() method compares position by position and casts each value to Comparable, so a tuple that holds a non-comparable object throws a ClassCastException when sorted.
int order = Pair.with("a", 2).compareTo(Pair.with("a", 10)); // -1
5. Benefits of Tuples
Javatuples classes bring a set of useful properties with no extra code.
- They are type-safe through generics, as long as we use getValueX() and not getValue(int).
- They are immutable, so no method changes a tuple in place.
- They implement Iterable, Serializable and Comparable.
- They implement equals() and hashCode() based on the values, and a readable toString().
The immutability is shallow, as with records and unmodifiable lists. If a tuple holds a mutable object such as an ArrayList, code that has a reference to that list can still add elements, and the tuple shows the change.
Tuples also have limits. The getter names say nothing about the data, primitive values are boxed, the largest class holds 10 values, and the library has not changed since 2011. These limits are why records replaced javatuples in most modern code.
6. Comparison of Tuples vs Lists/Arrays
A tuple looks like a short list, but it has a fixed size and a fixed type per position. Records add real names on top of that. Performance is rarely the deciding factor here, but a javatuples tuple stores each boxed value twice, in a field such as val0 and in an internal array and list, so it uses more memory than a record with the same values.
| Feature | javatuples tuple | Record | List<Object> / Object[] |
|---|---|---|---|
| Different types per position | Yes, checked by the compiler | Yes, checked by the compiler | Yes, but every read needs a cast |
| Names for values | No, getValue0(), getValue1() | Yes, name(), age() | No, index only |
| Size | Fixed, 1 to 10 | Fixed, any number | Array fixed, list grows |
| Mutability | Immutable | Immutable fields | List and array are mutable |
| equals() and hashCode() | Value-based | Value-based | Value-based for List, identity for arrays |
| Needs a dependency | Yes | No, Java 16+ | No |
For a result that leaves a method or crosses a module, use a record, because its names document the data. A javatuples tuple fits short-lived values inside one method or one stream pipeline, and a List fits when the number of values changes at runtime and every value has the same type.
For example, a report that groups orders by city and month can use Pair.with(city, month) as the map key inside one method, while the service method that returns the totals to a controller returns a record CityMonthTotal(String city, YearMonth month, BigDecimal total).
7. Java Tuple FAQs
Python and Kotlin have tuples in the language, so programmers who switch to Java look for the matching type and for a way to return several values from a method.
7.1. Does Java have a built-in tuple class?
No. The JDK has no general tuple type. The closest built-in options are records for any number of named values and Map.Entry (created with Map.entry()) for two values.
7.2. Are Java records tuples?
Yes, records are named tuples. A record is a fixed group of typed values with value-based equals() and hashCode(), and JEP 395 describes records as nominal tuples, meaning each record type and each component has a name.
7.3. How do we return multiple values from a method in Java?
We return a record that holds the values, such as PriceRange in section 2. For two values in internal code, Map.entry() or a javatuples Pair also work, while an Object[] loses the types and should be avoided.
7.4. Is javatuples still maintained, and does it work with Java 25?
The library is not maintained, since version 1.2 from 2011 is the last release. It has no dependencies and uses only basic Java APIs, so it compiles and runs on Java 25 from the classpath.
7.5. Can a tuple be a key in a HashMap?
Yes. Javatuples classes and records implement equals() and hashCode() from their values, so two tuples with the same values find the same entry.
Map<Pair<String, Integer>, String> seats = new HashMap<>();
seats.put(Pair.with("A", 1), "Lokesh");
String guest = seats.get(Pair.with("A", 1)); // "Lokesh"
8. Conclusion
A tuple groups a fixed number of values of different types. Java has no tuple syntax, so we model tuples with classes.
Records are the first choice since Java 16, because they give each value a name and need no library. Javatuples still offers handy generic classes from Unit to Decade, with immutable setAtX(), add() and removeFromX() operations, but its positional getters and its last release in 2011 make it a better fit for short-lived values than for public APIs. For more on the collection types used in the comparison, the Java collections tutorial covers lists and maps in depth.
9. References
Happy Learning !!