Java Best Practices
This document outlines the definitive best practices for Java development within our team. Adherence to these rules ensures consistent, high-quality, and maintainable code, leveraging modern Java features and enterprise standards.
1. Code Organization and Structure
1.1. Style Guide Adherence
Always follow the Google Java Style Guide for formatting, naming, and structural conventions. Configure your IDE (e.g., IntelliJ IDEA, VS Code with appropriate plugins) to automatically apply this style.
1.2. File Naming and Encoding
Source files must be named after the single top-level class they contain, with a .java extension. All source files must use UTF-8 encoding.
1.3. Indentation and Whitespace
Use 4 spaces for indentation. Never use tabs. For wrapped lines, use 8 spaces (twice the normal indentation).
❌ BAD
public class MyClass {
void myMethod() {
// Tab used for indentation
System.out.println("Hello");
}
}
✅ GOOD
public class MyClass {
void myMethod() {
// 4 spaces for indentation
System.out.println("Hello");
}
}
1.4. Braces
Always use K&R style braces for non-empty blocks. Braces are mandatory for if, else, for, do, and while statements, even for single-line bodies.
❌ BAD
if (condition)
doSomething(); // Missing braces
while (true){ // Bad K&R style
doSomething();
}
✅ GOOD
if (condition) {
doSomething();
}
while (true) {
doSomethingElse();
}
1.5. Package and Import Statements
Every class must belong to a package. Use explicit imports; wildcard imports (*) are forbidden. Imports are ordered: static imports, then non-static imports, separated by a single blank line, both groups in ASCII sort order.
❌ BAD
import java.util.*; // Wildcard import
import static com.example.Constants.*; // Wildcard static import
public class MyClass { /* ... */ }
✅ GOOD
import static com.example.Constants.DEFAULT_VALUE;
import java.util.ArrayList;
import java.util.List;
public class MyClass { /* ... */ }
1.6. Naming Conventions
Adhere strictly to these conventions:
- Packages:
com.yourcompany.project.module(all lowercase, dot-separated). - Classes/Enums/Records/Interfaces:
PascalCase(nouns). - Variables/Parameters:
camelCase. - Constants:
SCREAMING_SNAKE_CASE(forstatic finalfields). - Methods:
camelCase(verbs). - Boolean Variables/Methods: Prefix with
is,has,can,should. - Collections: Use plural names (e.g.,
users,messages). - Test Methods:
featureUnderTest_testScenario_expectedBehavior().
❌ BAD
public class userManager { // Class name not PascalCase
public static final String default_user_name = "guest"; // Constant not SCREAMING_SNAKE_CASE
private List userList; // Collection not plural
public boolean checkUser(User u) { /* ... */ } // Boolean method name unclear
}
✅ GOOD
package com.example.app.users;
import java.util.List;
public class UserManager {
public static final String DEFAULT_USER_NAME = "guest";
private List<User> users;
public boolean isValidUser(User user) { /* ... */ }
}
2. Common Patterns and Anti-patterns
2.1. Prefer Immutability
Design classes to be immutable whenever possible. This simplifies concurrency and reasoning about state. Use record classes (Java 16+) for simple immutable data carriers.
❌ BAD
public class User {
private String name;
public User(String name) { this.name = name; }
public String getName() { return name; }
public void setName(String name) { this.name = name; } // Mutable setter
}
✅ GOOD
public record User(String name) {} // Immutable record (Java 16+)
// Or traditional immutable class:
public final class ImmutableUser {
private final String name;
public ImmutableUser(String name) { this.name = name; }
public String getName() { return name; }
}
2.2. Embrace Functional Programming with Streams and Optionals
Leverage Java's Stream API for collection processing and Optional for handling potentially absent values.
❌ BAD
List<String> names = getUsers().stream()
.filter(u -> u.getAge() > 18)
.map(User::getName)
.collect(Collectors.toList());
// Handling null:
User user = findUserById(123L);
if (user != null) {
System.out.println(user.getName());
} else {
System.out.println("User not found");
}
✅ GOOD
List<String> names = users.stream()
.filter(user -> user.getAge() > 18)
.map(User::getName)
.toList(); // Java 16+ for .toList()
// Handling Optional:
findUserById(123L)
.map(User::getName)
.ifPresentOrElse(
System.out::println,
() -> System.out.println("User not found")
);
2.3. Resource Management with Try-with-resources
Always use try-with-resources for any resource that implements AutoCloseable.
❌ BAD
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// ... read from fis
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
✅ GOOD
try (FileInputStream fis = new FileInputStream("file.txt")) {
// ... read from fis
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
3. Performance Considerations
3.1. Efficient String Concatenation
Use StringBuilder for concatenating multiple strings in loops or when building complex strings.
❌ BAD
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Creates many intermediate String objects
}
✅ GOOD
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
3.2. Logging Performance
Avoid expensive string formatting or method calls in logging statements unless the log level is enabled. Use parameterized logging.
❌ BAD
logger.debug("Processing user: " + user.getName() + " with ID: " + user.getId()); // String concatenation always happens
✅ GOOD
logger.debug("Processing user: {} with ID: {}", user.getName(), user.getId()); // Parameters only evaluated if debug is enabled
4. Common Pitfalls and Gotchas
4.1. Avoid Mutable Static State
Mutable static fields are a common source of concurrency bugs and make testing difficult. Minimize their use.
❌ BAD
public class Counter {
public static int count = 0; // Mutable static field
public static void increment() { count++; }
}
✅ GOOD
// Use dependency injection for stateful services or pass state explicitly
public class CounterService {
private int count = 0; // Instance field
public synchronized void increment() { count++; } // Thread-safe instance method
public int getCount() { return count; }
}
4.2. Dependency Version Management
Always pin dependency versions. Use Bill-of-Materials (BOMs) for managing compatible versions across related libraries (e.g., Spring Boot BOM, Google Cloud Libraries BOM).
❌ BAD (Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- No version specified, relies on parent or latest -->
</dependency>
✅ GOOD (Maven with BOM)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version> <!-- Pin BOM version -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- Version inherited from BOM -->
</dependency>
</dependencies>
4.3. Correct equals() and hashCode() Implementation
Always override both equals() and hashCode() together, or neither. Use Objects.equals() and Objects.hash() for robust implementations. For data classes, record types handle this automatically.
❌ BAD
public class Point {
private int x, y;
// Only equals() overridden, or incorrectly
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
// Missing hashCode()
}
✅ GOOD
import java.util.Objects;
public final class Point { // Make final if not intended for extension
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return