How Generics + Functional Programming Make Java Code Look More Sophisticated
The article demonstrates how combining Java generics with functional programming—using lambda‑based callbacks and generic pagination utilities—can transform repetitive data‑access code into a concise, reusable structure, while also discussing the benefits, trade‑offs, and potential debugging challenges of this approach.
Introduction
Today we walk step by step to experience the elegance of generics and functional programming in Java.
Case Study: Structured Pagination Code
Two methods— queryBook and queryPencil —show a typical pagination pattern: create a PageData object, query the total count, return early if zero, otherwise fetch the detail list and populate the result.
public PageData queryBook(BookRequest request) {
// 1. create pagination object
PageData pageData = new PageData();
// 2. calculate record count
int count = bookMapper.queryBookCount(request);
// 3. if zero, return empty page
if (count == 0) {
pageData.setCount(0);
return pageData;
}
// 4. otherwise fetch details
List<BookDO> bookList = bookMapper.queryBookList(request);
// 5. set count and result
pageData.setCount(count);
pageData.setResult(bookList);
return pageData;
} public PageData queryPencil(PencilRequest request) {
PageData pageData = new PageData();
int count = pencilMapper.queryPencilCount(request);
if (count == 0) {
pageData.setCount(0);
return pageData;
}
List<PencilDO> pencilList = pencilMapper.queryPencilList(request);
pageData.setCount(count);
pageData.setResult(pencilList);
return pageData;
}The two snippets are structurally and semantically identical: both query a count first and then, if the count is non‑zero, retrieve the list.
IDE does not flag these as duplicate code because the duplicated “skeleton” is hard to extract into a reusable method.
Why Generics + Functional Programming Helps
By abstracting the variable parts (the count query and the list query) into functional parameters, we can collapse the repeated skeleton into a single generic method.
Generic Wrapper Example
A typical generic response class is shown below:
public class ServiceResult<T> {
private Boolean success;
private String errorCode;
private String errorMsg;
private T content;
// getters and setters omitted
}Functional Interface Example
Using Guava’s ListenableFuture we can pass a lambda as a method argument:
public static <V> ListenableFuture<V> invokeWithFuture(Callable<V> callable) {
return gPool.submit(callable);
} @Test
public void invokeWithFuture() throws Execution {
ListenableFuture<String> result = AsyncInvoke.invokeWithFuture(() -> "hello");
System.out.println(result.get());
}Generic Pagination Utility
The following generic method accepts two Callable arguments: one to obtain the total count and another to obtain the detail list.
@SneakyThrows
public static <T> PageData buildPageData(
Callable<Integer> countFunction,
Callable<List<T>> listFunction) {
PageData pageData = new PageData();
int count = countFunction.call();
if (count == 0) {
pageData.setCount(0);
return pageData;
}
List<T> resultList = listFunction.call();
pageData.setCount(count);
pageData.setResult(resultList);
return pageData;
} static class PageData<T> {
private int count;
private T result;
} @Test
public void testBuildPageData() {
buildPageData(() -> 1, () -> Arrays.asList("1"));
}Async Callback Example Using Generics
An interface CallbackTask<R> defines the contract for asynchronous execution.
public interface CallbackTask<R> {
R execute();
default void onSuccess(R r) {}
default void onFailure(Throwable t) {}
}The implementation uses CompletableFuture to run the task and invoke the appropriate callbacks.
private static <R> CompletableFuture<R> doInvoker(CallbackTask<R> executeTask) {
return CompletableFuture.supplyAsync(() -> {
try {
return executeTask.execute();
} catch (Exception e) {
throw new BizException(ASYNC_INVOKER_ERROR.getErrorCode(), e.getMessage());
}
}, gPool).whenComplete((result, throwable) -> {
if (throwable == null) {
executeTask.onSuccess(result);
}
}).exceptionally(throwable -> {
executeTask.onFailure(throwable);
return null;
});
} CompletableFuture<Integer> result = AsyncInvoke.doInvoker(new CallbackTask<Integer>() {
public Integer execute() {
int result = 1 + 1;
return result;
}
public void onSuccess(Integer integer) {
System.out.println("on success result: " + integer);
}
public void onFailure(Throwable t) {
System.out.println("error " + t.getMessage());
}
});Benefits and Drawbacks
Generics provide universality and reduce boilerplate.
Functional programming allows passing code blocks as parameters, increasing flexibility.
Both together lower code duplication and make the skeleton reusable.
However, they can make debugging harder, have a learning curve, and may reduce readability for developers unfamiliar with the style.
Final Thoughts
Generics and functional programming are syntactic sugar in Java; the real value lies in abstracting concrete scenarios and then implementing them with appropriate tools. The article encourages focusing on abstraction rather than the sugar itself.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
