How do I use a method reference in Java?

A method reference lets you use an existing method as the implementation of a functional interface. It can make a lambda expression shorter when the lambda only calls that method.

In this example, you will use a method reference to print each language in a list.

Source code

Save the following code as MethodReferenceExample.java:

import java.util.List;

public class MethodReferenceExample {
    void main() {
        List<String> languages = List.of(
                "Java", "Python", "JavaScript");

        languages.forEach(IO::println);
    }
}

Explanation

The example creates a list containing three language names:

List<String> languages = List.of(
        "Java", "Python", "JavaScript");

The forEach() method performs an action for each element in the list:

languages.forEach(IO::println);

It accepts a Consumer, a functional interface whose abstract method takes one argument and returns no value.

Referring to an existing method

The expression IO::println is a reference to the static println() method in java.lang.IO.

The double colon (::) separates the class name from the method name. The method reference does not call the method immediately. It supplies the action that forEach() invokes for each element.

This method reference can replace the following lambda expression:

languages.forEach(language -> IO.println(language));

Both versions print each language on a separate line, in list order.

Matching the functional interface

For this list, forEach() needs an action that accepts a string and returns no value.

IO.println(Object) is compatible because it accepts a String as an Object and returns void.

You do not add parentheses after the method name in a method reference:

IO::println

Parentheses belong to a method call, such as:

IO.println("Java");

Run the example

Compile and run the file:

javac MethodReferenceExample.java
java MethodReferenceExample

Output

Java
Python
JavaScript

Use a method reference when an existing method already performs the required action. Keep a lambda expression when you need additional logic, such as adding a prefix before printing each value.

What Is Hibernate ORM?

When you use JDBC to read a row from a database, you normally write a SQL query, execute it, and copy the values from the ResultSet into a Java object. If your application has classes such as Book, Author, and Publisher, you repeat this kind of mapping for each class.

Hibernate ORM is a Java framework that handles much of this mapping. You describe how your Java classes correspond to tables, and Hibernate uses that information to load objects and save changes. This series uses Java 25 and Hibernate ORM 7.4.15.Final, the latest stable release as of November 9, 2026. Hibernate ORM 7.4 implements Jakarta Persistence 3.2. See the Hibernate ORM release notes.

What does ORM mean?

ORM stands for object-relational mapping. It connects the way Java represents data as objects with the way a relational database stores data in tables.

For a simple book catalog, the mapping looks like this.

Java Database
Book class books table
One Book object One row in books
id field id primary key column
title field title column

A class that Hibernate can persist is called an entity. Here is the first entity in our catalog.

package org.kodejava.hibernate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "books")
public class Book {
    @Id
    private Long id;

    @Column(nullable = false)
    private String title;

    protected Book() {
    }

    public Book(Long id, String title) {
        this.id = id;
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}
  • @Entity marks Book as a persistent entity.
  • @Table(name = "books") maps the entity to the books table.
  • @Id identifies the field that maps to the primary key. This example assigns the ID in the constructor.
  • @Column(nullable = false) declares the title column as non-nullable in the mapping.

Hibernate can read and write the mapped fields directly because the mapping annotations are on the fields. The protected no-argument constructor allows the persistence provider to instantiate a Book when it loads data.

How does Hibernate save an object?

After Hibernate is configured, you can use an EntityManager to work with entities. The following snippet assumes an existing EntityManager and an active transaction.

Book book = new Book(1L, "Effective Java");
entityManager.persist(book);

Calling persist() makes the new Book a managed entity. Hibernate now tracks it in the persistence context. When Hibernate flushes the context, typically as part of committing the transaction, it executes an SQL statement similar to this one.

insert into books (title, id) values (?, ?)

Hibernate binds the values from the object to those parameters and executes the statement through JDBC. You can load a book by its primary key with entityManager.find(Book.class, 1L). If the persistence context already manages that book, Hibernate can return it directly. Otherwise, Hibernate can load it from the database, and find() returns null if the book does not exist.

What is the difference between Jakarta Persistence and Hibernate?

Jakarta Persistence, often called JPA, defines standard annotations and interfaces such as @Entity, @Id, and EntityManager. Hibernate ORM implements that standard and provides its own API, including SessionFactory and Session.

We will start with Jakarta Persistence so you can learn the standard mapping and transaction APIs. Later articles will use Hibernate-specific features where they help solve a concrete problem, such as inspecting SQL activity or tuning fetch behavior. The Hibernate ORM Javadocs describe both APIs.

Where does JDBC fit?

Hibernate uses JDBC to communicate with the database. With plain JDBC, you usually write the SQL and map the results yourself. With Hibernate, you provide entity mappings, and Hibernate handles many routine operations, such as inserting a row or finding it by primary key.

The SQL still matters. Loading a list of books and then accessing each book’s author can trigger extra queries. Later in the series, we will inspect the generated SQL and see how a fetch join can avoid that pattern.

In the next article, we will create a Maven project, configure an in-memory H2 database, and run the Book example from start to finish. We will use plain Java to make the Hibernate configuration and transaction boundaries visible.

Maven Dependencies

Add the following dependency inside the <dependencies> section of your pom.xml. Hibernate Core brings in the Jakarta Persistence API transitively, including the annotations used above. To run the example against a database, you will also need a JDBC driver. We will add the H2 driver in the next article.

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.4.15.Final</version>
</dependency>

How do I calculate a moving average with Stream Gatherers in Java 24?

A moving average smooths out short-term changes in a series of readings. Instead of averaging all the readings once, you average the first few readings, move one position forward, and repeat.

In this example, you’ll calculate an average for every three consecutive temperature readings using Java’s Gatherers.windowSliding().

Stream Gatherers became a standard feature in Java 24, so no preview flags or external libraries are needed. See the Java 24 Gatherers API.

1. Group the readings into overlapping windows

Start with five readings: 18, 21, 24, 27, 23. A window size of three gives you [18, 21, 24] first. The next window drops 18 and adds 27, giving you [21, 24, 27].

The call .gather(Gatherers.windowSliding(3)) turns a stream of individual readings into a stream of these lists.

2. Keep complete windows and calculate each average

If the input has one or two readings, windowSliding(3) produces a single shorter window. The filter() below skips that window so every reported average uses exactly three readings. Empty input produces no windows.

For each remaining list, mapToInt() creates a stream of integers and average() computes the mean. The result is present because every window that reaches this step contains three values.

3. Compile and run the example

Save this file as MovingAverageExample.java:

import java.util.List;
import java.util.Locale;
import java.util.stream.Gatherers;

public class MovingAverageExample {
    public static void main(String[] args) {
        List<Integer> readings = List.of(18, 21, 24, 27, 23);
        int windowSize = 3;

        readings.stream()
                .gather(Gatherers.windowSliding(windowSize))
                .filter(window -> window.size() == windowSize)
                .forEachOrdered(window -> {
                    double average = window.stream()
                            .mapToInt(Integer::intValue)
                            .average()
                            .orElseThrow();

                    System.out.printf(Locale.ROOT, "%s -> %.2f%n",
                            window, average);
                });
    }
}

From the directory containing the file, run:

javac --release 24 MovingAverageExample.java
java MovingAverageExample

Output:

[18, 21, 24] -> 21.00
[21, 24, 27] -> 24.00
[24, 27, 23] -> 24.67

The last window averages 74 / 3. The %.2f format rounds the displayed value to two decimal places; it does not change the stored average.

Best practices

  • Put readings in chronological order before making windows. The gatherer follows the stream’s existing order.
  • Use a positive window size. If it comes from user input, validate it before creating the gatherer.
  • Decide what to do when there are too few readings. This example waits for a full window; if you need early results, define how partial averages should work.
  • A three-reading window counts samples. If your readings arrive at irregular intervals and you need a three-minute average, group them by timestamps instead.

How do I convert byte arrays to hexadecimal and back in Java 17?

When you inspect binary data, a hexadecimal string can be easier to read than a list of decimal byte values. You might use it when troubleshooting a file format or checking a message exchanged between applications.

This tutorial shows a complete conversion in both directions, including readable formatting and invalid input handling.

1. Choose a hexadecimal format

Java 17 introduced java.util.HexFormat as a standard API. No preview or incubator flags are needed.

Use HexFormat.of() for lowercase output without separators. For uppercase output with colons between bytes, use:

HexFormat readable =
        HexFormat.ofDelimiter(":").withUpperCase();

Each byte becomes two hexadecimal digits. These options are documented in the Java 17 HexFormat API.

2. Create the complete example

Save the following as HexFormatExample.java:

import java.util.Arrays;
import java.util.HexFormat;

public class HexFormatExample {
    public static void main(String[] args) {
        byte[] packet = {
                0, 15, 16, 127, (byte) 128, (byte) 255
        };

        HexFormat plain = HexFormat.of();
        HexFormat readable =
                HexFormat.ofDelimiter(":").withUpperCase();

        System.out.println(
                "Compact: " + plain.formatHex(packet));

        String formatted = readable.formatHex(packet);
        System.out.println("Readable: " + formatted);

        byte[] restored = readable.parseHex(formatted);
        System.out.println(
                "Same bytes: " + Arrays.equals(packet, restored));
        System.out.println(
                "Java bytes: " + Arrays.toString(restored));

        try {
            readable.parseHex("00:GG");
        } catch (IllegalArgumentException exception) {
            System.out.println(
                    "Invalid input: expected hexadecimal byte pairs");
        }
    }
}

The program first creates sample data. It then prints two representations, parses the colon-separated version, and checks whether the recovered bytes match the original.

3. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 HexFormatExample.java
java HexFormatExample

Output:

Compact: 000f107f80ff
Readable: 00:0F:10:7F:80:FF
Same bytes: true
Java bytes: [0, 15, 16, 127, -128, -1]
Invalid input: expected hexadecimal byte pairs

4. Understand the result

The two formatted strings represent identical data. Java’s signed byte representation explains the negative decimal values in the recovered array: the bit patterns 80 and FF correspond to -128 and -1.

Arrays.equals() confirms that the conversion preserved the bytes.

Parsing must use the expected delimiters. Invalid digits, such as GG, cause IllegalArgumentException; hexadecimal digit letters themselves are case-insensitive. See HexFormat.parseHex().

Best practices

  • Define the expected representation when exchanging data with another application.
  • Use a matching formatter for encoding and decoding.
  • Keep the original bytes when your next operation requires binary data.
  • Handle invalid external input at the point where it enters your application.
  • Remember that hexadecimal encoding makes data readable; it does not encrypt it.

Summary

Use formatHex() to turn bytes into hexadecimal text and parseHex() to recover them. A short round-trip example helps verify your chosen representation before integrating it into a larger application.

How do I write a lambda expression in Java?

A lambda expression provides a concise implementation of a functional interface. A functional interface has one abstract method.

In this example, you will use a lambda expression to greet someone by name.

Source code

Save the following code as LambdaExample.java:

public class LambdaExample {
    void main() {
        Greeting greeting = name -> "Hello, " + name + "!";

        IO.println(greeting.greet("Alice"));
        IO.println(greeting.greet("Bob"));
    }
}

@FunctionalInterface
interface Greeting {
    String greet(String name);
}

Explanation

The Greeting interface declares one abstract method:

@FunctionalInterface
interface Greeting {
    String greet(String name);
}

The method accepts a name and returns a greeting.

The @FunctionalInterface annotation lets the compiler check that the interface meets the requirements of a functional interface.

Creating the lambda expression

The following statement assigns a lambda expression to a Greeting variable:

Greeting greeting = name -> "Hello, " + name + "!";

Each part has a purpose:

Part Purpose
name Receives the method argument
-> Separates the parameter from the body
"Hello, " + name + "!" Produces the returned value

The compiler knows that name is a String because the greet() method declares a String parameter.

For a single parameter with an inferred type, parentheses are optional. You can also write:

Greeting greeting = (name) -> "Hello, " + name + "!";

Returning a value

This lambda has an expression body:

name -> "Hello, " + name + "!"

The expression’s result is returned automatically. You do not need braces or a return statement.

If you use a block body instead, you must return the value explicitly:

Greeting greeting = name -> {
    return "Hello, " + name + "!";
};

Both forms provide the same behavior.

Calling the implementation

Call the interface method through the variable:

greeting.greet("Alice")

This passes "Alice" to the lambda and returns "Hello, Alice!". The program then prints the returned string using IO.println().

Run the example

Compile and run the file:

javac LambdaExample.java
java LambdaExample

Output

Hello, Alice!
Hello, Bob!

A lambda expression needs a target functional interface, such as Greeting in this example. You can also use lambdas with standard functional interfaces from java.util.function, including Predicate, Function, and Consumer.