Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Java is a class-based, object-oriented programming language designed for portability and cross-platform compatibility. It is known for its “write once, run anywhere” capability and is widely used for enterprise applications.
Java, despite its robustness and maturity, has several common anti-patterns that can lead to bugs, performance issues, and maintenance problems. Here are the most important anti-patterns to avoid when writing Java code.
// Anti-pattern: Not closing resources
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// What if an exception occurs? Resources might not be closed
}
// Better approach: Use try-with-resources
public void readFile(String path) throws IOException {
try (FileReader reader = new FileReader(path);
BufferedReader bufferedReader = new BufferedReader(reader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
// Resources are automatically closed
}
Always use try-with-resources (Java 7+) for automatic resource management to ensure resources are properly closed, even if exceptions occur.
// Anti-pattern: Using raw types
List myList = new ArrayList();
myList.add("string");
myList.add(42); // No compile-time type checking
String s = (String) myList.get(1); // ClassCastException at runtime
// Better approach: Use generics
List<String> myList = new ArrayList<>();
myList.add("string");
// myList.add(42); // Compile-time error
String s = myList.get(0); // No cast needed
Raw types bypass the compile-time type safety that generics provide. Always use proper generic types to catch type errors at compile time.
// Anti-pattern: Excessive null checking
public String getUserCity(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
return city.getName();
}
}
}
return "Unknown";
}
// Better approach: Use Optional (Java 8+)
public String getUserCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.map(City::getName)
.orElse("Unknown");
}
Excessive null checks lead to deeply nested code that’s hard to read and maintain. Use Optional (Java 8+) for cleaner handling of potentially null values.
// Anti-pattern: Using exceptions for flow control
public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
// Better approach: Use validation
public boolean isInteger(String s) {
if (s == null || s.isEmpty()) {
return false;
}
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i))) {
return false;
}
}
return true;
}
Exceptions are for exceptional conditions, not normal flow control. They have performance overhead and make code harder to understand.
// Anti-pattern: Public mutable fields
public class User {
public List<String> roles = new ArrayList<>();
}
// Anyone can modify the list
user.roles.clear();
// Better approach: Encapsulation
public class User {
private final List<String> roles = new ArrayList<>();
public List<String> getRoles() {
return Collections.unmodifiableList(roles);
}
public void addRole(String role) {
roles.add(role);
}
public void removeRole(String role) {
roles.remove(role);
}
}
Public mutable fields break encapsulation and can lead to unexpected state changes. Use proper encapsulation with getters and setters.
// Anti-pattern: Returning null instead of empty collection
public List<Customer> getCustomers() {
if (customers.isEmpty()) {
return null;
}
return customers;
}
// Client code needs null check
List<Customer> customers = getCustomers();
if (customers != null) {
for (Customer customer : customers) {
// Process customer
}
}
// Better approach: Return empty collections
public List<Customer> getCustomers() {
return new ArrayList<>(customers); // Returns empty list if customers is empty
}
// Client code is simpler
for (Customer customer : getCustomers()) {
// Process customer
}
Returning null instead of empty collections forces clients to add null checks. Return empty collections instead of null to simplify client code.
// Anti-pattern: Using == for object comparison
String a = new String("test");
String b = new String("test");
if (a == b) { // Always false
// This code never executes
}
// Better approach: Use equals() for object comparison
if (a.equals(b)) { // True
// This code executes
}
The ==
operator compares object references, not their contents. Use equals()
to compare object values.
// Anti-pattern: String concatenation in loops
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Creates a new String object each time
}
// Better approach: Use StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
String concatenation in loops creates many temporary String objects, impacting performance. Use StringBuilder for efficient string building.
// Anti-pattern: Non-thread-safe singleton
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // Not thread-safe
}
return instance;
}
}
// Better approach: Thread-safe singleton
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// Or even better: Enum singleton
public enum Singleton {
INSTANCE;
public void doSomething() {
// Implementation
}
}
Incorrectly implemented singletons can lead to thread safety issues or multiple instances. Use enum singletons or proper double-checked locking.
// Anti-pattern: Using concrete class for type
public ArrayList<Customer> getCustomers() {
return new ArrayList<>(customers);
}
// Better approach: Use interface for type
public List<Customer> getCustomers() {
return new ArrayList<>(customers);
}
Program to interfaces, not implementations. This allows you to change the implementation without affecting client code.
// Anti-pattern: Incorrect exception handling
public void processFile(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// Process file
fis.close(); // Won't be called if an exception occurs
} catch (IOException e) {
e.printStackTrace(); // Just printing stack trace
}
}
// Better approach: Proper exception handling
public void processFile(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// Process file
} catch (IOException e) {
// Log the exception
logger.error("Error processing file: " + path, e);
// Rethrow or handle appropriately
throw new ApplicationException("Could not process file", e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
logger.error("Error closing file", e);
}
}
}
}
// Even better: Use try-with-resources (Java 7+)
public void processFile(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
// Process file
} catch (IOException e) {
logger.error("Error processing file: " + path, e);
throw new ApplicationException("Could not process file", e);
}
}
Proper exception handling includes resource cleanup in finally blocks or using try-with-resources, meaningful error messages, and appropriate exception handling strategies.
// Anti-pattern: Not using modern Java features
List<String> filtered = new ArrayList<>();
for (String s : strings) {
if (s.length() > 3) {
filtered.add(s.toUpperCase());
}
}
// Better approach: Use streams and lambdas
List<String> filtered = strings.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
Modern Java (8+) provides many features like streams, lambdas, and method references that can make code more concise and readable.
// Anti-pattern: Excessive checked exceptions
public void doSomething() throws IOException, SQLException, ParseException {
// Implementation
}
// Client code has to handle or propagate all exceptions
try {
doSomething();
} catch (IOException e) {
// Handle IOException
} catch (SQLException e) {
// Handle SQLException
} catch (ParseException e) {
// Handle ParseException
}
// Better approach: Use unchecked exceptions for non-recoverable errors
public void doSomething() {
try {
// Implementation
} catch (IOException | SQLException | ParseException e) {
throw new ServiceException("Operation failed", e);
}
}
// Client code is simpler
try {
doSomething();
} catch (ServiceException e) {
// Handle service exception
}
Excessive use of checked exceptions can lead to cluttered code. Use unchecked exceptions for non-recoverable errors and checked exceptions only when the caller can reasonably recover.
// Anti-pattern: Mutable value objects
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
}
// Better approach: Immutable value objects
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public Point withX(int x) {
return new Point(x, this.y);
}
public Point withY(int y) {
return new Point(this.x, y);
}
}
Immutable objects are thread-safe, simpler to reason about, and prevent unexpected state changes. Make value objects immutable when possible.
// Anti-pattern: Using System.out for logging
public void processOrder(Order order) {
System.out.println("Processing order: " + order.getId());
// Process order
System.out.println("Order processed successfully");
}
// Better approach: Use a proper logging framework
private static final Logger logger = LoggerFactory.getLogger(OrderProcessor.class);
public void processOrder(Order order) {
logger.info("Processing order: {}", order.getId());
// Process order
logger.info("Order processed successfully");
}
Using System.out.println
for logging doesn’t provide features like log levels, formatting, and output configuration. Use a proper logging framework like SLF4J with Logback or Log4j.
// Anti-pattern: Hard-coded dependencies
public class OrderService {
private final CustomerRepository customerRepository = new CustomerRepositoryImpl();
private final EmailService emailService = new EmailServiceImpl();
public void placeOrder(Order order) {
// Use customerRepository and emailService
}
}
// Better approach: Dependency injection
public class OrderService {
private final CustomerRepository customerRepository;
private final EmailService emailService;
public OrderService(CustomerRepository customerRepository, EmailService emailService) {
this.customerRepository = customerRepository;
this.emailService = emailService;
}
public void placeOrder(Order order) {
// Use customerRepository and emailService
}
}
Hard-coded dependencies make code hard to test and maintain. Use dependency injection to provide dependencies from outside the class.
// Anti-pattern: Using wrong collection types
// Using ArrayList when frequent insertions/deletions in the middle
List<String> items = new ArrayList<>();
// Frequent insertions at the beginning
items.add(0, "newItem"); // O(n) operation
// Better approach: Choose appropriate collection types
// For frequent insertions/deletions, use LinkedList
List<String> items = new LinkedList<>();
items.add(0, "newItem"); // O(1) operation
// For fast lookups by key, use HashMap
Map<String, Customer> customerMap = new HashMap<>();
// For sorted data, use TreeMap or TreeSet
SortedMap<String, Customer> sortedCustomers = new TreeMap<>();
Choosing the wrong collection type can lead to performance issues. Understand the characteristics of different collection types and choose the appropriate one for your use case.
// Anti-pattern: Using low-level synchronization
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// Better approach: Use appropriate concurrency utilities
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
Java provides many high-level concurrency utilities like AtomicInteger, ConcurrentHashMap, and ExecutorService that are more efficient and easier to use correctly than low-level synchronization.
// Anti-pattern: Using old date/time APIs
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, 1);
Date tomorrow = calendar.getTime();
// Better approach: Use Java Time API
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
// Working with time zones
ZonedDateTime nowInNewYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime nowInTokyo = nowInNewYork.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
The old date/time APIs (Date, Calendar) are error-prone and hard to use correctly. Java 8+ provides a much better date/time API that is immutable, thread-safe, and more intuitive.