Background
From the beginning, Java was developed as an object-oriented language, where the main task was solved by creating class instances. However, utility classes with a set of static methods (e.g., java.util.Collections) began to appear for frequently used methods (e.g., sorting, data transformation).
Problem
Static methods simplify the calling of utility functions, but they are not suitable for storing state, injecting dependencies, and testing in isolation. On the other hand, class instances are more flexible but increase the amount of code required for initialization and require careful lifecycle management.
Solution
Utility classes — a set of stateless static methods that do not require the creation of an object. They are good for operations on collections, transformations, and mathematical operations.
Class instances — they store state, use dependencies, and provide extensibility and testability. They are used in business logic, services, controllers, etc.
Example code:
// Example of a utility class public class MathUtils { public static int add(int a, int b) { return a + b; } } // Usage: int sum = MathUtils.add(1, 2); // Example of a class instance for business logic public class OrderService { private final OrderRepository repo; public OrderService(OrderRepository repo) { this.repo = repo; } public void placeOrder(Order o) { repo.save(o); } }
Key features:
Can utility classes be inherited, and can their static methods be extended?
No, utility classes are usually declared as final, with a private constructor. Inheriting static methods is possible but makes no sense, as static methods are not inherited at the instance call level.
Example code:
public final class MyUtils { private MyUtils() {} // prevents instance creation }
Can utility classes contain state?
No. If a utility class contains state (instance or static fields), it violates the principle of writing utilities, leads to multithreading errors, and decreases readability.
Can static methods of utility classes be mocked during testing?
Only using special tools like PowerMock, which make tests more complex and sometimes unstable. In typical cases, a DI-friendly approach with instances is preferred for tests.
In the project, all services were implemented using utility static methods. Dependency injection was impossible, unit tests were not isolated, and each test depended on the state of the environment.
Pros:
Cons:
In services, separate classes with dependency injection through interfaces are used. For transformations and simple operations, separate stateless utility classes are utilized.
Pros:
Cons: