Java • Java-Specific Topics

Side Effects & Immutability — Understand side effects and immutability considerations (intro)

Learn why controlling change in your data leads to safer, more predictable Java programs.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 4 figures

SECTION 1

Historical Context & Motivation

Ever since people began writing software, they have struggled with bugs caused by data changing at the wrong time. In the earliest computers of the 1940s, programs stored values in memory locations that any part of the program could overwrite. This made it extremely difficult to track down errors, because one section of code might silently change a value that another section depended on. The concepts of side effects and immutability arose as programmers looked for ways to make software more reliable and easier to reason about.

A side effect occurs whenever a method does something beyond returning a value — it might modify a field, write to a file, or print to the console. Immutability is the design choice to make objects whose state can never change after creation. Together, these ideas help programmers write code that is predictable and less error-prone. Let's trace the key moments that brought these concepts into the Java world.

1958 • Lisp Introduces Functional Ideas

John McCarthy's Lisp language emphasized expressions without side effects, planting the seeds for functional programming and immutable data structures.

1995 • Java 1.0 Launches with String Immutability

Java's designers made the String class immutable from day one, proving that immutability could be practical in a mainstream object-oriented language.

2004 • Effective Java Popularizes Immutability

Joshua Bloch's book Effective Java urged developers to minimize mutability and limit side effects, making these ideas standard best practices.

2014 • Java 8 Brings Lambdas & Streams

With lambda expressions and the Stream API, Java embraced a functional style that discourages side effects and favors transformations over in-place mutation.

2021 • Java Records Simplify Immutable Data

Java 16 finalized record types — concise, immutable data carriers — signaling that the language itself now encourages immutability by default.

This history reveals a consistent trend: as software systems grow more complex and more concurrent, controlling when and how data changes becomes increasingly important. The central question this lesson addresses is: how do side effects and mutability introduce bugs, and what strategies does Java offer to prevent them?

SECTION 2

Core Principles & Definitions

Before diving into code, you need a clear vocabulary. The two main ideas in this lesson — side effects and immutability — are connected. When an object is immutable, it cannot produce certain kinds of side effects because its state never changes. Understanding both concepts together gives you a powerful framework for writing safer Java programs.

1

Side Effect

Any observable change a method makes beyond returning a value. Examples include modifying a field, writing to a file, or printing to the console.
2

Pure Function

A method with no side effects. Given the same inputs, it always returns the same output and changes nothing outside itself.
3

Immutable Object

An object whose state cannot be modified after construction. Java's String class is the classic example.
4

Mutable Object

An object whose fields or internal data can change over time. ArrayList and StringBuilder are common mutable types in Java.
5

Defensive Copy

A copy of a mutable object made to protect an immutable class from outside modification. This ensures that the original data stays unchanged.
✦

KEY TAKEAWAY

Think of a side effect like writing on a shared whiteboard: anyone else looking at the board sees your changes immediately, even if they weren't expecting them. An immutable object is like a printed handout — once it's printed, nobody can erase or rewrite the words. Everyone who holds a copy can trust that the information on it will stay the same forever.

SECTION 3

Visual Explanation — Side Effects in Action

The diagram below shows what happens when two methods share access to the same mutable object. Method A calls a helper method that unexpectedly mutates the shared list, causing Method B to receive different data than it anticipated. This is the core danger of uncontrolled side effects.

Side Effects: Shared Mutable State ProblemMethod Areads listHelper Methodmutates list ⚠Method Breads listShared Mutable List["Alice", "Bob"]→ ["Alice", "Bob", "Eve"]❌ With Side Effects1. Method A reads ["Alice","Bob"]2. Helper adds "Eve" to list3. Method B reads ["Alice","Bob","Eve"]→ Method B gets unexpected data!✅ Without Side Effects1. Method A reads ["Alice","Bob"]2. Helper returns NEW list with "Eve"3. Method B reads ["Alice","Bob"]→ Original list stays unchanged!
The left scenario shows how a helper method's side effect (adding "Eve" to the shared list) corrupts Method B's expected data. The right scenario shows the safe approach: the helper returns a new list instead of mutating the original, so both Method A and Method B see consistent data.

Notice the critical difference. In the side-effect scenario (left), the helper method modifies data that other methods depend on. In the safe scenario (right), the helper produces a new list and leaves the original alone. This distinction is at the heart of why immutability matters: when objects cannot change, methods that share those objects can never accidentally interfere with each other.

SECTION 4

How Immutability Works in Java

Java does not have a single keyword that makes a class immutable. Instead, you achieve immutability by following a set of design rules. The final keyword is important, but it is only part of the picture. Let's walk through the four rules for creating an immutable class in Java.

Rule 1 — Declare all fields private and final

Marking a field as private prevents outside code from accessing it directly, and marking it final ensures it can only be assigned once — in the constructor. Together, they guarantee that no code can reassign the field after the object is built.

Rule 2 — Do not provide setter methods

A setter (like setName()) is a method whose whole purpose is to change a field's value. If you want your object to be immutable, simply don't write any setters. The only way to set data is through the constructor.

Rule 3 — Declare the class final (or make the constructor private)

If someone can create a subclass of your immutable class, they could override methods and introduce mutable behavior. Declaring the class final prevents subclassing entirely, closing that loophole.

Rule 4 — Make defensive copies of mutable fields

This is the rule most beginners forget. If your class stores a reference to a mutable object (like a Date or an array), outside code could still mutate that object through its original reference. The solution is to copy the mutable object in the constructor and in any getter that returns it. This technique is called a defensive copy.

⚠

Watch Out for final

The final keyword on a variable means the reference cannot be reassigned, but it does not mean the object itself is immutable. A final ArrayList<String> still lets you call .add() and .remove() to change the list's contents. Only the variable's pointer is locked — the data inside is still mutable.

SECTION 5

Types of Side Effects in Java

Not all side effects are bad — without them, your program could never print output, save a file, or communicate over a network. The goal is not to eliminate side effects entirely, but to isolate and control them. The diagram below classifies side effects by type and shows which ones are more dangerous than others.

Classification of Side Effects in JavaOrdered from most dangerous (top) to least dangerous (bottom)1Modifying Shared Mutable StateChanging a field, collection, or static variable that other code reads.list.add(item); counter++; obj.name = "new";2Modifying Input ParametersMutating an object that was passed into your method by the caller.void process(List<String> items) { items.clear(); }3I/O OperationsReading/writing files, database calls, network requests.Files.write(path, data); conn.executeUpdate(sql);4Console OutputPrinting to the screen — observable but rarely causes logical bugs.System.out.println("Processing...");✓Pure Function — No Side EffectsDepends only on inputs; returns a new value; changes nothing.
The danger ranking goes from most dangerous at the top (modifying shared mutable state) to safest at the bottom (pure functions). Console output sits in the middle — it's technically a side effect, but it rarely causes bugs in your program logic.

The key insight from this classification is that you should push side effects to the edges of your program. Keep the core logic as pure functions that take inputs and return outputs. Let a small, well-defined layer handle I/O and state changes. This pattern is sometimes called functional core, imperative shell, and it makes testing dramatically easier since pure functions can be tested without setting up files, databases, or network connections.

SECTION 6

Worked Example — Building an Immutable Class

Let's walk through creating an immutable Student class step by step. We'll start with a mutable version and transform it into a safe, immutable design.

Converting a Mutable Student Class to Immutable

  1. 1

    Step 1 — Identify the Mutable Version

    Here is a typical mutable class that most beginners write: public class Student { private String name; private List<String> courses; public void setName(String n) { name = n; } public List<String> getCourses() { return courses; } }. This class has two problems: it exposes a setter, and the getter returns a direct reference to the internal list.
    Problem identified: setter allows name changes; getter leaks the mutable list.
  2. 2

    Step 2 — Declare the Class final

    Add the final keyword to the class declaration: public final class Student { ... }. This prevents anyone from subclassing Student and overriding methods to introduce mutability.
    Class is now sealed — no subclass can break immutability.
  3. 3

    Step 3 — Make Fields private and final

    Declare both fields with private final: private final String name; private final List<String> courses;. The fields can now only be set once, in the constructor.
    Fields locked — no reassignment possible after construction.
  4. 4

    Step 4 — Remove Setters and Add a Constructor

    Delete setName() entirely. Create a constructor that accepts both values and makes a defensive copy of the mutable list: public Student(String name, List<String> courses) { this.name = name; this.courses = new ArrayList<>(courses); }. The new ArrayList<>(courses) creates a separate copy so changes to the original list won't affect the Student object.
    Defensive copy in constructor protects against external mutation.
  5. 5

    Step 5 — Return Defensive Copies from Getters

    Instead of returning the internal list directly, return a copy or an unmodifiable view: public List<String> getCourses() { return Collections.unmodifiableList(courses); }. Now if someone tries to call .add() on the returned list, Java will throw an UnsupportedOperationException.
    The Student class is now fully immutable. Its state can never change after construction.
💡

Java Records Shortcut

Starting with Java 16, you can use record to create simple immutable data classes in one line: public record Student(String name, List<String> courses) {}. Records automatically generate final fields, a constructor, getters, equals(), hashCode(), and toString(). However, you still need to manually add defensive copies for mutable fields like lists!

SECTION 7

Mutable vs. Immutable — Strengths & Limitations

Immutability is not a silver bullet. There are real tradeoffs to consider when choosing between mutable and immutable designs. Understanding these tradeoffs helps you make informed decisions in your own projects.

CriterionMutable ObjectsImmutable Objects
Thread SafetyRequires explicit synchronization (locks, etc.) to share safely between threads.Inherently thread-safe. Multiple threads can read the same object simultaneously with no risk.
Memory UsageEfficient — one object is updated in place, no extra copies needed.Each change creates a new object, which uses more memory temporarily.
DebuggingHarder — any code with a reference could have changed the object's state.Easier — the object you see is exactly as it was when created.
PerformanceBetter for frequent small updates (e.g., building a long string in a loop).Better for sharing and caching. Can be used safely as HashMap keys.
Code SimplicityCan lead to complex state management, especially in large programs.Simpler to reason about — fewer moving parts, fewer surprises.

Comparing mutable and immutable object design in Java

✦

KEY TAKEAWAY

Think of mutable objects like a shared Google Doc — anyone with the link can edit it at any time, which is great for collaboration but risky if someone makes an unexpected change. Immutable objects are like a PDF — once created, the content is fixed. If you need a different version, you make a new PDF. Use immutability by default and only switch to mutable objects when you have a clear performance reason to do so.

SECTION 8

Connection to Advanced Concepts

The ideas of side effects and immutability you've learned here are foundational to several advanced Java topics. As you continue your studies, you'll encounter these concepts again in more sophisticated forms. The table below previews where these ideas lead.

What You Learned TodayWhere It Leads
Pure functions have no side effectsJava's Stream API uses pure lambda expressions. Operations like .map() and .filter() expect side-effect-free functions.
Immutable objects are thread-safeIn concurrent programming, immutable objects eliminate entire categories of race conditions and deadlocks.
Defensive copies protect internal stateDesign patterns like the Builder Pattern create complex immutable objects step by step, then freeze them.
Mutable vs. immutable tradeoffsPersistent data structures (used in functional languages like Clojure on the JVM) offer efficient immutable collections that share structure.

How today's concepts connect to advanced Java topics

You don't need to master these advanced topics right now. The important thing is to recognize that by understanding side effects and immutability, you're building the mental model that makes functional programming, concurrent programming, and design patterns much easier to learn later.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

A method called calculateAverage(int[] scores) takes an array of test scores and returns the average as a double. It does not modify the array or access any fields outside itself. Is this method a pure function? Explain why or why not.
Show answer
Yes, this method is a pure function. It meets both criteria: it depends only on its input (the scores array) and it produces no side effects (it does not modify the array, does not change any external state, and only returns a value). Every time you pass in the same array of scores, you will get the same average back. This makes the method easy to test, easy to reason about, and safe to use in multi-threaded programs.

Question 2: BASIC CALCULATION

Consider this class: public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } } Is this class immutable? Check it against each of the four rules for immutability and state which rules are satisfied.
Show answer
Yes, the Point class is fully immutable. Rule 1 (fields are private and final): both x and y are declared private final — satisfied. Rule 2 (no setters): there are no setter methods — satisfied. Rule 3 (class is final): the class is declared final — satisfied. Rule 4 (defensive copies of mutable fields): both x and y are primitive ints, which are inherently immutable (they are values, not references), so no defensive copies are needed — satisfied. Since all four rules are met and primitives cannot be mutated, this class is a textbook example of a correct immutable class.

Question 3: INTERMEDIATE

Find the bug in this "immutable" class and explain how to fix it: public final class Roster { private final List<String> names; public Roster(List<String> names) { this.names = names; } public List<String> getNames() { return names; } }
Show answer
This class has two violations of Rule 4 (defensive copies). First, the constructor stores the original list reference directly. If the caller later modifies their list (e.g., callerList.add("Hacker")), the Roster object's internal state changes too. Fix: this.names = new ArrayList<>(names). Second, the getNames() method returns a direct reference to the internal list. Anyone who calls getNames() can call .add() or .remove() on it and mutate the Roster. Fix: return Collections.unmodifiableList(names) or return new ArrayList<>(names). Both fixes together ensure that the internal list can never be modified after construction.

Question 4: APPLIED

You are writing a student grade tracker. The method addGrade(List<Integer> grades, int newGrade) currently works by calling grades.add(newGrade) and returning nothing (void). Rewrite this method so that it has no side effects. Describe what the new method signature would look like and how it would work.
Show answer
Instead of modifying the original list, the method should create and return a new list that contains all the old grades plus the new one. The new signature would be: public static List<Integer> addGrade(List<Integer> grades, int newGrade). Inside the method, you would write: List<Integer> updated = new ArrayList<>(grades); updated.add(newGrade); return updated;. This approach has no side effects because the original grades list is never modified. The caller receives a brand-new list with the additional grade, while the original list remains unchanged. Any other code that references the original list will not be surprised by unexpected changes.

Question 5: CRITICAL THINKING

Java's String class is immutable, yet Java also provides StringBuilder, which is mutable. Why do you think the Java designers chose to include both? Describe a scenario where using only immutable Strings would cause a significant performance problem, and explain how StringBuilder solves it. Then discuss: does using StringBuilder introduce side effects? Why or why not?
Show answer
The Java designers included both because immutability and performance sometimes conflict. Consider building a long string inside a loop, like concatenating 10,000 names: each time you use the + operator on Strings, Java creates an entirely new String object and copies all previous characters into it. After 10,000 iterations, you've created 10,000 temporary objects and copied billions of characters total, making the operation extremely slow (roughly O(n²) time). StringBuilder solves this by maintaining a mutable internal character array that grows as needed, so each append is O(1) amortized. Using StringBuilder does introduce a form of side effect — calling .append() modifies the internal state of the StringBuilder object. However, this is a controlled, local side effect: the StringBuilder is typically created, used, and converted to a String all within one method, so no external code is affected. The key lesson is that you should use immutable objects by default for safety and switch to mutable ones like StringBuilder only in performance-critical situations, keeping the mutation as local as possible.

SUMMARY

Lesson Summary

Summary

A side effect is any observable change a method makes beyond returning a value — modifying a field, writing a file, or printing to the console. A pure function has no side effects and always produces the same output for the same input. An immutable object cannot be changed after construction, which eliminates an entire category of side effects related to shared state. To build an immutable class in Java, follow four rules: declare all fields private final, provide no setters, declare the class final, and make defensive copies of any mutable fields in both constructors and getters.

Not all side effects are equally dangerous — modifying shared mutable state is the most common source of bugs, while console output is usually harmless. The best practice is immutability by default: design objects to be immutable unless you have a specific performance reason to make them mutable. Remember that the final keyword only locks the reference, not the object it points to — a final List can still have items added and removed. These foundational concepts prepare you for Java's Stream API, concurrent programming, and design patterns you'll study in more advanced courses.

Varsity Tutors • Java • Side Effects & Immutability — Understand side effects and immutability considerations (intro)