1. Overview
When working with inheritance in Java, we often start with an abstract class that defines a common method that all subclasses need to implement. For instance, let’s consider an abstract class Item that defines the method use(). Every subclass of Item (such as Book or Key) may need its own implementation of use(). However, an issue may arise when each subclass requires different arguments for the use() method.
In this tutorial, we’ll explore how to implement an abstract method in Java that accepts a variable number of arguments.
2. Problem Statement
Suppose we have the abstract class Item:
public abstract class Item {
public abstract void use();
}
In our scenario, the subclass Book needs no arguments, whereas the subclass Key needs both a String (the door identifier) and a Queue (of doors). However, since Java is strongly typed, it doesn’t enable us to easily declare one abstract method and then override it with arbitrary parameters.
Therefore, let’s implement a workaround to handle the issue:
In the above flowchart, we see a visual overview of how the classes connect. At the top, the class Item<C> defines the common base. From there, each subclass selects the type of context to work with:
- The subclass Book works with the context type EmptyContext, which represents the case where no additional data is required
- The subclass Key works with the context type KeyContext, which contains the door identifier and the queue of doors
Therefore, every subclass has a dedicated context type to model its exact needs.
3. Solution – Typed Context Object Approach
Here, we implement the typed context object approach. The abstract base class contains one method use (C context), in which C is a generic context type. Each subclass selects a C that precisely models the data it needs, keeping the API small and enabling compile-time type safety.
3.1. Item – Abstract Base With Generics
The Item.java file defines the abstract method signature once, using a generic type parameter C for the context:
public abstract class Item<C> {
public abstract void use(C context);
}
Instead of many use(…) overloads, we now have one use(C) method whereby each subclass decides its own C type.
3.2. EmptyContext – Context for No Arguments
The EmptyContext.java file provides a context for items that require no parameters, such as Book:
package com.example.items;
public final class EmptyContext {
public static final EmptyContext INSTANCE = new EmptyContext();
private EmptyContext() {}
}
Book can still implement Item<C>, but with C = EmptyContext, avoiding null and keeping the API consistent.
3.3. KeyContext – Context for Key Parameters
KeyContext contains exactly the data a Key needs (doorId and a Queue of door Strings):
public final class KeyContext {
private final String doorId;
private final Queue<String> doorsQueue;
public KeyContext(String doorId, Queue<String> doorsQueue) {
this.doorId = doorId;
this.doorsQueue = doorsQueue;
}
public String getDoorId() {
return doorId;
}
public Queue<String> getDoorsQueue() {
return doorsQueue;
}
}
Grouping parameters into a named type, in this case, KeyContext, specifies what data is expected and prevents argument-order mistakes. Furthermore, the compiler checks types for us.
3.4. Book – Item That Needs No Data
The Book file represents an item that uses EmptyContext since it requires no parameters:
public class Book extends Item<EmptyContext> {
private final String title;
public Book(String title) {
this.title = title;
}
@Override
public void use(EmptyContext ctx) {
System.out.println("You read the book: " + title);
}
}
Although a subclass like Book doesn’t need any arguments, we still need it to fit the same method pattern as other items. That’s why we create EmptyContext, a placeholder object that carries no data. To clarify:
- Book can implement use(EmptyContext context) without extra arguments
- Other subclasses, like Key, can use their own context classes with real data
Thus, all items can now share the same uniform method signature.
3.5. Key – Item That Uses KeyContext
Key demonstrates how a class receives a typed context with multiple fields:
package com.example.items;
public class Key extends Item<KeyContext> {
private final String name;
public Key(String name) {
this.name = name;
}
@Override
public void use(KeyContext ctx) {
System.out.println("Using key '" + name + "' on door: " + ctx.getDoorId());
System.out.println("Doors remaining in queue: " + ctx.getDoorsQueue().size());
}
}
So, the Key class can only be used with a KeyContext, and Java’s compiler makes sure of that. Therefore, we don’t need to worry about unsafe type conversions.
4. Tests
At this point, let’s create the test classes BookUnitTest.java and KeyUnitTest.java to verify that Book and Key work correctly.
4.1. BookUnitTest.java
ln this test, let’s verify that calling use() on a Book object works with EmptyContext:
package com.example.items;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class BookUnitTest {
@Test
void givenBook_whenUseWithEmptyContext_thenNoException() {
Book book = new Book("The Hobbit");
assertDoesNotThrow(() -> book.use(EmptyContext.INSTANCE));
}
}
Here’s what the test does:
- Creates a Book with the title “The Hobbit”
- Calls its use() method, passing in an EmptyContext
- The test passes if no exception is thrown
The test confirms that items that don’t require extra data, like Book, can safely use the EmptyContext without runtime errors.
Here, using assertDoesNotThrow verifies that the method executes safely without errors. Our aim is to confirm type-safety and not to test the printed output. Even though we can capture System.out to verify the printed output, keeping the test simple makes it clearer that we’re only validating correct API usage.
Meanwhile, using EmptyContext.INSTANCE also avoids the need to pass null, which can otherwise cause runtime issues. So, even no-argument cases remain consistent with the overall method signature shared across all subclasses.
4.2. KeyUnitTest.java
Next, let’s verify that a Key can safely unlock a door when provided with a KeyContext:
package com.example.items;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import java.util.Queue;
import java.util.LinkedList;
public class KeyUnitTest {
@Test
void givenKey_whenUseWithKeyContext_thenNoException() {
Queue<String> doors = new LinkedList<>();
doors.add("front-door");
doors.add("back-door");
Key key = new Key("MasterKey");
KeyContext ctx = new KeyContext("front-door", doors);
assertDoesNotThrow(() -> key.use(ctx));
}
}
Here’s what the test does:
- Creates a queue of doors (front-door, back-door)
- Creates a Key labeled “MasterKey”
- Constructs a KeyContext with the current door (front-door) and the queue
- Calls key.use(ctx) and asserts that it doesn’t throw an exception
The test confirms that the Key needs to receive the correct context type (KeyContext), ensuring type safety, and that the method behaves correctly when managing doors. Unlike BookUnitTest, the test here shows a stateful interaction whereby each time a door is unlocked, it’s removed from the queue.
Additionally, it shows how the compiler enforces the correct usage of contexts, since if we tried to pass an EmptyContext to a Key, the code doesn’t even compile. Thus, we can catch accidental misuse early, long before runtime.
4.3. Test Results
Let’s now run our tests:
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...
The output above shows that BookUnitTest.java validates the no-argument case with EmptyContext, whereas KeyUnitTest.java validates the argumented case with KeyContext.
These tests together show the two extremes, where one item requires no data and one item requires multiple pieces of information. Therefore, the typed context object approach is robust, regardless of how many parameters a subclass may need. For instance, adding new items (a Potion class with a PotionContext) would follow the same pattern and only require new tests without altering the abstract base.
Here’s a summary of how our Book and Key items work with their respective contexts:
- The single abstract method use(C) gives a consistent API for all subclasses
- Each subclass selects a context type that exactly models the data it needs (zero or many parameters)
- The compiler enforces types and, as a result, we avoid runtime casts and fragile Object… handling
- EmptyContext keeps the no-argument case explicit and clean
Thus, the approach is flexible, type-safe, and easy to extend.
5. Conclusion
In this article, we examined how to implement an abstract method in Java that works with a variable set of arguments.
So, we used a typed context object approach, whereby each subclass defines exactly the data it needs through its own context class. In scenarios containing no-argument cases, the EmptyContext ensures the signature method remains uniform without special handling. Additionally, we don’t need unsafe type conversions since the compiler ensures that we always use the correct types.
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.