Tutorial

How To Create an Immutable Class in Java

Updated on November 17, 2022
authorauthor

Pankaj and Andrea Anderson

How To Create an Immutable Class in Java

Introduction

This article provides an overview of how to create an immutable class in Java programming.

An object is immutable when its state doesn’t change after it has been initialized. For example, String is an immutable class and, once instantiated, the value of a String object never changes. Learn more about why the String class is immutable in Java.

Because an immutable object can’t be updated, programs need to create a new object for every change of state. However, immutable objects also have the following benefits:

  • An immutable class is good for caching purposes because you don’t have to worry about the value changes.
  • An immutable class is inherently thread-safe, so you don’t have to worry about thread safety in multi-threaded environments.

Learn more about multi-threading in Java and browse the Java Multi-Threading Interview Questions.

Creating an Immutable Class in Java

To create an immutable class in Java, you need to follow these general principles:

  1. Declare the class as final so it can’t be extended.
  2. Make all of the fields private so that direct access is not allowed.
  3. Don’t provide setter methods for variables.
  4. Make all mutable fields final so that a field’s value can be assigned only once.
  5. Initialize all fields using a constructor method performing deep copy.
  6. Perform cloning of objects in the getter methods to return a copy rather than returning the actual object reference.

The following class is an example that illustrates the basics of immutability. The FinalClassExample class defines the fields and provides the constructor method that uses deep copy to initialize the object. The code in the main method of the FinalClassExample.java file tests the immutability of the object.

Create a new file called FinalClassExample.java and copy in the following code:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// fields of the FinalClassExample class
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Getter function for mutable objects

	public HashMap<String, String> getTestMap() {
		return (HashMap<String, String>) testMap.clone();
	}

	// Constructor method performing deep copy
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// "this" keyword refers to the current object
		this.id=i;
		this.name=n;

		HashMap<String,String> tempMap=new HashMap<String,String>();
		String key;
		Iterator<String> it = hm.keySet().iterator();
		while(it.hasNext()){
			key=it.next();
			tempMap.put(key, hm.get(key));
		}
		this.testMap=tempMap;
	}

	// Test the immutable class

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// print the ce values
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// change the local variable values
		i=20;
		s="modified";
		h1.put("3", "third");
		// print the values again
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Compile and run the program:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Note: You might get the following message when you compile the file: Note: FinalClassExample.java uses unchecked or unsafe operations because the getter method is using an unchecked cast from HashMap<String,String> to Object. You can ignore the compiler warning for the purposes of this example.

You get the following output:

Output
Performing Deep Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second} ce testMap after changing variable from getter methods: {1=first, 2=second}

The output shows that the HashMap values didn’t change because the constructor uses deep copy and the getter function returns a clone of the original object.

What happens when you don’t use deep copy and cloning

You can make changes to the FinalClassExample.java file to show what happens when you use shallow copy instead of deep copy and return the object insetad of a copy. The object is no longer immutable and can be changed. Make the following changes to the example file (or copy and paste from the code example):

  • Delete the constructor method providing deep copy and add the constructor method providing shallow copy that is highlighted in the following example.
  • In the getter function, delete return (HashMap<String, String>) testMap.clone(); and add return testMap;.

The example file should now look like this:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// fields of the FinalClassExample class
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Getter function for mutable objects

	public HashMap<String, String> getTestMap() {
		return testMap;
	}

	//Constructor method performing shallow copy

	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Shallow Copy for Object initialization");
		this.id=i;
		this.name=n;
		this.testMap=hm;
	}

	// Test the immutable class

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// print the ce values
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// change the local variable values
		i=20;
		s="modified";
		h1.put("3", "third");
		// print the values again
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Compile and run the program:

  1. javac FinalClassExample.java
  2. java FinalClassExample

You get the following output:

Output
Performing Shallow Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second, 3=third} ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}

The output shows that the HashMap values got changed because the constructor method uses shallow copy there is a direct reference to the original object in the getter function.

Conclusion

You’ve learned some of the general principles to follow when you create immutable classes in Java, including the importance of deep copy. Continue your learning with more Java tutorials.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author(s)

Category:
Tutorial
Tags:

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
JournalDev
DigitalOcean Employee
DigitalOcean Employee badge
December 23, 2010

… and here is a translation of the entire exercise into Scala, except without the wasteful copying and cloning, and with correct equals and hashCode methods: case class FinalClassExample(id: Int, name: String, testMap: Map[String,String])

- Ken

JournalDev
DigitalOcean Employee
DigitalOcean Employee badge
December 23, 2010

I didn’t understood what are you trying to point here?

- Pankaj

JournalDev
DigitalOcean Employee
DigitalOcean Employee badge
December 23, 2010

I belive that he was trying to point out that Scala lets you create immutable classes in more concise way. I’m new to Scala myself and already I love and often use these one liners. Less code less opportunities to make mistakes :)

- Paweł Chłopek

    JournalDev
    DigitalOcean Employee
    DigitalOcean Employee badge
    December 23, 2010

    Shouldn’t it be “shallow copy” instead of “swallow copy” unless I am missing something?

    - Shantanu Kumar

    JournalDev
    DigitalOcean Employee
    DigitalOcean Employee badge
    December 23, 2010

    Thanks for pointing out the typo error. Corrected now to shallow copy.

    - Pankaj

      JournalDev
      DigitalOcean Employee
      DigitalOcean Employee badge
      December 23, 2010

      why don’t you just do this: import static java.util.Collections.unmodifiableMap; public final class FinalClassExample { ... private final Map testMap; public FinalClassExample(int i, String n, Map m){ id = i; name = n; testMap = unmodifiableMap(new HashMap (m)); } public Map getTestMap() { return testMap; } ... }

      - John

      JournalDev
      DigitalOcean Employee
      DigitalOcean Employee badge
      December 23, 2010

      In this case, when we will get the testMap from getTestMap() function, we will not be allowed to modify it and it will throw exception. Also in that case again we are passing the reference and the values will change accordingly. Try executing this class: package com.journeldev.java; import static java.util.Collections.unmodifiableMap; import java.util.HashMap; import java.util.Map; public final class FinalClassExample1 { private final Map testMap; public Map getTestMap() { return testMap; } public FinalClassExample1(Map hm) { this.testMap = unmodifiableMap(hm); } public static void main(String[] args) { HashMap h1 = new HashMap(); h1.put("1", "first"); h1.put("2", "second"); FinalClassExample1 ce = new FinalClassExample1(h1); System.out.println("ce testMap:" + ce.getTestMap()); h1.put("3", "third"); System.out.println("ce testMap after local variable change:"+ ce.getTestMap()); Map hmTest = ce.getTestMap(); hmTest.put("4", "new"); System.out.println("ce testMap after changing variable from accessor methods:"+ ce.getTestMap()); } } Output will be: ce testMap:{2=second, 1=first} ce testMap after local variable change:{3=third, 2=second, 1=first} Exception in thread “main” java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1285) at com.journeldev.java.FinalClassExample1.main(FinalClassExample1.java:33)

      - Pankaj

      JournalDev
      DigitalOcean Employee
      DigitalOcean Employee badge
      December 23, 2010

      Pankaj, You should not be allowed to modify a map you get from getTestMap. Objects coming from an immutable object, I would expect to also be immutable so you can not change the internals of the object after it is created. A mutable map may imply that the objects map is changed. Sure you can document that the object returned is new, but being defensive upfront seems better. Another alternative is to never return collections. Instead, define a forEach method that takes a function: public void forEach(Function fn) { for(Thing t : Iterable collection) { fn.apply(t); } }

      - Steve

        JournalDev
        DigitalOcean Employee
        DigitalOcean Employee badge
        December 24, 2010

        you didn’t notice that I wrote: this.testMap = modifiableMap(**new HashMap** (m)) `` so you couldn't change the inner map after construction. and I agree with Steve that the map returned by getTestMap() should not be modifiable as this would give the false impression that you're actually changing the immutable object. in that case I would prefer doing something like this: `FinalClassExample example = new FinalClassExample(...); Map newMap = new HashMap(example.getTestMap()); newMap.put("hello", "world");` ``

        - John

          JournalDev
          DigitalOcean Employee
          DigitalOcean Employee badge
          December 23, 2010

          In Groovy you can annotate the class as @Immutable and get /almost/ similar results to the scala example without all the boilerplate. IMHO Scala is better for it’s immutable support though. Also, don’t forget that Java Date, Dimension, and other JDK classes are not immutable as well, so you need to make defensive copies of those classes as well.

          - Hamlet D’Arcy

          JournalDev
          DigitalOcean Employee
          DigitalOcean Employee badge
          December 23, 2010

          Exactly, for all the mutable objects we need to return the defensive copy rather than same object reference. Have to dig into Scala now.

          - Pankaj

            JournalDev
            DigitalOcean Employee
            DigitalOcean Employee badge
            December 23, 2010

            Out of curiosity, why the requirement to have the class be marked as final so as not to be extended? What does disallowing subclasses actually provide in terms of allowing objects of this type to be immutable? Further, you don’t have to mark fields as private only just so long as you can guarantee that all constructor’s of the class properly initialize all of the fields. As a side note, you *can* have setters, but with the nuance that instead of changing an internal field, what the setter really does is specify a return type of the class the method is on, and then behind the scenes creates a new object using a constructor that accepts all internal fields, using the internally held state in for all params with the exception of the field represented by the setter called since you want the new object to have that field updated.

            - whaley

            JournalDev
            DigitalOcean Employee
            DigitalOcean Employee badge
            December 23, 2010

            If the class is not marked as final then its function can be overridden in the subclass either accidentally or intentionally. So its more related to keep the object secure. For this either all the getter methods can be made final or the class itself - this is again a design decision and depends on the requirement. Again if the fields wont be private then client application can override the value. Make the HashMap as public in the code and run the below code to see yourself. FinalClassExample fce = new FinalClassExample(1,"", new HashMap()); System.out.println(fce.testMap); HashMap hm = fce.testMap; hm.put("1", "1"); System.out.println(fce.testMap); Having a setter function will give the feeling that the actual object has been modified whereas internally creating a new object. Its better to client application know that its immutable (like String).

            - Pankaj

              JournalDev
              DigitalOcean Employee
              DigitalOcean Employee badge
              May 5, 2011

              It’s super webpage, I was looking for something like this

              - Łomża Zuhlke

                JournalDev
                DigitalOcean Employee
                DigitalOcean Employee badge
                June 2, 2012

                Thanks mate, great details… – Anish Sneh

                - Anish Sneh

                JournalDev
                DigitalOcean Employee
                DigitalOcean Employee badge
                October 17, 2012

                Thanks for the kind words.

                - Pankaj

                  JournalDev
                  DigitalOcean Employee
                  DigitalOcean Employee badge
                  September 19, 2013

                  Thanks, you know it and you know how to explain it too! I will definitely read more of your articles :)

                  - Mirey

                    JournalDev
                    DigitalOcean Employee
                    DigitalOcean Employee badge
                    August 18, 2014

                    Thanks for the detailed tutorial, well written and the flow goes exactely to showing up almost the need of every instruction in the code :) One side question, even if I know we are talking about Objects immutability,but what about the other instance variables you introucted in the FinalClassExample (id, name)? Is there any way to make them immutable?

                    - Marwen

                    JournalDev
                    DigitalOcean Employee
                    DigitalOcean Employee badge
                    August 18, 2014

                    int and String both are already immutable, since there are no setter methods for them. For any other class variables, you should return a deep copy of the variable to avoid mutability.

                    - Pankaj

                      JournalDev
                      DigitalOcean Employee
                      DigitalOcean Employee badge
                      November 10, 2014

                      Should not be String name declared as a not final? Its not mutable anyway.

                      - Ramakant

                        JournalDev
                        DigitalOcean Employee
                        DigitalOcean Employee badge
                        November 22, 2014

                        Hi Pankaj, It was a great learning about creating Immutable objects.If you are performing step 5 and 6 then step 4 is not required I guess. You are not storing or returning original reference of HashMap, You are using clone concept for that, Hence as a result client application have no way to reassign new object to declared HaspMap. Please correct me If I am missing some thing Thanks& Regards Rais Alam

                        - Rais Alam

                          JournalDev
                          DigitalOcean Employee
                          DigitalOcean Employee badge
                          February 2, 2015

                          Can you please describe no 6. more deeply.I am not able to understand it.

                          - mahi

                            JournalDev
                            DigitalOcean Employee
                            DigitalOcean Employee badge
                            February 12, 2015

                            Excellent post!!

                            - S

                              JournalDev
                              DigitalOcean Employee
                              DigitalOcean Employee badge
                              March 7, 2015

                              thanx i disable ABP for u

                              - ahmed

                                JournalDev
                                DigitalOcean Employee
                                DigitalOcean Employee badge
                                March 14, 2015

                                I can modify the object using ce.testMap.put(“10”, “ten”); Output: Performing Deep Copy for Object initialization true false ce id:10 ce name:original ce testMap:{2=second, 1=first} ce id after local variable change:10 ce name after local variable change:original ce testMap after local variable change:{2=second, 1=first, 10=ten} ce testMap after changing variable from accessor methods:{2=second, 1=first, 10=ten}

                                - Bijoy

                                  JournalDev
                                  DigitalOcean Employee
                                  DigitalOcean Employee badge
                                  September 14, 2015

                                  private final String name; …Why this is final …String is already final …do we need to declare it again…???

                                  - Ajaz

                                    JournalDev
                                    DigitalOcean Employee
                                    DigitalOcean Employee badge
                                    December 18, 2015

                                    Thank you Pankaj. I’m a big fan of your writing skills. You cover every details and explain concepts in easy to understand language. Thanks again.

                                    - Vijay Nandwana

                                      JournalDev
                                      DigitalOcean Employee
                                      DigitalOcean Employee badge
                                      December 22, 2015

                                      Very easy to understand and useful post!!

                                      - Vineet kaushik

                                        JournalDev
                                        DigitalOcean Employee
                                        DigitalOcean Employee badge
                                        December 28, 2015

                                        Thanks for great article! Can we use testMap.clone() in the constructor instead of going through all of the map items with Iterator?

                                        - Bektur Toktosunov

                                          JournalDev
                                          DigitalOcean Employee
                                          DigitalOcean Employee badge
                                          January 13, 2017

                                          Hi Pankaj I didn’t Understood the System.out.println(h1 == ce.getTestMap()) answer is False. Can you Please explain why it is false.

                                          - WAA

                                            Try DigitalOcean for free

                                            Click below to sign up and get $200 of credit to try our products over 60 days!

                                            Sign up

                                            Join the Tech Talk
                                            Success! Thank you! Please check your email for further details.

                                            Please complete your information!

                                            Become a contributor for community

                                            Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

                                            DigitalOcean Documentation

                                            Full documentation for every DigitalOcean product.

                                            Resources for startups and SMBs

                                            The Wave has everything you need to know about building a business, from raising funding to marketing your product.

                                            Get our newsletter

                                            Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

                                            New accounts only. By submitting your email you agree to our Privacy Policy

                                            The developer cloud

                                            Scale up as you grow — whether you're running one virtual machine or ten thousand.

                                            Get started for free

                                            Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

                                            *This promotional offer applies to new accounts only.