Question

How to Join Two Lists in Java Without Modifying the Originals

javalistmergejava-5

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:

  1. create a new list
  2. copy elements from the first list
  3. 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.

  • listOne is one train
  • listTwo is 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.

Take Quiz

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 of listOne
  • result.addAll(listTwo) appends all elements from listTwo

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

  1. listOne contains:

    • red
    • green
  2. listTwo contains:

    • blue
    • yellow
  3. new ArrayList<>(listOne) creates a new list:

    • red
    • green
  4. result.addAll(listTwo) appends all elements from listTwo:

    • red
    • green
    • blue
    • yellow
  5. 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);
Take Quiz

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:

  • listOne is 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

ApproachModifies originals?JDK-only?Java versionNotes
new ArrayList<>(listOne); result.addAll(listTwo);NoYes5+Best general solution
listOne.addAll(listTwo)YesYes1.2+Short, but changes listOne
new ArrayList<>(size); addAll(...); addAll(...);NoYes5+Good when you want pre-sizing
Stream.concat(...).collect(...)NoYes8+Useful in stream-based code
Double-brace initializationNoYes5+Works, but usually avoided
new ArrayList(listOne); result.addAll(listTwo);NoYes1.3 styleSame idea without generics

addAll vs Stream.concat

FeatureaddAll approachStream.concat approach
SimplicityVery simpleSlightly more advanced
ReadabilityExcellent for beginnersGood if using streams already
PerformanceVery goodUsually fine, but more overhead
Java versionOlder and modern JavaJava 8+ only
Best useBasic list joiningStream 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);
Take Quiz

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

  • addAll appends all elements from one collection to another.
  • listOne.addAll(listTwo) modifies listOne.
  • 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 resizable ArrayList.

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);
Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more