| name | java |
| description | This skill should be used when the user asks to "write java code", "java best practices", "jdk features", "java records", "java streams", "java optional", "java testing", "java spring", "java patterns", or needs guidance on professional Java development. |
Java Development Best Practices
Apply these standards when writing Java code to ensure maintainability, performance, and professional quality.
Naming Conventions
Follow Oracle's Java naming conventions consistently throughout the codebase.
Use PascalCase for:
- Classes and interfaces:
UserService, OrderRepository, Comparable
- Enums:
OrderStatus, LogLevel
- Annotation types:
Override, Deprecated
Use camelCase for:
- Methods:
getUserById, calculateTotal, isActive
- Variables and parameters:
userId, orderCount, firstName
- Non-constant fields:
private String userName
Use UPPER_SNAKE_CASE for:
- Constants:
MAX_RETRY_COUNT, DEFAULT_TIMEOUT
- Enum constants:
OrderStatus.PENDING, LogLevel.WARNING
Use lowercase with dots for packages: com.company.project.module. Use meaningful names that reveal intent. Avoid Hungarian notation and unnecessary prefixes.
public class OrderProcessingService {
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final Duration RETRY_DELAY = Duration.ofSeconds(5);
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final Logger logger;
public OrderResult processOrder(OrderRequest request) {
var orderId = request.getOrderId();
}
}
Modern Java Features (JDK 17+)
Records for Data Classes
Use records for immutable data carriers. Records provide constructors, accessors, equals(), hashCode(), and toString() automatically.
public record User(String id, String name, String email) {}
public record Order(String id, String customerId, BigDecimal total, List<OrderLine> lines) {
public Order {
Objects.requireNonNull(id, "id must not be null");
Objects.requireNonNull(customerId, "customerId must not be null");
if (total.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("total must not be negative");
}
lines = List.copyOf(lines);
}
public int lineCount() {
return lines.size();
}
}
var user = new User("1", "Alice", "alice@example.com");
var email = user.email();
Sealed Classes
Use sealed classes to restrict inheritance and enable exhaustive pattern matching.
public sealed interface Shape permits Circle, Rectangle, Triangle {
double area();
}
public record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
public record Triangle(double base, double height) implements Shape {
@Override
public double area() {
return 0.5 * base * height;
}
}
public String describeShape(Shape shape) {
return switch (shape) {
case Circle c -> "Circle with radius " + c.radius();
case Rectangle r -> "Rectangle " + r.width() + "x" + r.height();
case Triangle t -> "Triangle with base " + t.base();
};
}
Pattern Matching
Use pattern matching for cleaner type checks and casts.
public String formatValue(Object obj) {
if (obj instanceof String s) {
return "String: " + s.toUpperCase();
} else if (obj instanceof Integer i) {
return "Integer: " + i * 2;
} else if (obj instanceof List<?> list && !list.isEmpty()) {
return "List with " + list.size() + " elements";
}
return "Unknown: " + obj;
}
public double calculateArea(Object shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case null -> 0.0;
default -> throw new IllegalArgumentException("Unknown shape");
};
}
public String categorize(Integer value) {
return switch (value) {
case Integer i when i < 0 -> "negative";
case Integer i when i == 0 -> "zero";
case Integer i when i <= 100 -> "small";
default -> "large";
};
}
Streams and Functional Programming
Use streams for declarative data processing. Keep stream pipelines readable and focused.
List<String> activeUserEmails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.filter(email -> email.endsWith("@company.com"))
.sorted()
.toList();
Map<Department, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Map<Department, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
BigDecimal totalRevenue = orders.stream()
.map(Order::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Use method references when they improve readability. Avoid side effects in stream operations. Prefer toList() (JDK 16+) over collect(Collectors.toList()).
long count = hugeList.parallelStream()
.filter(this::expensiveComputation)
.count();
list.parallelStream()
.map(this::transform)
.forEachOrdered(System.out::println);
Optional Usage
Use Optional for return types that may have no value. Never use Optional for fields, method parameters, or collection elements.
public Optional<User> findUserById(String id) {
return Optional.ofNullable(userMap.get(id));
}
public String getUserEmail(String userId) {
return findUserById(userId)
.map(User::getEmail)
.orElse("unknown@example.com");
}
public User getRequiredUser(String userId) {
return findUserById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
findUserById(userId).ifPresent(user -> {
logger.info("Found user: {}", user.getName());
sendWelcomeEmail(user);
});
public Optional<String> getActiveUserEmail(String userId) {
return findUserById(userId)
.filter(User::isActive)
.map(User::getEmail);
}
Avoid Optional.get() without checking isPresent(). Use orElse() for default values, orElseGet() for computed defaults, and orElseThrow() for required values.
Exception Handling
Use specific exceptions for different error categories. Create custom exceptions for domain-specific errors.
public class DomainException extends RuntimeException {
protected DomainException(String message) {
super(message);
}
protected DomainException(String message, Throwable cause) {
super(message, cause);
}
}
public class EntityNotFoundException extends DomainException {
private final String entityType;
private final String entityId;
public EntityNotFoundException(String entityType, String entityId) {
super("%s with ID '%s' not found".formatted(entityType, entityId));
this.entityType = entityType;
this.entityId = entityId;
}
public String getEntityType() { return entityType; }
public String getEntityId() { return entityId; }
}
public Order processOrder(String orderId) {
try {
var order = orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("Order", orderId));
paymentService.processPayment(order);
return orderRepository.save(order);
} catch (PaymentDeclinedException e) {
logger.warn("Payment declined for order {}: {}", orderId, e.getMessage());
throw new OrderProcessingException("Payment failed", e);
} catch (DatabaseException e) {
logger.error("Database error processing order {}", orderId, e);
throw new OrderProcessingException("System error", e);
}
}
Use try-with-resources for all AutoCloseable resources. Log exceptions with appropriate levels. Preserve exception chains with cause.
Dependency Injection
Use constructor injection for required dependencies. Prefer immutability with final fields.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
private final Logger logger;
public OrderService(
OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = Objects.requireNonNull(orderRepository);
this.paymentService = Objects.requireNonNull(paymentService);
this.notificationService = Objects.requireNonNull(notificationService);
this.logger = LoggerFactory.getLogger(getClass());
}
public Order createOrder(CreateOrderRequest request) {
logger.info("Creating order for customer {}", request.customerId());
}
}
Avoid field injection (@Autowired on fields)—it hides dependencies and complicates testing. Use interfaces for dependencies to enable mocking and alternative implementations.
Testing with JUnit 5
Structure tests using Arrange-Act-Assert pattern. Use descriptive test names that explain the scenario.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@Mock
private NotificationService notificationService;
@Mock
private CustomerService customerService;
@InjectMocks
private OrderService orderService;
@Test
@DisplayName("Should create order successfully with valid request")
void createOrder_withValidRequest_returnsCreatedOrder() {
var request = new CreateOrderRequest("customer-1", List.of(
new OrderItem("product-1", 2, new BigDecimal("10.00"))
));
when(orderRepository.save(any(Order.class)))
.thenAnswer(invocation -> {
Order order = invocation.getArgument(0);
return order.withId("order-1");
});
Order result = orderService.createOrder(request);
assertThat(result.getId()).isEqualTo("order-1");
assertThat(result.getCustomerId()).isEqualTo("customer-1");
assertThat(result.getLines()).hasSize(1);
verify(orderRepository).save(any(Order.class));
verify(notificationService).sendOrderConfirmation(any());
}
@Test
@DisplayName("Should throw exception when customer not found")
void createOrder_withInvalidCustomer_throwsException() {
var request = new CreateOrderRequest("invalid-customer", List.of());
when(customerService.findById("invalid-customer"))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> orderService.createOrder(request))
.isInstanceOf(EntityNotFoundException.class)
.hasMessageContaining("Customer")
.hasMessageContaining("invalid-customer");
}
@ParameterizedTest
@ValueSource(ints = {0, -1, -100})
@DisplayName("Should reject orders with invalid quantities")
void createOrder_withInvalidQuantity_throwsValidationException(int quantity) {
var request = new CreateOrderRequest("customer-1", List.of(
new OrderItem("product-1", quantity, new BigDecimal("10.00"))
));
assertThatThrownBy(() -> orderService.createOrder(request))
.isInstanceOf(ValidationException.class);
}
}
Use AssertJ for fluent assertions. Use @Nested classes to organize related tests. Use @ParameterizedTest for data-driven tests.
Concurrency
Use modern concurrency utilities from java.util.concurrent. Prefer immutable objects to avoid synchronization.
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public CompletableFuture<OrderDetails> getOrderDetails(String orderId) {
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderRepository.findById(orderId).orElseThrow());
CompletableFuture<Customer> customerFuture = orderFuture
.thenCompose(order ->
CompletableFuture.supplyAsync(() -> customerService.findById(order.getCustomerId())));
CompletableFuture<List<Product>> productsFuture = orderFuture
.thenCompose(order ->
CompletableFuture.supplyAsync(() -> productService.findByIds(order.getProductIds())));
return orderFuture
.thenCombine(customerFuture, (order, customer) -> new Object[] {order, customer})
.thenCombine(productsFuture, (arr, products) ->
new OrderDetails((Order) arr[0], (Customer) arr[1], products));
}
OrderDetails getOrderDetailsStructured(String orderId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var orderFuture = scope.fork(() -> orderRepository.findById(orderId).orElseThrow());
var customerFuture = scope.fork(() -> customerService.findById(customerId));
scope.join();
scope.throwIfFailed();
return new OrderDetails(orderFuture.get(), customerFuture.get());
}
}
Use ConcurrentHashMap for thread-safe maps. Use AtomicReference and atomic classes for simple shared state. Avoid synchronized blocks when possible—use higher-level abstractions.
Code Quality Tools
Configure static analysis tools for consistent code quality.
Checkstyle Configuration
<module name="Checker">
<module name="TreeWalker">
<module name="ConstantName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName"/>
<module name="MethodName"/>
<module name="PackageName"/>
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<module name="AvoidStarImport"/>
<module name="IllegalImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<module name="MethodLength">
<property name="max" value="50"/>
</module>
<module name="ParameterNumber">
<property name="max" value="7"/>
</module>
</module>
</module>
SpotBugs and Error Prone
Configure SpotBugs for bug detection and Error Prone for compile-time checks.
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.2.0</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
</configuration>
</plugin>
Documentation with Javadoc
Document all public APIs with Javadoc. Include @param, @return, @throws, and @see tags.
public Order processOrder(String orderId) {
}
Use {@code} for inline code and {@link} for references. Document thread safety and null handling. Keep documentation synchronized with code changes.
Additional Resources
For detailed patterns and anti-patterns, consult:
references/patterns.md - Comprehensive Java patterns and examples