Copy the first list, then add the second: List<String> combined = new ArrayList<>(a); combined.addAll(b); That builds a new, modifiable list and leaves a and b alone. On Java 16+, Stream.concat(a.stream(), b.stream()).toList() does it in one expression, but the list it returns can't be changed.
Java has no + for lists and no concat method on List, so joining two lists means choosing who owns the result. addAll appends into a list that already exists, which is only safe when you created that list yourself: calling it on a list from List.of or Arrays.asList throws. Streams give you a one-line alternative that also scales to many lists, and arrays, which can't grow at all, need a new array and a copy. Each example runs on this page: hit Run, then edit the code and run it again.
1new ArrayList<>(a) plus addAll(b)Recommended
The copy constructor new ArrayList<>(a) makes a new list holding everything in a, and addAll(b) appends every element of b to the end of it. It works on every Java version, the result is an ordinary ArrayList you can keep adding to, and neither input changes. The same two calls are also where the most common error comes from, so the end of the example shows it on purpose.
Output
Prints [red, green, blue, yellow], then the untouched [red, green] [blue, yellow], then [red, green, blue, yellow, purple] after the add. The pre-sized version prints the same four colours; the capacity hint only saves the internal array from being resized as it fills. The last two lines are the trap: a.addAll(b) on a List.of list, and fixed.addAll(b) on an Arrays.asList list, both throw java.lang.UnsupportedOperationException. List.of lists are immutable, and Arrays.asList is a fixed-size view of an array, so neither can grow. Copying into a new ArrayList<>(...) first is the fix, which is why the copy comes before the addAll. See initializing a list in one line for which factory methods give you which kind of list.
2Stream.concat with toList() or Collectors.toList()
Stream.concat(a.stream(), b.stream()) (Java 8+) is a stream of every element of a followed by every element of b. How you collect it decides what kind of list you get back. .toList() (Java 16+) is the shortest and returns an unmodifiable list. .collect(Collectors.toList()) works on Java 8 and happens to return an ArrayList, but its documentation makes no promise about mutability, so use Collectors.toCollection(ArrayList::new) when you plan to change the result.
Output
Prints [1, 2, 3, 4, 5, 6], then toList() result can't be changed, because add on that list throws UnsupportedOperationException. The collector versions accept changes: [1, 2, 3, 4, 5, 6, 7] and [0, 1, 2, 3, 4, 5, 6]. The last line prints [#2, #4, #6], which is the real reason to pick a stream over addAll: filtering, mapping or sorting while you merge costs no extra code. For a plain join of two lists, addAll is just as short and skips the stream machinery.
3Three or more lists: Stream.of(...).flatMap
Stream.concat takes exactly two streams. For more, stream the lists themselves with Stream.of(a, b, c) and let flatMap(List::stream) replace each list with its elements. The same call flattens a List<List<T>> whose length you don't know until runtime. If you'd rather not use streams, a loop of addAll calls does the same, and a small varargs helper keeps it to one call.
Output
The flatMap line prints [a, b, c, d, e], the batches flatten to [1, 2, 3, 4, 5, 6] and sum to 21, and the helper and the nested concat both print [a, b, c, d, e] as well. Prefer flatMap over nesting: the Stream.concat Javadoc warns that a deeply nested chain can end in a StackOverflowError. The helper's List<? extends T> parameter lets you mix element types, so a List<Integer> and a List<Double> can be joined into a List<Number>. Libraries have this built in too: Guava's Iterables.concat returns a live view without copying, and Apache Commons' ListUtils.union is, despite its name, a plain concatenation.
4Merge without duplicates
When the combined list should hold each value once, merge into a LinkedHashSet. A set ignores values it already contains, and the linked version remembers insertion order, so the result reads like a concatenation with the repeats taken out. A plain HashSet dedupes just as well but iterates in no particular order. On a stream, distinct() does the same job.
Output
The set prints [go, java, rust, zig, kotlin]: java and go from b were already there, so they keep their first positions and zig and kotlin follow in the order b had them. Copied back into a list, unique.get(3) is zig. distinct() gives the same [go, java, rust, zig, kotlin], the TreeSet sorts it to [go, java, kotlin, rust, zig], and removeAll leaves [zig, kotlin], the new items only. All of these compare with equals and hashCode (or compareTo for the TreeSet), so a class of your own needs those methods for its duplicates to be spotted.
5Concatenate two arrays
An array has a fixed length, and a + b on two arrays doesn't compile, so concatenating means allocating an array of a.length + b.length and copying both into it. System.arraycopy does the copying and is the fastest option. Arrays.copyOf allocates and copies the first array in one call, which also makes a generic helper possible. Streams are the one-expression version, with one catch: primitive arrays such as int[] need IntStream, not Stream.
Output
The two arraycopy calls and the helper each print [a, b, c, d, e], and the helper also joins Integer arrays into [1, 2, 3]. The stream versions print [a, b, c, d, e] and a length of 6 for three arrays, and IntStream.concat prints [1, 2, 3, 4, 5]. The generic helper can't take an int[], because generics don't cover primitives, so write an int[] overload with the same two lines or use IntStream. Pass String[]::new to toArray: without it you get an Object[]. Arrays print through Arrays.toString, as covered in printing an array. Apache Commons Lang has the one-liner ArrayUtils.addAll(a, b) if it's already on your classpath.
6Which should you use?
| Method | Result | Java version | Best for |
|---|---|---|---|
| new ArrayList<>(a); addAll(b) | New, modifiable list | Any | Two lists: the default |
| a.addAll(b) | Changes a in place | Any | Growing a list you created |
| Stream.concat(...).toList() | Unmodifiable list | 16+ | Read-only results, filter or map while merging |
| collect(Collectors.toList()) | ArrayList, not guaranteed | 8+ | Stream code that must run on Java 8 |
| Stream.of(a, b, c).flatMap(List::stream) | Depends on the collector | 8+ | Three or more lists, or a List<List<T>> |
| new LinkedHashSet<>(a); addAll(b) | Set, no duplicates, order kept | Any | Merging without repeats |
| Arrays.copyOf + System.arraycopy | New array | Any | Object arrays, fastest |
| IntStream.concat(...).toArray() | New int[] | 8+ | Primitive arrays in one expression |
Frequently asked questions
How do I join two lists in Java?
Copy the first list and append the second: List<String> combined = new ArrayList<>(a); combined.addAll(b);. The result is a new, modifiable list and both inputs are unchanged. On Java 16+ you can also write Stream.concat(a.stream(), b.stream()).toList(), which returns an unmodifiable list. Java has no + operator or concat method for lists.
Why does addAll throw UnsupportedOperationException?
The list you called it on can't grow. Lists from List.of, Stream.toList() and Collections.unmodifiableList are unmodifiable, and Arrays.asList returns a fixed-size view of an array: you can set its elements but not add to it. List.of("a").addAll(List.of()) throws even though there is nothing to add. Copy into a new ArrayList<>(list) first and call addAll on the copy.
What is the difference between Stream.toList() and Collectors.toList()?
Stream.toList() (Java 16+) always returns an unmodifiable list, and it accepts null elements. collect(Collectors.toList()) (Java 8+) currently returns a java.util.ArrayList you can modify, but the documentation doesn't guarantee its type or mutability; use Collectors.toCollection(ArrayList::new) when you rely on changing the result. Collectors.toUnmodifiableList() (Java 10+) is unmodifiable too but throws NullPointerException on a null element.
How do I concatenate two arrays in Java?
Allocate an array of both lengths and copy each one in: String[] both = Arrays.copyOf(a, a.length + b.length); System.arraycopy(b, 0, both, a.length, b.length);. For a one-expression version use Stream.concat(Arrays.stream(a), Arrays.stream(b)).toArray(String[]::new), or IntStream.concat(Arrays.stream(x), Arrays.stream(y)).toArray() for an int[]. Writing a + b for two arrays is a compile error: bad operand types for binary operator '+'.
How do I combine two lists without duplicates?
Add both to a LinkedHashSet: Set<String> merged = new LinkedHashSet<>(a); merged.addAll(b);. It keeps the first occurrence of each value in order, then wrap it in new ArrayList<>(merged) if you need a List. With streams, Stream.concat(a.stream(), b.stream()).distinct().toList() gives the same order. Use a TreeSet if you want the result sorted.
Does addAll copy the objects in the list?
No. addAll and new ArrayList<>(a) copy the references, so the combined list and the originals share the same element objects. If the elements are mutable, a change made through one list shows up in the other: appending "!" to a StringBuilder taken from the combined list also changes the one in the original list. Only the lists themselves are separate.