Back to Blog
Java

Returning Multiple Values from a Java Method

java return multiple values: Practical options for returning multiple values from a Java method: arrays, maps, records, and custom classes, with the tradeoffs of each...

Java recordsmethod return typestype safetyJava collectionsJava arrays
Illustration of a Java method returning a single container object that splits into multiple distinct typed values

java return multiple values requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Java methods can only declare one return type. To return multiple values from a Java method, you must wrap the results in a single container: an array, a collection, a map, or a custom class. Each choice changes type safety, readability, and how callers consume the result.

Why Java Doesn't Have Native Multiple Returns

Java's method signature allows exactly one return type. Unlike Python or Go, there is no tuple syntax in the language core. This is a deliberate design decision that keeps the type system explicit. Every method returns one value, and that value's type is visible at the call site.

That constraint does not mean you are limited to one piece of data. It means you must wrap related values in a single object. The real question is which wrapper to use, and the answer depends on how much type safety and readability you need at the call site.

Returning an Array: Simple but Weakly Typed

The simplest approach is to return an array:

public static int[] minMax(int[] numbers) { int min = numbers[0]; int max = numbers[0]; for (int n : numbers) { if (n < min) min = n; if (n > max) max = n; } return new int[] { min, max }; }

The caller must remember that index 0 is the minimum and index 1 is the maximum:

int[] result = minMax(new int[] { 3, 7, 2, 9 }); int min = result[0]; int max = result[1];

This works, but it has two problems. First, the meaning of each element is implicit. Nothing in the type int[] tells you which value is which. Second, arrays work well only when all returned values share the same type. If you need to return an int and a String, an array will not compile.

Arrays are acceptable for small, homogeneous results where the method and its caller are maintained by the same person. They become risky in shared code where a future reader has to guess the index meanings.

Returning a List or Map: More Flexible, Still Weakly Typed

A List gives you the same index-based problem as an array but with the ability to grow. A Map lets you name the values:

public static Map<String, Integer> divideWithRemainder(int dividend, int divisor) { Map<String, Integer> result = new HashMap<>(); result.put("quotient", dividend / divisor); result.put("remainder", dividend % divisor); return result; }

Callers retrieve values by key:

Map<String, Integer> result = divideWithRemainder(17, 5); int quotient = result.get("quotient"); int remainder = result.get("remainder");

This reads better than an array, but it has a serious weakness: the map values are all Integer, so type safety is lost. If you change the remainder to a double, callers will not get a compile error; they will get a ClassCastException at runtime. The keys are also just strings, so a typo like "remiander" silently returns null.

Maps are useful when the number of returned values varies or when keys are genuinely dynamic. For a fixed set of known fields, a map is usually the wrong tool.

Using a Custom Class: The Type-Safe Default

The most robust way to return multiple values is to define a class that holds them. This is the approach Java's design pushes you toward.

public record DivisionResult(int quotient, int remainder) {}

A record gives you a named type, constructor, accessors, equals, hashCode, and toString in one declaration. The method becomes:

public static DivisionResult divideWithRemainder(int dividend, int divisor) { return new DivisionResult(dividend / divisor, dividend % divisor); }

Callers get compile-time type checking:

DivisionResult result = divideWithRemainder(17, 5); int quotient = result.quotient(); int remainder = result.remainder();

If you rename a component, the compiler tells every caller what broke. If you change the type of remainder from int to long, callers see the error immediately. That is the fundamental advantage over arrays and maps.

Records require Java 16 or later. If you are on an older Java version, a plain class with final fields and a constructor works the same way, with more boilerplate.

When a Custom Class Is Overkill

A custom class is not always justified. For a private helper method used in one place, defining a record just for two values can feel heavy. In that situation, an array or a small map may be pragmatic.

The decision rule is simple: use a custom class when the result is part of a public API, when the values are semantically distinct, or when the result is used in more than one place. Use a lightweight container when the result is local, transient, and homogeneous.

The table below summarizes the tradeoffs:

ApproachType safetyReadabilityBoilerplateBest for
ArrayWeakLowMinimalHomogeneous transient results
MapWeakMediumMinimalDynamic keys
RecordStrongHighMinimalFixed named fields (Java 16+)
Plain classStrongHighModerateFixed named fields (older Java)

Performance and Allocation Behavior

Returning a container object always allocates. An array allocates an array object. A map allocates the map plus internal nodes. A record allocates a single object with fields. For most application code, this allocation is negligible. The JVM's escape analysis can even eliminate the allocation when the object does not escape the calling method.

If you are writing a hot loop that calls such a method millions of times, the allocation cost can become measurable. In that case, consider restructuring: pass a mutable result object into the method and reuse it, or return primitive fields through a small holder that the caller reuses. These patterns complicate the code, so apply them only when profiling shows they matter.

Compatibility Considerations

Records are the cleanest solution, but they require Java 16+. If your project targets Java 11 or 15, you have two options: a plain class with explicit fields and accessors, or a third-party tuple library. A plain class keeps you on standard Java with no dependencies. A tuple library like javatuples provides generic Pair and Triplet types, but generic tuples sacrifice the semantic naming that records give you.

The choice between a record and a plain class is mostly a version constraint. The choice between a custom class and a generic tuple is a maintainability decision: DivisionResult tells the reader what the values mean; Pair<Integer, Integer> does not.

Putting It Together: A Practical Example

Here is a complete example showing why the record approach scales better than the alternatives when the result grows:

public record UserStats(int postCount, int commentCount, int followerCount) {} public static UserStats computeStats(User user) { return new UserStats( user.posts().size(), user.comments().size(), user.followers().size() ); }

If you later add a likesReceived field, the record declaration changes and the compiler points out every place that constructs or consumes UserStats. With an array or map, the change would be silent until runtime.

The general rule: when the returned values are fixed, known, and semantically distinct, define a named type. When they are dynamic or homogeneous, a collection is acceptable. When they are transient and local, a simple array may suffice. The named type is the default for production code because it keeps the compiler on your side.

java return multiple values: Practical Usage and Code Exampl | RYUSLOG DEV