Immutability is a very important feature that can save you lot of trouble especially in multithreaded environments.

In this write-up, we are going to cover in-depth Lombok @Value annotation and showcase how to use it to create immutable classes in Java.

So, let’s get started!

Quick Introduction to @Value

In short, an immutable object is an object whose state cannot be changed once it’s created.

Immutable objects promote good caching, security and thread saftey, so using them (whenever is possible) is always considered a good practice.

immutability benefits

Implementing an immutable class can be a little bit challenging in Java, because we are required to follow some important rules.

So, this is where Lombok @Value annotation comes into the picture! To simplify the logic of creating immutable objects.

Simple put, @Value annotation is the immutable version of @Data. It’s mainly introduced (in lombok v0.11.4) to make Java classes immutable.

In general, Lombok project provides @Value as a simple shortcut for the following annotations: @Getter, @AllArgsConstructor, @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE), @ToString and @EqualsAndHashCode.

Create an Immutable Class Without Using @Value

Before diving deep into how to use lombok value annotation, let’s see how we would normally create an immutable class using the old way (without using @Value).

In general, in order to implement immutable classes in Java, we need to follow some important key points:

  • Declare your class final to protect it from being extended by other classes

  • Make all fields private

  • Make all mutable fields final

  • Don’t provide setters or methods that can modify the state of an object

  • Initialize all mutable fields via a constructor using deep copy

  • Getters or any method returning a mutable object, must always return a new instance

So, let’s take a close look at how to create an immutable class in Java without using any lombok specific annotations:

    
        public final class Student {
            private final String firstName;
            private final String lastName;
            private final int age;
            
            // All-args constructor
            public Student(String firstName, String lastName, int age) {
                this.firstName   = firstName;
                this.lastName    = lastName;
                this.age         = age;  
            }
            
            // No Setters

            // Getters
            public String getFirstName() {
                return this.firstName;
            }

            public String getLastName() {
                return this.lastName;
            }

            public Date getAge() {
                return this.age;
            }  
        }
    

As we can see, there are more than 30 lines of code! This is too much for an immutable class of 3 fields.

Of course, the code size will increase considerably and grow bigger and bigger with the number of the fields.

Example of Using Lombok @Value Annotation

Lombok has a lot of power when it comes to reducing boilerplate code and spicing up your java code.

@Value provides a fancy way to easily make Java classes and their instances immutable.

In short, by adding the @Value annotation, we tell Lombok to:

  • Declare the marked class final

  • Generate an all-args constructor

  • Mark all fields private and final

  • Add Getter method for every field of the class

  • Generate toString(), hashCode() and hashCode() methods for the annotated class

  • NOT add Setter methods

  • NOT provide a zero argument constructor

Now, let’s see how we can use Lombok value annotation to make our Student class immutable:

    
        @Value
        public class Student {
            
            String firstName;
            String lastName;
            int age;
            
        }
    

Pretty simple, right? All we need to do is just annotate our Student class with @Value annotation, nothing else is required!

lombok value annotation to create immutable class

We can also replace @Value annotation with the following lombok annotations:

  1. @Getter: generates Getter methods for all fields

  2. @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE): makes all fields final and private

  3. @AllArgsConstructor generates a constructor with one argument for every field in the class

  4. @ToString generates toString() method

  5. @EqualsAndHashCode provides equals() and hashCode() methods

    
        @Getter
        @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
        @AllArgsConstructor
        @ToString
        @EqualsAndHashCode
        public class Student {
            // class properties
        }
    

If we de-lombok our Student class, we will get:

    
        public final class Student {
            private final String firstName;
            private final String lastName;
            private final int age;

            public Student(final String firstName, final String lastName, final int age) {
                this.firstName = firstName;
                this.lastName = lastName;
                this.age = age;
            }

            public String getFirstName() {
                return this.firstName;
            }

            public String getLastName() {
                return this.lastName;
            }

            public int getAge() {
                return this.age;
            }

            @Override
            public boolean equals(final Object o) {
                if (o == this) return true;
                if (!(o instanceof Student)) return false;
                final Student other = (Student) o;
                final Object this$firstName = this.getFirstName();
                final Object other$firstName = other.getFirstName();
                if (this$firstName == null ? other$firstName != null : !this$firstName.equals(other$firstName)) return false;
                final Object this$lastName = this.getLastName();
                final Object other$lastName = other.getLastName();
                if (this$lastName == null ? other$lastName != null : !this$lastName.equals(other$lastName)) return false;
                if (this.getAge() != other.getAge()) return false;
                return true;
            }

            @Override
            public int hashCode() {
                final int PRIME = 59;
                int result = 1;
                final Object $firstName = this.getFirstName();
                result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode());
                final Object $lastName = this.getLastName();
                result = result * PRIME + ($lastName == null ? 43 : $lastName.hashCode());
                result = result * PRIME + this.getAge();
                return result;
            }

            @Override
            public String toString() {
                return "Student(firstName=" + this.getFirstName() + ", lastName=" + this.getLastName() + ", age=" + this.getAge() + ")";
            }
        }
    

Note that, lombok value annotation does not generate any setter methods which honors immutability principle.

We can notice also that all fields are maked as final and private. Similarly, the class itself is declared final.

It’s worth mentioning that, we can always change the modifiers of a field using @NonFinal and @PackagePrivate annotations.

Now, let’s do some test around lombok generated methods:

    
        public static void main(String[] args) {
            Student student = new Student("Robert", "Hernandez", 22);
            
            // Testing Getter methods
            System.out.println(student.getFirstName());

            // Testing toString method
            System.out.println(student.toString());
            
            // Testing hashCode method
            System.out.println(student.hashCode());

            // Testing equals method
            Student student2 = new Student("Maria", "Hernandez", 21);
            System.out.println(student.equals(student2));
        }
    

Create Static Factory Method Using @Value

@Value provides a handy attribute called staticConstructor to help us encapsulate the logic of instantiating a class in a separate static factory method.

staticConstructor tells lombok to generate a static factory method and mark the class constructor as private.

    
        @Value(staticConstructor = "of")
        public class Student
    

of” will be the name of the generated static factory method! Let’s de-lombok Student class again to see the changes brought by staticConstructor attribute:

    
        private Student(final String firstName, final String lastName, final int age) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.age = age;
        }

        public static Student of(final String firstName, final String lastName, final int age) {
            return new Student(firstName, lastName, age);
        }
    

As we can see, we can’t call directly the class constructor to create objects as it’s marked with private, we have to use of method: Student student = Student.of(“Linda”, “Depp”, 23);

@Value and Inheritance

Since @Value annotation makes the annotated class final, then we can’t use it at parent class level. The main idea of final keyword is to prevent inheritance.

However, we can use @Data to preserve the default behavior and keep our classes open to inheritance.

Static Fields with @Value

Static fields are not like instance fields because they are bound to the class and not to a particular instance. As a result, they are skipped entirely.

So, what will happen if we define a static field inside a class decorated with @Value? To find out the answer, let’s declare a static field inside a simple Java class.

    
        @Value
        public class Product {
            
            int idPoroduct;
            String name;
            String description;
            double price;
            // Our Static variable
            static int counter;

        }
    

De-lomboking our Product.java will produce the following result:

    
        public final class Product {
        private final int idPoroduct;
        private final String name;
        private final String description;
        private final double price;
        // No changes for counter 
        static int counter;

        public Product(final int idPoroduct, final String name, final String description, final double price) {
            this.idPoroduct = idPoroduct;
            this.name = name;
            this.description = description;
            this.price = price;
        }

        public int getIdPoroduct() {
            return this.idPoroduct;
        }

        public String getName() {
            return this.name;
        }

        public String getDescription() {
            return this.description;
        }

        public double getPrice() {
            return this.price;
        }

        @Override
        public boolean equals(final Object o) {
            if (o == this) return true;
            if (!(o instanceof Product)) return false;
            final Product other = (Product) o;
            if (this.getIdPoroduct() != other.getIdPoroduct()) return false;
            final Object this$name = this.getName();
            final Object other$name = other.getName();
            if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
            final Object this$description = this.getDescription();
            final Object other$description = other.getDescription();
            if (this$description == null ? other$description != null : !this$description.equals(other$description)) return false;
            if (Double.compare(this.getPrice(), other.getPrice()) != 0) return false;
            return true;
        }

        @Override
        public int hashCode() {
            final int PRIME = 59;
            int result = 1;
            result = result * PRIME + this.getIdPoroduct();
            final Object $name = this.getName();
            result = result * PRIME + ($name == null ? 43 : $name.hashCode());
            final Object $description = this.getDescription();
            result = result * PRIME + ($description == null ? 43 : $description.hashCode());
            final long $price = Double.doubleToLongBits(this.getPrice());
            result = result * PRIME + (int) ($price >>> 32 ^ $price);
            return result;
        }

        @Override
        public String toString() {
            return "Product(idPoroduct=" + this.getIdPoroduct() + ", name=" + this.getName() + ", description=" + this.getDescription() + ", price=" + this.getPrice() + ")";
        }
    }
    

counter field is ignored and skipped by lombok! As we can see, the property is not included in the constructor, not declared as private final and no Getter method is generated for it.

@Value and @Builder

Lombok provides @Builder annotation to easily implement builder pattern without writing the required code yourself.

You can always combine @Builder with @Value. However, @Builder will bring some changes to the code generated by @Value annotation.

Let’s see what will happen if we add @Builder annotation to our Product class:

    
        //...
        Product(final int idPoroduct, final String name, final String description, final double price) {
            this.idPoroduct = idPoroduct;
            this.name = name;
            this.description = description;
            this.price = price;
        }


        public static class ProductBuilder {
            private int idPoroduct;
            private String name;
            private String description;
            private double price;

            ProductBuilder() {
            }

            public Product.ProductBuilder idPoroduct(final int idPoroduct) {
                this.idPoroduct = idPoroduct;
                return this;
            }

            public Product.ProductBuilder name(final String name) {
                this.name = name;
                return this;
            }

            public Product.ProductBuilder description(final String description) {
                this.description = description;
                return this;
            }

            public Product.ProductBuilder price(final double price) {
                this.price = price;
                return this;
            }

            public Product build() {
                return new Product(this.idPoroduct, this.name, this.description, this.price);
            }

            @Override
            public String toString() {
                return "Product.ProductBuilder(idPoroduct=" + this.idPoroduct + ", name=" + this.name + ", description=" + this.description + ", price=" + this.price + ")";
            }
        }

        public static Product.ProductBuilder builder() {
            return new Product.ProductBuilder();
        }
        //...
    

As we can see, the visibility scope of All-args constructor changed from public to package. @Builder generated a public static inner class - ProductBuilder - to encapsulate the logic of creating Product objects.

In addition, @Builder annotation generates a static builder() method that uses ProductBuilder class to create Product instances.

Let’s create a new Product object using builder pattern:

    
        Product prd01 = Product.builder()
               .idPoroduct(1)
               .name("Product 01")
               .price(1550)
               .build();
    

Lombok Value Annotation with Jackson

Jackson is a great library that we can use to easily serialize and deserialize our objects. In general, Jackson relies on default constructor or a constructor annotated with @JsonCreator to create objects.

Since @Value generates only an All-args constructor, we’ll need to make some slight changes to make Lombok and Jackson work well together.

Firstly, we need to add @NoArgsConstructor annotation to our class:

    
        @Value
        @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
        @AllArgsConstructor
        public class Product {
        }
    

@NoArgsConstructor overrides the constructor generated by the @Value, this is why we added @AllArgsConstructor annotation again even though it is part of value annotation.

Alternatively, we can decorate class fields with @JsonProperty and add lombok.anyConstructor.addConstructorProperties=true to lombok.config file:

    
        @Value
        public class Product {
            @JsonProperty("idPoroduct")
            int idPoroduct;
            @JsonProperty("name")
            String name;
            @JsonProperty("description")
            String description;
            @JsonProperty("price")
            double price;
        }
    

Now, let’s use Jackson to deserialize a Product object:

    
        ObjectMapper objectMapper = new ObjectMapper();
        String json = "{\"idPoroduct\" : 1, \"name\": \"Prod 01\", \"price\" : 100,\"description\": \"Prod desc\" }";
        Product prod = objectMapper.readValue(json, Product.class);

        System.out.println(prod.getName());
    

@Value Configuration Key

Lombok provides a configuration key for value annotation. The main role of this key is to emit a warning or an error (based on the specified value) message when @Value is activated.

    
        lombok.value.flagUsage = WARNING | ERROR
    

Lombok value configuration key

Conclusion:

That’s all! In this short write-up, we have explained and discussed how to use Lombok @Value annotation.

As always the source code of this article is available over on Github.