Question
I want to combine two List<String> objects into a new list in Java.
Is there a simpler approach than this?
List<String> newList = new ArrayList<String>();
newList.addAll(listOne);
newList.addAll(listTwo);
Requirements:
- Do not modify the original lists.
- Use JDK-only solutions.
- Do not use external libraries.
It would also be helpful to see:
- a one-line approach, if possible
- a version that works with JDK 1.3
Short Answer
By the end of this page, you will understand how to combine two lists into a new list in Java without changing the original lists. You will learn the standard JDK approach, compact one-liners, older pre-generics style for JDK 1.3, and how this pattern is used in real Java codebases.
Concept
Combining two lists means creating a new list that contains the elements of both source lists in order.
In Java, the most common way to do this is:
- create a new list
- copy elements from the first list
- copy elements from the second list
This matters because lists are mutable. If you simply reuse one of the original lists and call addAll, you will change that original list. When code must preserve existing data, you should create a new list first.
For example:
listOne.addAll(listTwo); // modifies listOne
That may be fine in some programs, but it breaks the requirement here because listOne is no longer unchanged.
The safe pattern is to copy, then append:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
This creates a separate list object. The original lists still exist unchanged.
A key idea here is the difference between:
- copying references into a new list
- deep-copying the objects inside the list
When you join two List<String> values, a new list container is created, but the contained objects are still the same element references. With String, that is usually fine because strings are immutable.
In real programming, this pattern is common when:
- building API responses
- merging configuration values
- combining query results
- preparing data for display
- avoiding side effects in shared code
Mental Model
Think of each list as a train of boxes.
listOneis one trainlistTwois another train- the new list is a third train
You are not changing either original train. You are building a new train and attaching copies of the box positions from the first train and then the second train.
So the operation is not:
- "stretch the first train"
It is:
- "build a new train and load it with the contents of both trains"
That mental model helps explain why this version is safe:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
The new train starts with everything from listOne, then adds everything from listTwo. The original trains stay as they were.
Syntax and Examples
The standard JDK approach is short and clear.
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
How it works
new ArrayList<>(listOne)creates a new mutable list containing the elements oflistOneresult.addAll(listTwo)appends all elements fromlistTwo
Example
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> listOne = Arrays.asList("A", "B");
List<String> listTwo = Arrays.asList("C", "D");
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
System.out.println(result); // [A, B, C, D]
System.out.println(listOne); // [A, B]
System.out.println(listTwo); // [C, D]
}
}
Compact one-liner
If you want a compact form and do not mind using an anonymous block style, you may see code like this:
List<String> result = new ArrayList<String>(listOne) {{ addAll(listTwo); }};
However, this creates an anonymous subclass and is generally not recommended in normal Java code because it adds unnecessary complexity.
A better modern compact style is still the two-line version because it is readable and standard.
Java 8+ stream alternative
If you are using Java 8 or later:
List<String> result = java.util.stream.Stream.concat(listOne.stream(), listTwo.stream())
.toList();
Or for broader compatibility before toList():
List<String> result = java.util.stream.Stream.concat(listOne.stream(), listTwo.stream())
.collect(java.util.stream.Collectors.toList());
This is elegant, but for simply joining two lists, new ArrayList<>(listOne) plus addAll is often the clearest choice.
JDK 1.3 style
JDK 1.3 does not have generics, so the syntax looks like this:
List result = new ArrayList(listOne);
result.addAll(listTwo);
The concept is exactly the same.
Step by Step Execution
Consider this example:
List<String> listOne = Arrays.asList("red", "green");
List<String> listTwo = Arrays.asList("blue", "yellow");
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
Step-by-step
-
listOnecontains:redgreen
-
listTwocontains:blueyellow
-
new ArrayList<>(listOne)creates a new list:redgreen
-
result.addAll(listTwo)appends all elements fromlistTwo:redgreenblueyellow
-
Final state:
listOne -> [red, green]
listTwo -> [blue, yellow]
result -> [red, green, blue, yellow]
Important detail
result is a different list object from both originals. That is why changing result later does not replace the original list object.
result.add("black");
System.out.println(result); // [red, green, blue, yellow, black]
System.out.println(listOne); // [red, green]
System.out.println(listTwo); // [blue, yellow]
Real World Use Cases
Here are common places where developers combine lists without modifying the originals.
Building API response data
You may fetch items from two sources and return them together.
List<Item> featured = itemService.getFeaturedItems();
List<Item> regular = itemService.getRegularItems();
List<Item> allItems = new ArrayList<>(featured);
allItems.addAll(regular);
Merging validation errors
A form may have field-level errors and business-rule errors.
List<String> allErrors = new ArrayList<>(fieldErrors);
allErrors.addAll(ruleErrors);
Combining search results
An application may search local cache first, then a database.
List<Result> combined = new ArrayList<>(cachedResults);
combined.addAll(databaseResults);
Preparing UI data
A screen may display pinned items first, followed by normal items.
List<Task> displayTasks = new ArrayList<>(pinnedTasks);
displayTasks.addAll(otherTasks);
Data processing pipelines
One stage may produce warnings, another stage may produce notices.
List<Message> messages = new ArrayList<>(warnings);
messages.addAll(notices);
Real Codebase Usage
In real Java projects, developers usually prefer the simplest readable solution.
Common pattern: copy then append
List<User> result = new ArrayList<>(activeUsers);
result.addAll(pendingUsers);
This is popular because it is:
- easy to read
- efficient enough for most cases
- supported across Java versions
- explicit about not mutating the originals
Pattern: pre-sizing for performance
If both lists are large, developers sometimes pre-size the target list.
List<String> result = new ArrayList<>(listOne.size() + listTwo.size());
result.addAll(listOne);
result.addAll(listTwo);
This can reduce internal array resizing.
Pattern: guard against null
In real systems, lists may be null due to old APIs or incomplete validation.
List<String> result = new ArrayList<>();
if (listOne != null) {
result.addAll(listOne);
}
if (listTwo != null) {
result.addAll(listTwo);
}
Pattern: return immutable output
Sometimes developers build a combined list and then prevent later modification.
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
result = java.util.Collections.unmodifiableList(result);
Pattern: use streams when already in a stream-based flow
List<String> result = java.util.stream.Stream.concat(listOne.stream(), listTwo.stream())
.collect(java.util.stream.Collectors.toList());
This is common when data is already being filtered or mapped with streams.
Common Mistakes
Mistake 1: modifying an original list by accident
Broken example:
listOne.addAll(listTwo);
Why it is a problem:
listOneis changed- this breaks the requirement to keep original lists unchanged
Fix:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
Mistake 2: assuming Arrays.asList() returns a normal resizable list
Broken example:
List<String> listOne = Arrays.asList("A", "B");
listOne.add("C"); // throws UnsupportedOperationException
Why it is a problem:
Arrays.asList()returns a fixed-size list view
Fix:
List<String> listOne = new ArrayList<>(Arrays.asList("A", "B"));
listOne.add("C");
Mistake 3: using double-brace initialization casually
Less ideal example:
List<String> result = new ArrayList<String>(listOne) {{ addAll(listTwo); }};
Why to avoid it:
- creates an anonymous inner subclass
- can be harder to read and maintain
- not the normal style in production code
Fix:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
Mistake 4: forgetting null checks when input lists may be null
Broken example:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo); // may throw NullPointerException
Fix:
List<String> result = new ArrayList<>();
if (listOne != null) result.addAll(listOne);
if (listTwo != null) result.addAll(listTwo);
Mistake 5: confusing shallow copy with deep copy
When the list contains mutable objects, the new list gets the same element references.
List<Person> result = new ArrayList<>(listOne);
result.addAll(listTwo);
This copies the list structure, not cloned Person objects. If an element changes, both lists still reference that same object.
Comparisons
| Approach | Modifies originals? | JDK-only? | Java version | Notes |
|---|---|---|---|---|
new ArrayList<>(listOne); result.addAll(listTwo); | No | Yes | 5+ | Best general solution |
listOne.addAll(listTwo) | Yes | Yes | 1.2+ | Short, but changes listOne |
new ArrayList<>(size); addAll(...); addAll(...); | No | Yes | 5+ | Good when you want pre-sizing |
Stream.concat(...).collect(...) | No | Yes | 8+ | Useful in stream-based code |
| Double-brace initialization | No | Yes | 5+ | Works, but usually avoided |
new ArrayList(listOne); result.addAll(listTwo); | No | Yes | 1.3 style | Same idea without generics |
addAll vs Stream.concat
| Feature | addAll approach | Stream.concat approach |
|---|---|---|
| Simplicity | Very simple | Slightly more advanced |
| Readability | Excellent for beginners | Good if using streams already |
| Performance | Very good | Usually fine, but more overhead |
| Java version | Older and modern Java | Java 8+ only |
| Best use | Basic list joining | Stream pipelines |
Best choice here
For this question, the most practical answer is:
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
And for JDK 1.3:
List result = new ArrayList(listOne);
result.addAll(listTwo);
Cheat Sheet
// Standard Java 5+ solution
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
// Pre-sized version
List<String> result = new ArrayList<>(listOne.size() + listTwo.size());
result.addAll(listOne);
result.addAll(listTwo);
// Java 8+
List<String> result = Stream.concat(listOne.stream(), listTwo.stream())
.collect(Collectors.toList());
// JDK 1.3 style
List result = new ArrayList(listOne);
result.addAll(listTwo);
Rules to remember
addAllappends all elements from one collection to another.listOne.addAll(listTwo)modifieslistOne.new ArrayList<>(listOne)creates a new mutable copy.- Copying a list does not deep-copy the objects inside it.
Arrays.asList(...)gives a fixed-size list, not a fully resizableArrayList.
Good default
List<T> result = new ArrayList<>(firstList);
result.addAll(secondList);
FAQ
What is the simplest way to join two lists in Java?
Use a new ArrayList initialized with the first list, then call addAll with the second list.
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
Does addAll modify the original list?
Yes. It modifies the list you call it on. That is why you should call it on a new list if you want to preserve the originals.
Is there a one-liner to combine two lists in Java?
A stream-based one-liner is possible in Java 8+:
List<String> result = Stream.concat(listOne.stream(), listTwo.stream())
.collect(Collectors.toList());
For simple cases, the two-line ArrayList approach is usually clearer.
How do I combine two lists in JDK 1.3?
Use the non-generic form:
List result = new ArrayList(listOne);
result.addAll(listTwo);
Is copying a list the same as cloning the objects inside it?
No. The new list contains the same element references. Only the list container is new.
Which is better: addAll or streams?
For simply joining two lists, addAll is usually easier to read. Streams are more useful when you are already filtering, mapping, or chaining other operations.
Can I join more than two lists with the same pattern?
Yes.
List<String> result = new ArrayList<>(listOne);
result.addAll(listTwo);
result.addAll(listThree);
Mini Project
Description
Build a small Java utility that combines two lists of usernames into a new list without changing the originals. This demonstrates safe list copying, addAll, and how to verify that source data remains unchanged after the merge.
Goal
Create a method that returns a new combined list from two input lists while preserving the original lists.
Requirements
- Create a method that accepts two
List<String>parameters. - Return a new list containing all elements from the first list followed by all elements from the second list.
- Do not modify either input list.
- Print the original lists and the merged list to verify the behavior.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.