From 481543bf262af3dada4960400dfa25ea62a70e69 Mon Sep 17 00:00:00 2001 From: alex052525 Date: Thu, 4 Jun 2026 16:24:26 +0900 Subject: [PATCH 1/2] feat: implement Immutable pattern #3448 Add immutable/ module with ImmutableUser (final class, defensive copy, withAge wither method), App demo, JUnit 5 tests, PlantUML diagram, and README following project conventions. Co-Authored-By: Claude Sonnet 4.6 --- immutable/README.md | 133 ++++++++++++++++++ immutable/etc/immutable.urm.puml | 21 +++ immutable/pom.xml | 70 +++++++++ .../main/java/com/iluwatar/immutable/App.java | 61 ++++++++ .../com/iluwatar/immutable/ImmutableUser.java | 96 +++++++++++++ .../iluwatar/immutable/ImmutableUserTest.java | 87 ++++++++++++ pom.xml | 1 + 7 files changed, 469 insertions(+) create mode 100644 immutable/README.md create mode 100644 immutable/etc/immutable.urm.puml create mode 100644 immutable/pom.xml create mode 100644 immutable/src/main/java/com/iluwatar/immutable/App.java create mode 100644 immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java create mode 100644 immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java diff --git a/immutable/README.md b/immutable/README.md new file mode 100644 index 000000000000..3d780ccf3109 --- /dev/null +++ b/immutable/README.md @@ -0,0 +1,133 @@ +--- +title: "Immutable Pattern in Java: Building Thread-Safe Objects" +shortTitle: Immutable +description: "Learn the Immutable pattern in Java with real-world examples, class diagrams, and tutorials. Understand how to create objects that cannot be modified after construction." +category: Idiom +language: en +tag: + - Immutability + - Thread safety + - Concurrency + - Object composition +--- + +## Also known as + +Value Object (when applied strictly to small domain values) + +## Intent of Immutable Design Pattern + +Ensure that an object's state cannot be changed after it is constructed, making it inherently thread-safe and easier to reason about. + +## Detailed Explanation of Immutable Pattern with Real-World Examples + +Real-world example + +> A birth certificate is a perfect real-world analogy for the Immutable pattern. Once issued, a birth certificate records a person's name, date of birth, and place of birth permanently. You cannot alter the certificate itself; if a legal correction is needed, a new certificate is issued. The original document remains unchanged, guaranteeing that every copy handed to a bank, school, or government office reflects exactly the same facts. + +In plain words + +> An immutable object is one whose state is fixed at construction time and can never change. Instead of modifying an existing object, you create a new one with the desired state. + +Wikipedia says + +> In object-oriented and functional programming, an immutable object (unchangeable object) is an object whose state cannot be modified after it is created. This is in contrast to a mutable object (changeable object), which can be modified after it is created. + +Class diagram + +![Immutable class diagram](./etc/immutable.urm.puml) + +## Programmatic Example of Immutable Pattern in Java + +The core of the pattern is `ImmutableUser`. All fields are `final`, the mutable `roles` list is defensively copied via `List.copyOf`, and "mutation" is expressed by returning a new instance. + +```java +public final class ImmutableUser { + + private final String name; + private final int age; + private final List roles; + + public ImmutableUser(String name, int age, List roles) { + this.name = name; + this.age = age; + this.roles = List.copyOf(roles); + } + + public String getName() { return name; } + public int getAge() { return age; } + public List getRoles() { return roles; } + + public ImmutableUser withAge(int newAge) { + return new ImmutableUser(this.name, newAge, this.roles); + } +} +``` + +`App` demonstrates the pattern in action: + +```java +var alice = new ImmutableUser("Alice", 30, List.of("admin", "user")); +LOGGER.info("Original user: {}", alice); + +var olderAlice = alice.withAge(31); +LOGGER.info("Updated user (new object): {}", olderAlice); +LOGGER.info("Original is unchanged: {}", alice); + +var mutableRoles = new ArrayList<>(List.of("viewer")); +var bob = new ImmutableUser("Bob", 25, mutableRoles); +mutableRoles.add("editor"); +LOGGER.info("Bob's roles (unchanged despite external list mutation): {}", bob.getRoles()); +``` + +Running the example produces output similar to: + +``` +INFO com.iluwatar.immutable.App - Original user: ImmutableUser{name='Alice', age=30, roles=[admin, user]} +INFO com.iluwatar.immutable.App - Updated user (new object): ImmutableUser{name='Alice', age=31, roles=[admin, user]} +INFO com.iluwatar.immutable.App - Original is unchanged: ImmutableUser{name='Alice', age=30, roles=[admin, user]} +INFO com.iluwatar.immutable.App - Bob's roles (unchanged despite external list mutation): [viewer] +``` + +## When to Use the Immutable Pattern in Java + +* When objects are shared across threads and synchronization overhead is undesirable. +* When you need objects to be used safely as map keys or in sets (consistent `hashCode`). +* When you want to model value types such as money, dates, or coordinates. +* When defensive programming is critical and you must prevent accidental state corruption. + +## Real-World Applications of Immutable Pattern in Java + +* `java.lang.String` — the quintessential immutable class in the JDK. +* `java.time.LocalDate`, `LocalDateTime` — immutable date/time representations. +* `java.math.BigDecimal`, `BigInteger` — immutable numeric types. +* Record classes introduced in Java 16 — compiler-generated immutable data carriers. + +## Benefits and Trade-offs of Immutable Pattern + +Benefits: + +* **Thread safety**: No synchronization needed; immutable objects can be shared freely across threads. +* **Simplicity**: Absence of state changes eliminates a whole category of bugs. +* **Safe sharing**: Can be freely passed to untrusted code without defensive copying at call sites. +* **Cache-friendly**: Immutable objects can be cached, interned, or pre-computed without risk. + +Trade-offs: + +* **Object creation overhead**: Every logical "update" allocates a new object, which may pressure the garbage collector in hot paths. +* **Verbose construction**: Complex objects often require a Builder to avoid unwieldy constructors. +* **Not always applicable**: Objects that model inherently stateful entities (e.g., a network connection) cannot reasonably be immutable. + +## Related Java Design Patterns + +* [Value Object](https://java-design-patterns.com/patterns/value-object/): Overlapping concept; value objects are typically immutable and compared by value rather than identity. +* [Builder](https://java-design-patterns.com/patterns/builder/): Commonly paired with Immutable to construct complex objects step-by-step before freezing them. +* [Prototype](https://java-design-patterns.com/patterns/prototype/): Cloning a mutable object is an alternative to immutability when shared state must occasionally change. +* [Flyweight](https://java-design-patterns.com/patterns/flyweight/): Leverages immutability to safely share fine-grained objects across many contexts. + +## References and Credits + +* [Effective Java, 3rd Edition — Item 17: Minimize Mutability](https://amzn.to/3JIYJoL) +* [Java Concurrency in Practice](https://amzn.to/3vXyUEh) +* [Clean Code: A Handbook of Agile Software Craftsmanship](https://amzn.to/3JIYJoL) +* [Wikipedia — Immutable object](https://en.wikipedia.org/wiki/Immutable_object) diff --git a/immutable/etc/immutable.urm.puml b/immutable/etc/immutable.urm.puml new file mode 100644 index 000000000000..a455b1eab93b --- /dev/null +++ b/immutable/etc/immutable.urm.puml @@ -0,0 +1,21 @@ +@startuml +package com.iluwatar.immutable { + class App { + - LOGGER : Logger {static} + + App() + + main(args : String[]) {static} + } + class ImmutableUser { + - name : String {final} + - age : int {final} + - roles : List {final} + + ImmutableUser(name : String, age : int, roles : List) + + getName() : String + + getAge() : int + + getRoles() : List + + withAge(newAge : int) : ImmutableUser + + toString() : String + } +} +App ..> ImmutableUser : creates +@enduml diff --git a/immutable/pom.xml b/immutable/pom.xml new file mode 100644 index 000000000000..b99704664fdf --- /dev/null +++ b/immutable/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + immutable + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.immutable.App + + + + + + + + + diff --git a/immutable/src/main/java/com/iluwatar/immutable/App.java b/immutable/src/main/java/com/iluwatar/immutable/App.java new file mode 100644 index 000000000000..c49cbe041e2d --- /dev/null +++ b/immutable/src/main/java/com/iluwatar/immutable/App.java @@ -0,0 +1,61 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.immutable; + +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Immutable pattern ensures that an object's state cannot be changed after construction. + * + *

In this example, {@link ImmutableUser} demonstrates the pattern: all fields are final, the + * mutable {@code roles} list is defensively copied, and any state change produces a brand-new + * instance via {@link ImmutableUser#withAge(int)}. + */ +public class App { + + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + /** + * Program entry point. + * + * @param args command line args + */ + public static void main(String[] args) { + var alice = new ImmutableUser("Alice", 30, List.of("admin", "user")); + LOGGER.info("Original user: {}", alice); + + var olderAlice = alice.withAge(31); + LOGGER.info("Updated user (new object): {}", olderAlice); + LOGGER.info("Original is unchanged: {}", alice); + + // Demonstrate defensive copy: mutating the source list does not affect alice + var mutableRoles = new java.util.ArrayList<>(List.of("viewer")); + var bob = new ImmutableUser("Bob", 25, mutableRoles); + mutableRoles.add("editor"); + LOGGER.info("Bob's roles (unchanged despite external list mutation): {}", bob.getRoles()); + } +} diff --git a/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java b/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java new file mode 100644 index 000000000000..87d245027915 --- /dev/null +++ b/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java @@ -0,0 +1,96 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.immutable; + +import java.util.List; + +/** + * An immutable representation of a user. + * + *

All fields are final and set only at construction time. The {@code roles} list is defensively + * copied to prevent external mutation. Any "modification" produces a new {@link ImmutableUser} + * instance, leaving the original unchanged. + */ +public final class ImmutableUser { + + private final String name; + private final int age; + private final List roles; + + /** + * Constructs an {@link ImmutableUser} with the given attributes. + * + * @param name the user's name + * @param age the user's age + * @param roles the user's roles; copied defensively so external changes have no effect + */ + public ImmutableUser(String name, int age, List roles) { + this.name = name; + this.age = age; + this.roles = List.copyOf(roles); + } + + /** + * Returns the user's name. + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Returns the user's age. + * + * @return age + */ + public int getAge() { + return age; + } + + /** + * Returns an unmodifiable view of the user's roles. + * + * @return roles + */ + public List getRoles() { + return roles; + } + + /** + * Returns a new {@link ImmutableUser} identical to this one but with the given age. + * + * @param newAge the new age value + * @return a new instance with the updated age + */ + public ImmutableUser withAge(int newAge) { + return new ImmutableUser(this.name, newAge, this.roles); + } + + @Override + public String toString() { + return "ImmutableUser{name='" + name + "', age=" + age + ", roles=" + roles + '}'; + } +} diff --git a/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java b/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java new file mode 100644 index 000000000000..0aa6dac92def --- /dev/null +++ b/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java @@ -0,0 +1,87 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.immutable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ImmutableUserTest { + + @Test + void constructorSetsAllFields() { + var user = new ImmutableUser("Alice", 30, List.of("admin")); + assertEquals("Alice", user.getName()); + assertEquals(30, user.getAge()); + assertEquals(List.of("admin"), user.getRoles()); + } + + @Test + void withAgeReturnsNewObjectWithUpdatedAge() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + var updated = original.withAge(31); + + assertEquals(31, updated.getAge()); + assertEquals("Alice", updated.getName()); + assertEquals(original.getRoles(), updated.getRoles()); + } + + @Test + void withAgeDoesNotMutateOriginal() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + original.withAge(99); + + assertEquals(30, original.getAge()); + } + + @Test + void withAgeReturnsDistinctInstance() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + var updated = original.withAge(31); + + assertNotSame(original, updated); + } + + @Test + void defensiveCopyPreventsExternalListMutation() { + var mutableRoles = new ArrayList<>(List.of("viewer")); + var user = new ImmutableUser("Bob", 25, mutableRoles); + + mutableRoles.add("editor"); + + assertEquals(List.of("viewer"), user.getRoles()); + } + + @Test + void getRolesReturnsUnmodifiableList() { + var user = new ImmutableUser("Bob", 25, List.of("viewer")); + + assertThrows(UnsupportedOperationException.class, () -> user.getRoles().add("editor")); + } +} diff --git a/pom.xml b/pom.xml index 3403c4607f37..c654c5c62dd0 100644 --- a/pom.xml +++ b/pom.xml @@ -147,6 +147,7 @@ health-check hexagonal-architecture identity-map + immutable intercepting-filter interpreter iterator From bc9f9465668f4ea434db6af1a789225e5b986a01 Mon Sep 17 00:00:00 2001 From: alex052525 Date: Thu, 4 Jun 2026 16:37:33 +0900 Subject: [PATCH 2/2] feat: add withName and withRoles wither methods to ImmutableUser #3448 Every field now has a corresponding wither for consistency. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iluwatar/immutable/ImmutableUser.java | 20 +++++++++++ .../iluwatar/immutable/ImmutableUserTest.java | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java b/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java index 87d245027915..4b4f38de4ac2 100644 --- a/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java +++ b/immutable/src/main/java/com/iluwatar/immutable/ImmutableUser.java @@ -79,6 +79,16 @@ public List getRoles() { return roles; } + /** + * Returns a new {@link ImmutableUser} identical to this one but with the given name. + * + * @param newName the new name value + * @return a new instance with the updated name + */ + public ImmutableUser withName(String newName) { + return new ImmutableUser(newName, this.age, this.roles); + } + /** * Returns a new {@link ImmutableUser} identical to this one but with the given age. * @@ -89,6 +99,16 @@ public ImmutableUser withAge(int newAge) { return new ImmutableUser(this.name, newAge, this.roles); } + /** + * Returns a new {@link ImmutableUser} identical to this one but with the given roles. + * + * @param newRoles the new roles; copied defensively + * @return a new instance with the updated roles + */ + public ImmutableUser withRoles(List newRoles) { + return new ImmutableUser(this.name, this.age, newRoles); + } + @Override public String toString() { return "ImmutableUser{name='" + name + "', age=" + age + ", roles=" + roles + '}'; diff --git a/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java b/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java index 0aa6dac92def..0479aff68d89 100644 --- a/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java +++ b/immutable/src/test/java/com/iluwatar/immutable/ImmutableUserTest.java @@ -84,4 +84,40 @@ void getRolesReturnsUnmodifiableList() { assertThrows(UnsupportedOperationException.class, () -> user.getRoles().add("editor")); } + + @Test + void withNameReturnsNewObjectWithUpdatedName() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + var updated = original.withName("Bob"); + + assertEquals("Bob", updated.getName()); + assertEquals(30, updated.getAge()); + assertEquals(original.getRoles(), updated.getRoles()); + } + + @Test + void withNameDoesNotMutateOriginal() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + original.withName("Bob"); + + assertEquals("Alice", original.getName()); + } + + @Test + void withRolesReturnsNewObjectWithUpdatedRoles() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + var updated = original.withRoles(List.of("viewer", "editor")); + + assertEquals(List.of("viewer", "editor"), updated.getRoles()); + assertEquals("Alice", updated.getName()); + assertEquals(30, updated.getAge()); + } + + @Test + void withRolesDoesNotMutateOriginal() { + var original = new ImmutableUser("Alice", 30, List.of("admin")); + original.withRoles(List.of("viewer")); + + assertEquals(List.of("admin"), original.getRoles()); + } }