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.