Partner – Orkes – NPI EA (cat=Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag=Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Browser testing is essential if you have a website or web applications that users interact with. Manual testing can be very helpful to an extent, but given the multiple browsers available, not to mention versions and operating system, testing everything manually becomes time-consuming and repetitive.

To help automate this process, Selenium is a popular choice for developers, as an open-source tool with a large and active community. What's more, we can further scale our automation testing by running on theLambdaTest cloud-based testing platform.

Read more through our step-by-step tutorial on how to set up Selenium tests with Java and run them on LambdaTest:

>> Automated Browser Testing With Selenium

Partner – Orkes – NPI EA (cat=Java)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

1. Introduction

In languages like C or C++, we can store a function in a variable and pass it around – this is known as a function pointer. Java doesn’t have function pointers, but we can achieve the same behaviour using other techniques. In this tutorial, we’ll explore a few common ways to simulate function pointers in Java.

2. Interfaces and Anonymous Classes

Before Java 8, the standard way to simulate function pointers involved defining single-method interfaces and implementing them with anonymous classes. This approach remains valuable for maintaining legacy code or working in environments without Java 8+ support.

Here’s how we define a simple interface for operations:

public interface MathOperation {
    int operate(int a, int b);
}

This interface has only one method, operate(), which receives two integers and returns a result.

Now we define a class that uses this interface:

public class Calculator {
    public int calculate(int a, int b, MathOperation operation) {
        return operation.operate(a, b);
    }
}

The calculate() method accepts an operation and delegates the calculation logic to the passed implementation.

Let’s test it using an anonymous class to do addition:

@Test
void givenAnonymousAddition_whenCalculate_thenReturnSum() {
    Calculator calculator = new Calculator();
    MathOperation addition = new MathOperation() {
        @Override
        public int operate(int a, int b) {
            return a + b;
        }
    };
    int result = calculator.calculate(2, 3, addition);
    assertEquals(5, result);
}

In this code, the interface is implemented directly using an anonymous class. This allows behaviour to be passed into the Calculator. The test confirms that 2 + 3 results in 5.

The interface approach works across all Java versions and provides clear type safety.

However, it requires significant boilerplate code, especially for simple operations. Each operation needs its own class implementation, which can clutter codebases with many small classes.

3. Lambda Expressions (Java 8+)

Java 8 introduced lambda expressions, which offer a shorter and more readable way to pass behaviour.

We can reuse the same MathOperation interface here:

@Test
void givenLambdaSubtraction_whenCalculate_thenReturnDifference() {
    Calculator calculator = new Calculator();
    MathOperation subtract = (a, b) -> a - b;
    int result = calculator.calculate(10, 4, subtract);
    assertEquals(6, result);
}

In this test, we use a lambda to perform subtraction. The expression (a, b) -> a – b defines the logic inline and matches the interface’s method signature.

The Calculator doesn’t change – it still accepts the interface and calls its method. The difference is that the behaviour is now passed in a much more concise way.

This approach is widely used in modern Java code. It improves readability and reduces boilerplate, especially when performing simple operations.

4. Built-in Functional Interfaces

In addition, Java 8 also introduced predefined functional interfaces in the java.util.function package. These allow us to avoid writing our own interfaces.

Let’s use a built-in interface called BiFunction, which takes two inputs and returns one result:

@Test
void givenBiFunctionMultiply_whenApply_thenReturnProduct() {
    BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
    int result = multiply.apply(6, 7);
    assertEquals(42, result);
}

BiFunction<T, U, R> represents a function that takes two arguments and returns one result. We store the logic in the variable multiply, and call it with the apply() method.

We can also use BiFunction in a method:

public class AdvancedCalculator {
    public int compute(int a, int b, BiFunction<Integer, Integer, Integer> operation) {
        return operation.apply(a, b);
    }
}

Let’s use the BiFunction approach to test the division:

@Test
void givenBiFunctionDivide_whenCompute_thenReturnQuotient() {
    AdvancedCalculator calculator = new AdvancedCalculator();
    BiFunction<Integer, Integer, Integer> divide = (a, b) -> a / b;
    int result = calculator.compute(20, 4, divide);
    assertEquals(5, result);
}

Using built-in interfaces allows us to keep our code clean and avoid extra boilerplate. This approach works well when the functional requirement matches one of the predefined interfaces, like Function, BiFunction, or Predicate.

This pattern is ideal when we want standardisation and consistency in function definitions. However, we may face limitations when we need custom parameters or return types that don’t fit these predefined types.

5. Method References

Method references provide a shorthand for lambda expressions when calling existing methods.

Let’s define a utility method for addition:

public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }
}

We can now use a method reference instead of writing a lambda:

@Test
void givenMethodReference_whenCalculate_thenReturnSum() {
    Calculator calculator = new Calculator();
    MathOperation operation = MathUtils::add;
    int result = calculator.calculate(5, 10, operation);
    assertEquals(15, result);
}

In this code, MathUtils::add is passed as a reference. The method signature matches the operate() method in MathOperation, so the compiler accepts it.

Method references are useful when we already have existing static or instance methods. They keep the code cleaner by avoiding repetition and are especially useful in stream operations or callback patterns.

This approach is most effective when the logic already exists. But if the behavior needs to be dynamic or customized, lambdas or interfaces may offer more flexibility.

6. Reflection

Java also allows methods to be called dynamically using reflection. This is more advanced and is typically used in frameworks, tools, or libraries where methods must be discovered and invoked at runtime.

Let’s define a dynamic operation method:

public class DynamicOps {
    public int power(int a, int b) {
        return (int) Math.pow(a, b);
    }
}

Now let’s call the method via reflection:

@Test
void givenReflection_whenInvokePower_thenReturnResult() throws Exception {
    DynamicOps ops = new DynamicOps();
    Method method = DynamicOps.class.getMethod("power", int.class, int.class);
    int result = (int) method.invoke(ops, 2, 3);
    assertEquals(8, result);
}

In this example, we retrieve the power() method reference from the DynamicOps class using its name and parameter types, then invoke it with arguments. This allows behavior to be selected and executed at runtime.

Reflection is powerful when we don’t know the method at compile time, such as in plug-in systems or annotation-based processing. However, it’s slower and more error-prone than other techniques, and it doesn’t offer compile-time type safety.

We usually avoid reflection in general application logic. It’s best reserved for specific use cases where dynamic loading or invocation is necessary.

7. Command Pattern

Another way to simulate function pointers in Java is by using the Command Pattern, which encapsulates behaviour into standalone objects.

This pattern is especially useful when we want to parameterise actions, delay their execution, or queue them dynamically. It also promotes loose coupling between the invoker of the operation and the logic itself.

Let’s continue using our MathOperation example. In this context, each math operation can be treated as a command. We start with the same functional interface:

public interface MathOperation {
    int operate(int a, int b);
}

Now, instead of passing lambdas or anonymous classes, we define individual command classes that implement this interface. For instance, we can create an AddCommand class as follows:

public class AddCommand implements MathOperation {
    @Override
    public int operate(int a, int b) {
        return a + b;
    }
}

Similarly, we could create other commands such as SubtractCommand, MultiplyCommand, or DivideCommand, each encapsulating a specific operation.

We can reuse our existing Calculator class to execute these commands. Let’s test the command pattern with the addition operation:

@Test
void givenAddCommand_whenCalculate_thenReturnSum() {
    Calculator calculator = new Calculator();
    MathOperation add = new AddCommand();
    int result = calculator.calculate(3, 7, add);
    assertEquals(10, result);
}

Here, we’re creating a specific AddCommand object and passing it to the calculator. This encapsulates the addition logic inside a reusable, standalone object, just like a command.

This approach works well when we need to pass around different behaviors as objects, especially in architectures that support undo operations, history tracking, or delayed execution. It also makes each operation easily testable and extendable in isolation.

8. Enum-Based

Additionally, Java enums aren’t limited to representing constants; they can also encapsulate logic. By allowing enums to define methods, we can group related behaviours together and pass them around as if they were function pointers.

This is particularly effective when we have a known set of fixed operations. Let’s return to our math operation example and implement the logic using an enum.

We define an enum MathOperationEnum, where each constant overrides an abstract method to provide its own behaviour:

public enum MathOperationEnum {
    ADD {
        @Override
        public int apply(int a, int b) {
            return a + b;
        }
    },
    SUBTRACT {
        @Override
        public int apply(int a, int b) {
            return a - b;
        }
    },
    MULTIPLY {
        @Override
        public int apply(int a, int b) {
            return a * b;
        }
    },
    DIVIDE {
        @Override
        public int apply(int a, int b) {
            if (b == 0) throw new ArithmeticException("Division by zero");
            return a / b;
        }
    };

    public abstract int apply(int a, int b);
}

With this structure, each enum constant is effectively its own function. We can easily use this in a calculator-like class:

public class EnumCalculator {
    public int calculate(int a, int b, MathOperationEnum operation) {
        return operation.apply(a, b);
    }
}

Let’s create a simple test that uses this enum-based approach:

@Test
void givenEnumSubtract_whenCalculate_thenReturnResult() {
    EnumCalculator calculator = new EnumCalculator();
    int result = calculator.calculate(9, 4, MathOperationEnum.SUBTRACT);
    assertEquals(5, result);
}

In this example, the behaviour is passed through the enum constant, which defines its implementation of the operation. This pattern provides type safety, centralises all possible operations in one place, and avoids the need for multiple class files or custom interfaces.

Using enums in this way is ideal when we have a predefined, finite set of behaviours that should be grouped logically.

9. Summary

Below is a comparison of the most commonly used approaches:

Approach Pros Cons When to Use
Interfaces + Anonymous Classes Works in all Java versions Verbose syntax When working with legacy codebases or pre-Java 8 environments
Lambda Expressions Short, modern, easy to read Java 8+ only When writing modern, concise, and readable functional code
Built-in Functional Interfaces No need to write custom interfaces Limited to predefined input/output types When common functional structures like BiFunction or Predicate fit the use case
Method References Clean syntax for using existing methods Less flexible for custom logic When reusing existing static or instance methods that match functional signatures
Reflection Dynamic and powerful Slow, unsafe, complex When methods must be discovered and invoked dynamically at runtime
Command Pattern Encapsulates behaviour into reusable objects Requires more boilerplate and class definitions When you need to queue, log, or parameterise operations as objects
Enum-Based Functional Behaviour Type-safe and centralised definition of fixed behaviours Limited to finite, predefined operations When the operation set is known and logically grouped together

10. Conclusion

In this article, we explored how Java can simulate the concept of function pointers using a variety of techniques. For modern Java development, lambda expressions and built-in functional interfaces are the most commonly used approaches due to their simplicity and readability.

In older or legacy codebases where Java 8 features aren’t available, using interfaces with anonymous classes remains a reliable alternative.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

Partner – Orkes – NPI EA (cat = Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag = Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)