How do I create a record to store data in Java?

A record is a concise way to define a class that holds data. Java automatically provides a constructor, accessor methods, and implementations of equals(), hashCode(), and toString().

In this example, you will create a Book record to store a title and an author.

Source code

Save the following code as RecordExample.java:

public class RecordExample {
    void main() {
        Book firstBook = new Book("Learning Java", "Alice");
        Book secondBook = new Book("Learning Java", "Alice");

        IO.println("Title: " + firstBook.title());
        IO.println("Author: " + firstBook.author());
        IO.println("Record: " + firstBook);
        IO.println("Equal values: " + firstBook.equals(secondBook));
        IO.println("Same object: " + (firstBook == secondBook));
    }
}

record Book(String title, String author) {
}

Explanation

The following declaration defines a record with two components:

record Book(String title, String author) {
}

The components are title and author, both of type String.

For this declaration, Java automatically provides:

Member Purpose
Book(String title, String author) Initializes the components
title() Returns the title
author() Returns the author
equals(Object other) Compares component values
hashCode() Calculates a hash code from the components
toString() Returns a text representation of the record

Creating a record object

Use the new keyword and pass a value for each component:

Book firstBook = new Book("Learning Java", "Alice");

The arguments follow the order of the components in the record declaration.

Reading component values

Record accessors use the component names directly:

firstBook.title()
firstBook.author()

The generated methods are named title() and author(), rather than getTitle() and getAuthor().

Each component has a private final field. Records do not generate setters, so you cannot reassign these fields after construction.

Comparing records

The example creates two separate objects with the same component values:

Book firstBook = new Book("Learning Java", "Alice");
Book secondBook = new Book("Learning Java", "Alice");

The generated equals() method returns true because both objects are instances of the same record class and have equal component values.

The == operator returns false because the variables refer to separate objects.

Printing a record

Passing the record to IO.println() uses its generated toString() method:

IO.println("Record: " + firstBook);

The result includes the record name, component names, and their values.

Run the example

Compile and run the file:

javac RecordExample.java
java RecordExample

Output

Title: Learning Java
Author: Alice
Record: Book[title=Learning Java, author=Alice]
Equal values: true
Same object: false

Are records immutable?

Records are shallowly immutable: their component fields cannot be reassigned, but referenced objects may still be mutable.

This Book record contains only strings, which are immutable. If a record contains a mutable list or array, additional steps are needed to protect that data from changes.

How do I define and use an enum in Java?

An enum defines a fixed set of named constants. You can use it to represent values such as order statuses, directions, or days of the week.

In this example, you will define an enum for order statuses and use a switch expression to select a message.

Source code

Save the following code as EnumExample.java:

public class EnumExample {
    void main() {
        OrderStatus status = OrderStatus.SHIPPED;

        String message = switch (status) {
            case PENDING -> "Your order is waiting for confirmation.";
            case PROCESSING -> "Your order is being prepared.";
            case SHIPPED -> "Your order is on its way.";
            case DELIVERED -> "Your order has arrived.";
        };

        IO.println("Current status: " + status);
        IO.println("Message: " + message);

        IO.println();
        IO.println("Available statuses:");

        for (OrderStatus availableStatus : OrderStatus.values()) {
            IO.println(availableStatus);
        }
    }
}

enum OrderStatus {
    PENDING,
    PROCESSING,
    SHIPPED,
    DELIVERED
}

Explanation

The enum keyword declares a type with a fixed set of constants:

enum OrderStatus {
    PENDING,
    PROCESSING,
    SHIPPED,
    DELIVERED
}

Enum constant names conventionally use uppercase letters, with underscores separating words.

Assigning an enum value

Use the enum type followed by a constant name:

OrderStatus status = OrderStatus.SHIPPED;

The variable status can hold a reference to an OrderStatus constant. You cannot assign a string such as "SHIPPED" directly to it.

This helps prevent mistakes caused by spelling or capitalization differences in string values.

Using an enum in a switch expression

The switch expression selects a message for the current status:

String message = switch (status) {
    case PENDING -> "Your order is waiting for confirmation.";
    case PROCESSING -> "Your order is being prepared.";
    case SHIPPED -> "Your order is on its way.";
    case DELIVERED -> "Your order has arrived.";
};

Inside these case labels, you can use the constant names without the OrderStatus. prefix.

The expression covers every constant declared in OrderStatus, so a default rule is unnecessary.

Looping through enum constants

Every enum provides a values() method that returns an array of its constants in declaration order:

for (OrderStatus availableStatus : OrderStatus.values()) {
    IO.println(availableStatus);
}

Here, the loop prints all four statuses, starting with PENDING.

Run the example

Compile and run the file:

javac EnumExample.java
java EnumExample

Output

Current status: SHIPPED
Message: Your order is on its way.

Available statuses:
PENDING
PROCESSING
SHIPPED
DELIVERED

Use an enum when a value should come from a known set of choices. It gives those choices meaningful names and lets the compiler check their usage.

How do I override a method in Java?

Method overriding lets a subclass provide its own implementation of an inherited instance method.

In this example, you will override the printDetails() method so an ebook displays its file format along with its title and author.

Source code

Save the following code as MethodOverridingExample.java:

public class MethodOverridingExample {
    void main() {
        Book book = new Book("Learning Java", "Alice");
        Book ebook = new Ebook("Exploring Java", "Bob", "PDF");

        book.printDetails();
        IO.println();
        ebook.printDetails();
    }
}

class Book {
    private final String title;
    private final String author;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public void printDetails() {
        IO.println("Title: " + title);
        IO.println("Author: " + author);
    }
}

class Ebook extends Book {
    private final String fileFormat;

    Ebook(String title, String author, String fileFormat) {
        super(title, author);
        this.fileFormat = fileFormat;
    }

    @Override
    public void printDetails() {
        super.printDetails();
        IO.println("File format: " + fileFormat);
    }
}

Explanation

The Book class defines a method that prints its title and author:

public void printDetails() {
    IO.println("Title: " + title);
    IO.println("Author: " + author);
}

The Ebook class extends Book and overrides that method:

@Override
public void printDetails() {
    super.printDetails();
    IO.println("File format: " + fileFormat);
}

The overriding method has the same name and parameter list as the inherited method. Here, it also has the same return type, void.

Using the @Override annotation

The @Override annotation tells the compiler that the method is intended to override an inherited method.

If you misspell the method name or use the wrong parameters, the compiler reports an error. This helps catch mistakes before running the program.

Calling the superclass implementation

Inside the overriding method, this statement calls the implementation in Book:

super.printDetails();

It prints the title and author. The next statement adds the ebook’s file format:

IO.println("File format: " + fileFormat);

Calling super.printDetails() is optional. This example uses it to reuse the existing printing code.

Selecting the method at runtime

The following variable has the declared type Book, but refers to an Ebook object:

Book ebook = new Ebook("Exploring Java", "Bob", "PDF");

When you call:

ebook.printDetails();

Java runs the implementation in Ebook because that is the object’s actual class. This behavior is called runtime polymorphism.

Run the example

Compile and run the file:

javac MethodOverridingExample.java
java MethodOverridingExample

Output

Title: Learning Java
Author: Alice

Title: Exploring Java
Author: Bob
File format: PDF

Overriding changes an inherited instance method’s behavior in a subclass. Overloading, covered earlier, defines methods with the same name but different parameter lists.

How do I protect a List inside a Java record?

A record is a convenient way to represent data, but a record containing a mutable list can still change unexpectedly. In this tutorial, you will protect a reading list from changes made through other references.

Records became a final language feature in Java 16. This tutorial uses Java 17 as its baseline and requires no preview or incubator flags. See Java’s language changes by release.

1. Understand the problem

Consider this record:

record ReadingList(String title, List<String> books) {
}

Java generates its constructor and accessors. However, the generated constructor stores the supplied list reference. A caller holding that list can change its contents afterward.

A final field prevents reassignment of the reference; it does not freeze the referenced object. Java’s record documentation explains the fields and methods generated for records.

2. Protect the list during construction

Use a compact constructor to replace the incoming list with an unmodifiable snapshot:

ReadingList {
    books = List.copyOf(books);
}

Assign to the constructor parameter, books. Java assigns the resulting value to the component field after the constructor body completes.

List.copyOf() preserves element order and prevents later changes to the original list from affecting the result. It rejects null collections and null elements. See the Java 17 List API.

3. Create the complete example

Save this code as RecordListExample.java:

import java.util.ArrayList;
import java.util.List;

public class RecordListExample {
    record ReadingList(String title, List<String> books) {
        ReadingList {
            if (title == null || title.isBlank()) {
                throw new IllegalArgumentException(
                        "Title must not be blank");
            }
            books = List.copyOf(books);
        }
    }

    public static void main(String[] args) {
        List<String> original =
                new ArrayList<>(List.of("Java Basics"));

        ReadingList readingList =
                new ReadingList("Weekend", original);

        original.add("Advanced Java");

        System.out.println("Original: " + original);
        System.out.println("Record: " + readingList.books());

        try {
            readingList.books().add("Another Book");
        } catch (UnsupportedOperationException exception) {
            System.out.println("Record list cannot be modified");
        }
    }
}

The example checks two paths: modifying the original list and attempting a modification through the record’s accessor.

4. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 RecordListExample.java
java RecordListExample

Output:

Original: [Java Basics, Advanced Java]
Record: [Java Basics]
Record list cannot be modified

The original list accepts another book. The record keeps its initial contents, and its returned list rejects additions.

5. Know the limit: elements are not copied

This approach protects the list structure. It does not create separate copies of its elements.

Here, the elements are strings, which are immutable. If the list contained mutable Book objects, callers could still change those objects. Use immutable element types or copy mutable elements when your data model requires deeper protection.

Best practices

  • Copy incoming collections at construction boundaries.
  • Document your null policy; this example rejects null lists and null elements.
  • Prefer immutable element types for value objects.
  • Avoid unnecessary copying in the accessor when the stored list is already protected.
  • Treat the caught exception as a demonstration; normal application code should avoid attempting unsupported modifications.

Summary

A record does not automatically make a referenced list immutable. Using List.copyOf() in its compact constructor protects the list against external structural changes while keeping the record concise.

How do I implement an interface in Java?

An interface defines a contract that implementing classes must follow. You can use it to describe a behavior without choosing a specific implementation.

In this example, you will create a Printable interface and implement it in a Book class.

Source code

Save the following code as InterfaceExample.java:

public class InterfaceExample {
    void main() {
        Printable item = new Book("Learning Java", "Alice");

        item.printDetails();
    }
}

interface Printable {
    void printDetails();
}

class Book implements Printable {
    private final String title;
    private final String author;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    @Override
    public void printDetails() {
        IO.println("Title: " + title);
        IO.println("Author: " + author);
    }
}

Explanation

The Printable interface declares a method:

interface Printable {
    void printDetails();
}

This declaration specifies the method’s name, parameters, and return type. It does not provide a method body.

The method is implicitly public and abstract, so you do not need to write those modifiers here.

Implementing the interface

The Book class uses the implements keyword:

class Book implements Printable {

Because Book is a concrete class, it must provide an implementation of the interface’s abstract method:

@Override
public void printDetails() {
    IO.println("Title: " + title);
    IO.println("Author: " + author);
}

The implementation must be public. Giving it more restrictive access would cause a compilation error.

The @Override annotation lets the compiler check that this method implements an inherited method correctly.

Using an interface as a variable type

The following statement creates a Book object and assigns its reference to a variable of type Printable:

Printable item = new Book("Learning Java", "Alice");

This is valid because Book implements Printable.

The calling code can then use the method declared by the interface:

item.printDetails();

At runtime, Java executes the implementation in Book.

Using an interface as the variable type lets the calling code work with any implementation of that interface.

Run the example

Compile and run the file:

javac InterfaceExample.java
java InterfaceExample

Output

Title: Learning Java
Author: Alice

A class can implement multiple interfaces. Use interfaces when different classes should provide the same behavior, even if those classes do not share a custom superclass.