Java Coding Practices
1. Embrace Immutability
✅ Good: Use final
and immutable classes
❌ Bad: Mutable fields and public setters
final
Fields
- To clearly express intent that a field is constant after construction
- For immutable objects (e.g., User, Car, Config).
-
To guarantee thread safety for shared data. ```java // ✅ Good public final class User { private final String name;// ✅ Immutable
public User(String name) { this.name = name; }
public String getName() { return name; } }
// ❌ Bad public class MutableUser { public String name;
public void setName(String name) {
this.name = name;
} } ```
final
Method Parameters
Use final to prevent accidental reassignment of method arguments.
- In large methods where reassignment might cause confusion.
- When passing parameters to inner classes or lambdas (required to be effectively final).
public void printUser(final String name) {
System.out.println(name);
name = "New Name"; // ❌ Compilation error
}
2. Leverage Streams and Functional Programming
✅ Good: Use Stream
for clean data processing
❌ Bad: Manual loops for everything
// ✅ Good
List<String> names = people.stream()
.filter(p -> p.getAge() > 30)
.map(Person::getName)
.collect(Collectors.toList());
// ❌ Bad
List<String> names = new ArrayList<>();
for (Person p : people) {
if (p.getAge() > 30) {
names.add(p.getName());
}
}
3. Use Design Patterns Thoughtfully
✅ Good: Use Builder for complex object creation
❌ Bad: Telescoping constructors
4. Master Exception Handling
✅ Good: Catch specific exceptions
❌ Bad: Catch Exception
or ignore/swallow it
// ✅ Good
try {
Files.readAllLines(Path.of("file.txt"));
} catch (IOException e) {
System.err.println("File read error: " + e.getMessage());
}
// ❌ Bad
try {
// risky code
} catch (Exception e) {
// silently ignore
}
5. Write Thread-Safe Code
✅ Good: Use ConcurrentHashMap
❌ Bad: Use unsynchronized shared data
// ✅ Good
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
// ❌ Bad
Map<String, Integer> map = new HashMap<>();
map.put("key", 1); // Not thread-safe
✅ Why Use ConcurrentHashMap
?
-
Thread Safety Without Synchronization Bottlenecks: Unlike
HashMap
, which is not thread-safe, orCollections.synchronizedMap()
, which locks the entire map,ConcurrentHashMap
uses fine-grained locking (bucket-level or segment-level), allowing better concurrency. -
No
ConcurrentModificationException
: Iterators are weakly consistent, meaning they reflect some, but not necessarily all, updates made after the iterator was created. -
Atomic Operations: Methods like
putIfAbsent
,computeIfAbsent
, andcompute
allow atomic updates, which are crucial in concurrent environments.
Scenarios Where ConcurrentHashMap
Is Useful
1. Caching Frequently Accessed Data
ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
cache.putIfAbsent("user_123", fetchUserFromDB("user_123"));
- Multiple threads can read/write to the cache without blocking each other.
2. Counting Word Frequencies in Parallel
Map<String, Integer> wordCounts = new ConcurrentHashMap<>();
words.parallelStream().forEach(word ->
wordCounts.merge(word, 1, Integer::sum)
wordCounts.merge(word, 1, (oldVal,newVal) -> oldVal + newVal)
);
// key: The key with which the resulting value is to be associated.
// value: The non-null value to be merged with the existing value.
// remappingFunction: A function that takes the existing value and the new value,
// and returns the value to be associated with the key.
- Efficiently aggregates counts without race conditions.
3. Storing Session Data in a Web Server
Map<String, Session> sessions = new ConcurrentHashMap<>();
- Each thread handling a request can safely read/write session data.
4. Tracking Active Users in a Chat App
Map<String, UserConnection> activeUsers = new ConcurrentHashMap<>();
- Threads can add/remove users as they join/leave without locking the whole map.
5. Implementing a Thread-Safe Singleton Registry
Map<String, Object> registry = new ConcurrentHashMap<>();
registry.computeIfAbsent("serviceA", k -> new ServiceA());
- Ensures only one instance is created even under concurrent access.
6. Favor Composition Over Inheritance
✅ Good: Use interfaces and delegate
❌ Bad: Deep inheritance trees
7. Apply SOLID Principles
✅ Good: One class = one responsibility
❌ Bad: God classes
// ✅ Good
class InvoicePrinter {
void print(Invoice invoice) {}
}
class InvoiceSaver {
void save(Invoice invoice) {}
}
// ❌ Bad
class InvoiceManager {
void print(Invoice invoice) {}
void save(Invoice invoice) {}
}
8. Use Dependency Injection
✅ Good: Inject via constructor
❌ Bad: Instantiate dependencies inside class
9. Optimize with Profiling, Not Guesswork
✅ Good: Use profilers
❌ Bad: Premature optimization
// ✅ Good
// Use JVisualVM or Flight Recorder to identify slow methods
// ❌ Bad
// Rewriting code for performance without knowing if it's a bottleneck
10. Write Tests Like a Pro
✅ Good: Use JUnit and Mockito
❌ Bad: No tests or untestable code
// ✅ Good
@Test
void testAddition() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
// ❌ Bad
// No tests or tests that rely on external systems
11. Use Optional to Avoid Nulls
- Optional
is a powerful tool to avoid NullPointerException. - Use it in return types, not in fields or parameters.