Java Code Review Checklist
Rapid-scan checklist for Java code reviews. Use this to quickly identify issues, then dive into linked documentation for details.
Security Red Flags - Block PR Immediately
- SQL injection: String concatenation in SQL queries → Use parameterized queries/PreparedStatement
- Deserialization of untrusted data:
ObjectInputStream.readObject()on external input → Deserialization vulnerabilities - Path traversal: User input directly in file paths (
new File(userInput)) → Validate and sanitize paths - Command injection: Runtime.exec() with user input → Avoid or use allowlists
- Hardcoded secrets: Passwords, API keys, tokens in source code → Configuration Management
- Reflection abuse:
Class.forName()orMethod.invoke()with unsanitized user input - XML External Entity (XXE): XML parsers without disabling external entities
- Insecure random: Using
java.util.Randomfor security tokens → UseSecureRandomfor cryptographic operations - Sensitive data in logs: Passwords, credit cards, SSNs, API keys in log statements → Sensitive Data Protection
- Missing input validation: User input not validated before processing
- Insecure crypto: Weak algorithms (MD5, SHA1, DES) or ECB mode → Use AES-256-GCM, SHA-256+
Common Mistakes
Exception Handling
- Catching generic
ExceptionorThrowable(too broad) → Catch specific exceptions - Empty catch blocks swallowing exceptions
- Not logging exceptions before rethrowing
- Exposing stack traces to users (leaks implementation details)
- Using exceptions for control flow instead of validation
- Not including cause when wrapping exceptions → Use
new CustomException(message, cause)
Resource Management
- Not using try-with-resources for
AutoCloseableresources → Resource leaks prevention - Streams/readers/writers not closed properly
- Database connections not returned to pool
- File handles leaked in error paths
- Thread pools not shut down in
finallyblock
// GOOD: try-with-resources ensures cleanup
try (var stream = Files.lines(path)) {
stream.forEach(System.out::println);
}
// BAD: Stream never closed if exception occurs
var stream = Files.lines(path);
stream.forEach(System.out::println);
Null Handling
- Missing null checks on method parameters
- Returning
nullinstead ofOptionalfor nullable values → Optional usage - Not using
@NonNull/@Nullableannotations - Calling methods on potentially null references without checks
Collections
- Using mutable collections when immutable would work → Use
List.of(),Set.of(),Map.of()(Collections) - Modifying collections while iterating (causes
ConcurrentModificationException) - Using wrong collection type (ArrayList for frequent inserts/deletes at beginning)
- Not specifying initial capacity for large collections (causes resizing overhead)
Equals and HashCode
- Overriding
equals()withouthashCode()(breaksHashSet/HashMap) - Using mutable fields in
hashCode()calculation equals()not symmetric, transitive, or consistent- Not checking
instanceofbefore casting inequals()
Watch For - Concurrency & Thread Safety
Thread Safety Issues
- Mutable static fields accessed by multiple threads → Java Concurrency
- Missing synchronization on shared mutable state
- Double-checked locking without
volatile(broken pattern) - Non-thread-safe collections (ArrayList, HashMap) shared between threads
- Race conditions in check-then-act patterns
// BAD: Race condition - multiple threads can pass check
if (counter < MAX) {
counter++; // Not atomic!
}
// GOOD: Atomic operation
atomicCounter.getAndIncrement();
Virtual Threads Issues (Java 21+)
- Pinning virtual threads with
synchronizedblocks doing I/O → UseReentrantLock(Virtual Threads) - Using thread-local storage heavily (memory overhead with millions of virtual threads)
- CPU-bound tasks in virtual threads (better on platform threads)
- Not using virtual threads for I/O-bound operations in Spring Boot 3.2+
Concurrency Primitives
- Using
Thread.stop()orThread.suspend()(deprecated, unsafe) - Not handling
InterruptedExceptionproperly (catching and ignoring) - Busy-waiting instead of using
wait()/notify()or higher-level constructs - Creating threads directly instead of using
ExecutorService - Not shutting down
ExecutorService(leaks threads)
CompletableFuture & Async
- Not handling exceptions in
CompletableFuturechains → Useexceptionally()orhandle() - Blocking operations in async callbacks (defeats purpose)
- Missing timeout on future operations (can hang indefinitely)
- Using default executor (ForkJoinPool.commonPool) for I/O tasks
Watch For - Performance & Correctness
String Operations
- String concatenation in loops (
str += x) → UseStringBuilder(Performance) - Using
String.format()in hot paths (slow) - Not reusing compiled
Patternfor repeated regex operations String.split()whenindexOf()would work
Object Creation
- Creating unnecessary objects in tight loops
- Not reusing immutable objects (wrapper types, strings)
- Using
new String("literal")instead of string literal - Boxing/unboxing in performance-critical code
Streaming & Collections
- Using streams for trivial operations (overhead vs simple loop)
- Not using parallel streams when beneficial (CPU-bound bulk operations)
- Using parallel streams for I/O operations (blocks threads)
- Creating intermediate collections unnecessarily
// BAD: Creates intermediate list
list.stream()
.collect(Collectors.toList())
.stream()
.filter(...)
// GOOD: Single stream pipeline
list.stream()
.filter(...)
Large Methods
- Methods longer than 40-50 lines (hard to test/understand)
- Cyclomatic complexity > 10 (too many branches)
- Deeply nested conditionals (> 3 levels)
- Missing early returns for validation
Best Practices - Code Quality
Modern Java Features (Java 21+)
- Records for immutable DTOs → Records
- Sealed classes for type hierarchies → Sealed Classes
- Pattern matching for
instanceofandswitch→ Pattern Matching - Text blocks for multi-line strings → Text Blocks
- Virtual threads for I/O-bound concurrency → Virtual Threads
Optionalfor nullable return values → Optional
Immutability
- Final fields by default → Immutability
- Immutable collections (
List.of(),Set.of(),Map.of()) - Defensive copying when exposing internal state
- Records instead of mutable POJOs for data carriers
Enums & Constants
- Enums instead of string/int constants → Enums
- Enums with behavior (methods per constant)
- Constants defined close to usage, not in global
Constantsclass → Constants Location
SOLID Principles
- Single Responsibility: one class, one reason to change → SRP
- Dependency Inversion: depend on interfaces, not concrete classes → DIP
- Constructor injection over field injection
- Composition over inheritance
Exception Handling
- Custom exceptions with domain context → Custom Exceptions
- Specific exception types (not generic
Exception) - Include cause when rethrowing/wrapping
- Proper logging before rethrowing
Code Quality
- Zero compiler warnings (enable
-Werror) - No
@SuppressWarningswithout justification comment - Spotless/Google Java Format for consistent formatting → Code Quality
- JavaDoc for public APIs and complex methods
Quick Scan Table
| Category | What to Check | Severity | Reference |
|---|---|---|---|
| Security | No SQL string concatenation | Block | SQL Injection |
| Security | No deserialization of untrusted data | Block | Security |
| Security | No path traversal vulnerabilities | Block | Validate file paths |
| Security | No hardcoded secrets | Block | Configuration |
| Security | SecureRandom for crypto (not Random) | Block | Use SecureRandom |
| Security | No sensitive data in logs | Block | Logging |
| Resources | try-with-resources for AutoCloseable | Fix | Java General |
| Exceptions | No empty catch blocks | Fix | Exception Handling |
| Exceptions | Specific exceptions (not generic) | Fix | Exception Handling |
| GOOD: | Null Safety | Optional for nullable returns | Review |
| Concurrency | No mutable static fields | Block | Concurrency |
| Concurrency | Proper synchronization on shared state | Fix | Concurrency |
| GOOD: | Concurrency | Virtual threads for I/O operations | Review |
| Concurrency | No pinning with synchronized + I/O | Fix | Virtual Threads |
| GOOD: | Collections | Immutable collections preferred | Review |
| Equals/Hash | hashCode() overridden with equals() | Fix | Common practice |
| Performance | StringBuilder in loops (not +=) | Fix | Performance |
| GOOD: | Modern Java | Records for immutable DTOs | Review |
| GOOD: | Modern Java | Sealed classes for type hierarchies | Review |
| GOOD: | Modern Java | Pattern matching used | Review |
| Code Quality | No compiler warnings | Fix | Code Quality |
Legend:
- Block PR - Security vulnerability or data corruption risk
- Fix Before Merge - Performance issue, correctness problem, or likely bug
- [GOOD] Review & Discuss - Code quality improvement
Additional Considerations
Deprecated APIs
- Using deprecated methods without migration plan
- Not addressing deprecation warnings
- Missing
@Deprecatedannotation with@deprecatedJavaDoc explaining alternative
Date/Time APIs
- Using legacy
Date,Calendar,SimpleDateFormat→ Usejava.time.*(LocalDate, Instant, ZonedDateTime) - Not using
DateTimeFormatterfor thread-safe date formatting - Missing timezone awareness when needed (
ZonedDateTimevsLocalDateTime)
Generics
- Raw types (missing generic parameters) → Use
List<String>notList - Unchecked casts without
@SuppressWarnings("unchecked")and justification - Type erasure issues in complex generic hierarchies
- Not using bounded wildcards when appropriate (
? extends T,? super T)
Serialization
- Implementing
SerializablewithoutserialVersionUID - Transient fields not properly initialized on deserialization
- Security vulnerabilities from deserializing untrusted data
- Not considering alternatives (JSON, Protocol Buffers) to Java serialization
Logging
- String concatenation in log statements → Use placeholders
log.info("User {}", userId)(Logging) - Wrong log levels (DEBUG for errors, ERROR for info)
- Logging entire request/response objects (may contain PII)
- Not including correlation IDs for tracing
Testing Considerations
- Missing unit tests for public methods → Java Testing
- Not testing edge cases (null, empty, boundary values)
- Integration tests without TestContainers (using in-memory DBs incorrectly)
- Missing mutation testing coverage
- Tests with hard-coded sleeps (
Thread.sleep()) instead of proper synchronization
Block Checklist - Must Fix Before Merge
- No SQL injection vulnerabilities (parameterized queries only)
- No deserialization of untrusted data
- No path traversal vulnerabilities
- No hardcoded secrets in code
-
SecureRandomused for security operations (notRandom) - No sensitive data in logs
- All
AutoCloseableresources use try-with-resources - No empty catch blocks
- No mutable static fields accessed by multiple threads
-
equals()andhashCode()overridden together - No compiler warnings (or justified suppressions)
- Virtual threads not pinned with synchronized blocks doing I/O
Java-Specific Testing Checklist
Before approving Java PRs:
- Unit tests cover happy path and error cases → Java Testing
- Edge cases tested (null, empty, boundary values)
- Exceptions properly tested
- Mocks/stubs used appropriately
- Integration tests use TestContainers (not H2 for PostgreSQL) → Testing
- Mutation testing passing configured threshold
- No flaky tests (tests pass consistently)
- Tests don't rely on execution order
- No hard-coded
Thread.sleep()(use proper await/polling) - Test names clearly describe what's being tested
Related Documentation
Java Language Guides:
- Java General Guidelines - Modern features, immutability, SOLID principles
- Java Concurrency - Virtual threads, thread safety, async patterns
- Java Performance - Optimization, profiling, common pitfalls
- Java Testing - Unit testing, integration testing, best practices
Framework-Specific (Spring Boot):
Process: