Loading...
Loading...
Opinionated modern Java (21+) coding best practices, style guides, and anti-patterns. Curated by VirtusLab engineers. Covers code style, null safety, error handling, immutability, testing, concurrency, and tooling.
npx skill4agent add virtuslab/agent-skills java-best-practicesrecord@Value@Data@Getter// WRONG — legacy style
@Value
public class User {
String username;
String email;
}
// WRONG — hand-written boilerplate
public class User {
private final String username;
private final String email;
// constructor, getters, equals, hashCode, toString...
}
// CORRECT
public record User(String username, String email) {}@Buildersealedswitchpublic sealed interface PaymentResult
permits PaymentSuccess, PaymentFailure, PaymentPending {
}
public record PaymentSuccess(String transactionId, Money amount) implements PaymentResult {}
public record PaymentFailure(String reason, ErrorCode code) implements PaymentResult {}
public record PaymentPending(String transactionId) implements PaymentResult {}// CORRECT
String message = switch (result) {
case PaymentSuccess success ->
"Paid %s".formatted(success.amount());
case PaymentFailure failure ->
"Failed: %s".formatted(failure.reason());
case PaymentPending pending ->
"Pending: %s".formatted(pending.transactionId());
};
// WRONG — old-style switch statement with break
switch (result.getType()) {
case SUCCESS:
message = "Paid";
break;
// ...
}switch// CORRECT — null is an explicit case
return switch (status) {
case null -> "unknown";
case ACTIVE -> "active";
case PAUSED -> "paused";
};
// WRONG — null handled outside the switch
if (status == null) return "unknown";
return switch (status) {
case ACTIVE -> "active";
case PAUSED -> "paused";
};instanceof// CORRECT
if (obj instanceof String s && !s.isBlank()) {
process(s);
}
// WRONG
if (obj instanceof String) {
String s = (String) obj;
process(s);
}String query = """
SELECT u.id, u.name
FROM users u
WHERE u.active = true
ORDER BY u.name
""";varvarvar// CORRECT — type is obvious
var users = userRepository.findAll();
var mapper = new ObjectMapper();
// WRONG — type is not obvious, use explicit type
var result = process(data);instanceofswitch// CORRECT
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
String desc = switch (shape) {
case Circle(double r) -> "Circle r=" + r;
case Rect(double w, double h) -> "Rect " + w + "x" + h;
};
// WRONG — old style: cast + field access
if (obj instanceof Point p) {
System.out.println(p.x() + ", " + p.y());
}_// CORRECT
try { ... } catch (Exception _) { ... }
try (var _ = ScopedValue.where(KEY, value)) { ... }
list.stream().map(_ -> "constant").toList();
// WRONG
try { ... } catch (Exception ignored) { ... }
try { ... } catch (Exception e) { ... } // when e is never usedArrays.asList()new ArrayList<>()// CORRECT
var roles = List.of("ADMIN", "USER");
var config = Map.of("timeout", 30, "retries", 3);
var tags = Set.of("java", "backend");
// WRONG
var roles = Arrays.asList("ADMIN", "USER");
var roles = new ArrayList<>(List.of("ADMIN", "USER"));
var roles = Collections.unmodifiableList(Arrays.asList("ADMIN", "USER"));getFirst()getLast()reversed()// CORRECT
var first = list.getFirst();
var last = list.getLast();
var rev = list.reversed();
// WRONG
var first = list.get(0);
var last = list.get(list.size() - 1);
var rev = new ArrayList<>(list); Collections.reverse(rev);String// CORRECT
str.isBlank() // replaces str.trim().isEmpty()
str.strip() // Unicode-aware trim
"ha".repeat(3) // "hahaha"
"Hello %s".formatted(name) // replaces String.format(...)
str.lines() // Stream<String> of lines
// WRONG
str.trim().isEmpty()
String.format("Hello %s", name)@NonNull@NullableOptionalnullnullOptional// CORRECT — Optional for methods that may return nothing
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(repository.get(email));
}
// WRONG — returning null
public User findByEmail(String email) {
return repository.get(email); // might return null
}OptionalOptionalOptional.get()orElseThrow()orElse()map()flatMap()ifPresent()// CORRECT
userService.findByEmail(email)
.map(User::username)
.orElseThrow(() -> new UserNotFoundException(email));
// WRONG
User user = userService.findByEmail(email).get();ResultRuntimeExceptionExceptionThrowable// CORRECT
catch (IOException | SQLException e) {
log.error("Data access failed", e);
}
// WRONG — duplicated handlers
catch (IOException e) { log.error("...", e); }
catch (SQLException e) { log.error("...", e); }// CORRECT
public User getUser(UserId id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// WRONG — returning null on error
public User getUser(UserId id) {
try {
return userRepository.findById(id).orElse(null);
} catch (Exception e) {
return null;
}
}PascalCaseUserServicePaymentProcessorcamelCasefindByEmailprocessPaymentcalculateTotalcamelCaseuserIdorderItemsUPPER_SNAKE_CASEMAX_RETRY_COUNTDEFAULT_TIMEOUTcom.company.project.domain.userishasshouldcanisValid()hasPermission()shouldRetry()usersordersproductsuserRepositoryuserRepotransactionManagertxMgrpublicfinalUser::userIdu -> u.userId()./gradlew spotlessApplymvn spotless:applyfilter()map()flatMap()toList()// CORRECT
var activeEmails = users.stream()
.filter(User::isActive)
.map(User::email)
.toList();
// WRONG — manual loop for simple transformation
var activeEmails = new ArrayList<String>();
for (User user : users) {
if (user.isActive()) {
activeEmails.add(user.getEmail());
}
}mapfilterflatMapforEachIterable.forEach()stream().forEach()// WRONG — side effect in intermediate operation
users.stream()
.map(user -> { emailService.send(user); return user.email(); })
.toList();
// WRONG — unnecessary stream() when there is no pipeline
users.stream().forEach(user -> emailService.send(user));
// CORRECT — Iterable.forEach for simple side effects
users.forEach(user -> emailService.send(user));
// CORRECT — pure filter, side effect only in terminal forEach
users.stream()
.filter(User::isActive)
.forEach(user -> emailService.send(user));
// CORRECT — plain loop when you need break/continue or checked exceptions
for (var user : users) {
emailService.send(user);
}methodName_shouldExpectedBehavior_whenCondition@Test
void findByEmail_shouldReturnUser_whenUserExists() { ... }
@Test
void findByEmail_shouldThrowNotFoundException_whenUserDoesNotExist() { ... }
@Test
void processPayment_shouldReturnSuccess_whenBalanceSufficient() { ... }camelCasesnake_casegiven/when/thenshould@DisplayName// CORRECT — Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future = executor.submit(() -> fetchData(url));
return future.get();
}
// WRONG — reactive chains for simple I/O
Mono.fromCallable(() -> fetchData(url))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(data -> process(data))
.subscribe();StructuredTaskScope// CORRECT
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> fetchUser(id));
var order = scope.fork(() -> fetchOrder(id));
scope.join().throwIfFailed();
return new Response(user.get(), order.get());
}
// WRONG — manual executor management, no automatic lifecycle
var executor = Executors.newFixedThreadPool(2);
var f1 = executor.submit(() -> fetchUser(id));
var f2 = executor.submit(() -> fetchOrder(id));
executor.shutdown();
return new Response(f1.get(), f2.get());CompletableFutureFilesPath.of()BufferedReaderWriter// CORRECT
String content = Files.readString(Path.of("config.json"));
Files.writeString(Path.of("output.txt"), data);
var path = Path.of("src", "main", "java");
// WRONG
var br = new BufferedReader(new FileReader("config.json"));
// ... manual read loop ...
var path = Paths.get("src", "main", "java");HttpClientHttpURLConnection// CORRECT
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(url)).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// WRONG — HttpURLConnection with manual stream handling
var conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
var reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
// ...java.timeDateCalendarSimpleDateFormat// CORRECT
LocalDate today = LocalDate.now();
LocalDate date = LocalDate.of(2025, Month.JANUARY, 15);
Instant now = Instant.now();
long days = ChronoUnit.DAYS.between(start, end);
Duration duration = Duration.ofMinutes(30);
// WRONG
Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.set(2025, 0, 15); // zero-indexed monthsorg.slf4j.LoggerSystem.out.println()System.err.println()// CORRECT
log.info("Processing order {} for user {}", orderId, userId);
// WRONG
log.info("Processing order " + orderId + " for user " + userId);errorwarninfodebugtraceconfigcom.company.project.<domain>.<layer>com.acme.shop.order.repository| Deprecated Pattern | Modern Replacement |
|---|---|
Lombok | |
| |
| |
| |
| RxJava / Project Reactor for simple I/O | Virtual Threads |
Old | Switch expressions |
Manual | Pattern matching |
String concatenation with | Text blocks |
| Anonymous inner classes for functional interfaces | Lambdas / method references |
| |
Returning | |
| SLF4J logging |
Raw types ( | Always use generics |
| |
| |
| |
| |
| |
| |
| |
Duplicate | Multi-catch |
| |
| |
| |
| Manual executor lifecycle for structured tasks | |