Java Style Guide
Java Style Guide команды: 47 правил JS-N.M про инструментарий, дизайн, исключения, запреты, и 90 рецептов Effective Java как теоретический референс. Коды для AI-ревью.
Операционный свод правил Java-кода нашей команды. Чисто-форматирование (отступы, переносы, выравнивание, порядок модификаторов, naming-механика, wildcard imports, C-стиль массивов) делают IntelliJ formatter и Checkstyle — здесь только то, что требует решения человека или AI-ревьюера.
Базовый принцип (JV-1.1): любое нарушение руководства допустимо, если оно улучшает читаемость. От автора ожидается явное объяснение, чем нарушение лучше — в коде или в описании PR.
Каждое правило имеет код (JV-2.5, JV-4.7, …) — на эти коды ссылается AI-скилл ucp-java-style-review в findings.
1. Общие рекомендации
JV-1.1 Любое нарушение допустимо, если оно улучшает читаемость
Это не индульгенция «писать как хочется». От автора ожидается явное объяснение, чем именно нарушение лучше — в коде или в описании PR. Цель руководства — улучшить читаемость, понимание и общее качество кода, а не превратить ревью в формальную проверку.
2. Именование
JV-2.2 Без множественного числа в пакетах
Следуем java.util (не java.utils).
package com.example.util; // PREFER
package com.example.utils; // AVOID
JV-2.3 Имена классов — существительные
class Car {} // PREFER
class Running {} // AVOID
JV-2.4 Имена интерфейсов — существительные или прилагательные на -able
Не начинаем с I — это привычка из C# / .NET.
interface Runnable {} // PREFER
interface Comparable {} // PREFER
interface IEnumerable {} // AVOID
JV-2.5 Аббревиатуры
Не должно быть нескольких заглавных подряд. Лучше — отказаться от аббревиатур.
- 2 буквы — обе CAPS.
- 3+ букв — только первая буква CAPS (цифры не считаются).
class IOStream {} // PREFER (2 буквы)
class IoStream {} // AVOID
class XmlParser {} // PREFER (3 буквы)
class XMLParser {} // AVOID
class Pk2dfCertificate {} // PREFER
class C2CTariff {} // PREFER (C2C — 2 «буквы», цифра не считается)
class C2сTariff {} // AVOID
JV-2.6 Имена методов — глаголы или описание действия
public String getName() {} // PREFER
public String name() {} // AVOID
public void expand() {} // PREFER
public boolean expanding() {} // AVOID
JV-2.6.1 Имена тестов
Имя теста должно отражать суть тест-кейса. Два допустимых подхода:
// PREFER — длинное говорящее имя метода
@Test
public void shouldReturnNullIfResponseEmptyArray() {}
// PREFER — короткое имя + @DisplayName
@Test
@DisplayName("should return null if response is empty array")
public void someShorterName() {}
// AVOID — snake_case
public void should_Return_Null_If_Response_Empty_Array() {}
// AVOID — слишком длинное имя без @DisplayName
public void shouldReturnNullIfResponseEmptyArrayOrExternalSystemIsDownAndStuff() {}
3. Выражения и поток управления
JV-3.1 Method reference вместо лямбды, где это имеет смысл
filter(someStrings::contains); // PREFER
filter(s -> someStrings.contains(s)); // AVOID
JV-3.2 Большие лямбды — в named methods
Если в лямбде логика «больше одного выражения», вынеси её в отдельный метод и передавай method reference.
JV-3.3 Guard expression вместо вложенных условий
public void someMethod() { // PREFER
if (!condition) {
throw e;
}
doSomething();
}
public void someMethod() { // AVOID
if (condition) {
doSomething();
} else {
throw e;
}
}
4. Инструменты (обязательно)
Lombok, MapStruct, Bean Validation — обязательный инструментарий, без них код не проходит ревью. Они снимают boilerplate и закрывают целые классы багов из Effective Java одной аннотацией.
JV-4.1 @Value для immutable классов
DTO, value objects, события домена — @Value. Одна аннотация даёт final класс, private final поля, getter, equals + hashCode + toString, all-args constructor. Ручная реализация immutable-класса — устаревшая идиома.
@Value // PREFER
public class Money {
Currency currency;
BigDecimal amount;
}
public final class Money { // AVOID — boilerplate
private final Currency currency;
private final BigDecimal amount;
// + конструктор, getters, equals, hashCode, toString
}
JV-4.2 @Builder для классов с 4+ параметрами или необязательными полями
Длинный конструктор нечитаем и хрупок к перестановке аргументов. @Builder даёт именованные параметры. Для иерархий с наследованием — @SuperBuilder.
Order order = Order.builder() // PREFER
.customerId(customerId)
.items(items)
.deliveryAddress(address)
.priority(HIGH)
.build();
Order order = new Order(customerId, items, address, HIGH, null, false); // AVOID
JV-4.3 @EqualsAndHashCode обязателен парой
Никогда отдельно — equals без hashCode ломает HashMap/HashSet. Для DDD-агрегатов сравнение по идентификатору: @EqualsAndHashCode(onlyExplicitlyIncluded = true) + @EqualsAndHashCode.Include на поле ID.
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Order {
@EqualsAndHashCode.Include
private OrderId id;
private List<Item> items; // не входит в equals
}
JV-4.4 @ToString с @ToString.Exclude на чувствительных полях
PII, секреты, токены, пароли — @ToString.Exclude. Лог-инциденты от случайной печати пароля в toString — реальная угроза, не теоретическая.
@ToString
public class User {
private String email;
@ToString.Exclude
private String passwordHash;
@ToString.Exclude
private String apiToken;
}
JV-4.5 @UtilityClass для классов из одних статических методов
Делает класс final, добавляет private constructor с UnsupportedOperationException, помечает поля и методы static. Ручная идиома (private конструктор + AssertionError) допустима только для устаревших классов без Lombok.
@UtilityClass // PREFER
public class StringUtils {
public String capitalize(String input) { ... }
}
JV-4.6 @NonNull на параметрах не-public методов
Lombok генерирует Objects.requireNonNull в начале метода — fail-fast вместо отложенного NPE. Для public API контракта — Bean Validation (@NotNull), не @NonNull.
void updateName(@NonNull String newName) { // PREFER (внутренний метод)
this.name = newName;
}
JV-4.7 Constructor injection без @Autowired
@RequiredArgsConstructor + private final поля. Spring с Java 8+ резолвит DI без аннотации, если конструктор один. Полевая инъекция (@Autowired на поле) запрещена — ломает иммутабельность и тестируемость.
@Service
@RequiredArgsConstructor // PREFER
public class OrderService {
private final OrderRepository repository;
private final PaymentClient payments;
}
JV-4.8 MapStruct для маппинга между слоями
DTO ↔ entity ↔ domain — только MapStruct-mapper. Никакого ручного new TargetClass(source.getX(), source.getY(), ...). Маппер компилируется в чистый Java-код, типобезопасен, легко тестируем.
@Mapper(componentModel = "spring") // PREFER
public interface OrderMapper {
OrderDto toDto(Order order);
Order toDomain(OrderDto dto);
}
JV-4.9 Bean Validation на DTO + @Validated на контроллере
@NotNull, @Size, @Positive, @Pattern, @Email — на полях DTO. Не пишите if (x == null) throw ... в use case — это работа Bean Validation. См. Validation Style Guide.
public record CreateOrderRequest(
@NotNull @Positive Long customerId,
@NotEmpty @Size(max = 100) List<@Valid OrderItem> items,
@Email String contactEmail
) {}
5. Дизайн классов и методов
Правила, которые Lombok не автоматизирует — judgment-уровень про композицию, immutability, область видимости, сигнатуры. Это то, где AI-ревью и человек-ревьюер ловят design-проблемы.
JV-5.1 Композиция > наследование
Наследуйте только в sealed-иерархиях, шаблонном методе или абстрактных скелетных классах (AbstractList, AbstractMap). Иначе — wrapper-class через @Delegate Lombok или явная композиция полем.
public class TimedCache<K, V> { // PREFER — композиция
@Delegate
private final Map<K, V> delegate = new HashMap<>();
// + логика таймера
}
public class TimedCache<K, V> extends HashMap<K, V> { ... } // AVOID
JV-5.2 Immutable по умолчанию
DTO, value, события — @Value. Mutable только если профайлер показал GC pressure от копирования или нужен идентичный объект (entity с lifecycle). Сомневаетесь — immutable.
JV-5.3 Программируй к интерфейсу
Параметры, поля, возвращаемые типы — List<E>, Map<K,V>, Set<E>. Конкретные классы (ArrayList, HashMap) — только локальные переменные на месте создания.
public List<Order> findByStatus(Status s) { ... } // PREFER
public ArrayList<Order> findByStatus(Status s) { ... } // AVOID
JV-5.4 Минимум доступности
Package-private — первый шаг. public — только если действительно часть внешнего API. protected — только в каркасных классах для расширения. Чем уже видимость, тем меньше связность.
JV-5.5 Static nested > non-static по умолчанию
Non-static вложенный класс хранит ссылку на enclosing instance — лишняя память, утечка при сериализации, скрытая связность. Если ссылка не нужна — static.
public class Outer {
static class Inner { ... } // PREFER
class Inner { ... } // AVOID, если this не нужен
}
JV-5.6 Один top-level class на файл
Checkstyle ловит автоматически. Несколько публичных классов в одном файле — путаница в навигации и импортах.
JV-5.7 Optional<T> только для return-type
И только когда «нет результата» — частый и значимый исход (findById, parse). НЕ для полей, НЕ для параметров, НЕ для коллекций (используйте пустую коллекцию).
Optional<User> findByEmail(String email); // PREFER
void process(Optional<User> user); // AVOID — параметр
private Optional<String> name; // AVOID — поле
JV-5.8 Возвращайте пустую коллекцию, не null
Collections.emptyList(), List.of(), new String[0]. Клиент не должен помнить null-checks вокруг каждого вызова.
public List<Order> findRecent() {
if (cache.isEmpty()) {
return List.of(); // PREFER
}
// ...
}
JV-5.9 Не более 3-4 параметров метода
Больше — @Builder на параметрический объект (SearchCriteria, OrderRequest) или helper-класс. Длинный список параметров одного типа особенно опасен — легко перепутать порядок.
public List<Order> search(SearchCriteria criteria); // PREFER
public List<Order> search( // AVOID
String customer, Status status, LocalDate from,
LocalDate to, BigDecimal minAmount, BigDecimal maxAmount, int limit
);
6. Исключения
Исключения — для исключительных ситуаций, не для control flow. Граница слоя — место для translation, не для swallow.
JV-6.1 Стандартные исключения JDK > свои
IllegalArgumentException (плохой параметр), IllegalStateException (объект не в нужном состоянии), NullPointerException (null где не ждали), IndexOutOfBoundsException, UnsupportedOperationException. Свои — только для domain-specific случаев.
if (amount.signum() < 0) { // PREFER
throw new IllegalArgumentException("amount must be non-negative: " + amount);
}
JV-6.2 Checked exception — только при возможности recovery
Checked — когда у клиента есть meaningful recovery (повторить, fallback, спросить пользователя). Иначе — RuntimeException. Бессмысленный throws засоряет сигнатуры.
JV-6.3 Не игнорируйте exceptions
catch (X ignored) — только с комментарием почему игнорируется. Пустой catch без комментария — баг.
try { // PREFER
Files.delete(tmpFile);
} catch (IOException ignored) {
// временный файл удалится сборщиком ОС, потеря допустима
}
JV-6.4 Translate exceptions на границах слоёв
С chaining (new Wrapper(e)). IOException из persistence → RepositoryException(e) в use case. Низкоуровневые детали (SQLException, IOException) не должны просачиваться наверх.
try {
return jdbc.query(...);
} catch (DataAccessException e) {
throw new OrderLookupException("failed to load order " + id, e);
}
JV-6.5 Detail message — relevant variables
size=10, index=12, не "Произошла ошибка при обработке". Сообщение для логов и stacktrace, не для UI. Локализованные сообщения для пользователя — отдельный слой.
JV-6.6 Atomicity
Метод не оставляет объект в inconsistent state при failure. Способы: immutable (нечего ломать); проверка параметров до изменений; работа на копии с заменой в конце.
JV-6.7 Документируйте @throws в Javadoc
Каждый checked + значимые runtime. Это часть контракта метода — клиент должен знать, что ловить.
/**
* @throws IllegalArgumentException если amount отрицательный
* @throws InsufficientFundsException если на счёте меньше amount
*/
public void withdraw(BigDecimal amount) { ... }
7. Запрещено
То, что в нашей кодовой базе мы не пишем. Каждое из этих правил — следствие либо инструментария (Lombok закрывает), либо багов из практики, либо современный Java идиоматичнее старого подхода.
JV-7.1 Ручные equals / hashCode / toString
Только @EqualsAndHashCode / @ToString Lombok. Ручная реализация — источник тонких багов (забытое поле, несимметричный equals, hashCode не согласован с equals).
JV-7.2 clone() / implements Cloneable
Copy constructor Foo(Foo source) или MapStruct-mapper. Cloneable — сломанный по дизайну механизм с непредсказуемой семантикой.
JV-7.3 finalize() / Cleaner как primary cleanup
Только try-с-ресурсами + AutoCloseable. finalize() deprecated с Java 9, Cleaner — safety net, не основной механизм.
try (var conn = dataSource.getConnection()) { // PREFER
// ...
}
JV-7.4 Raw types
List без параметра типа — потеря type safety, warnings компилятора. Если параметр не важен — List<?>.
List<String> names = new ArrayList<>(); // PREFER
List<?> anything = service.fetch(); // PREFER (если тип не важен)
List names = new ArrayList(); // AVOID
JV-7.5 Object.wait() / notify() / notifyAll()
Только java.util.concurrent — CountDownLatch, BlockingQueue, Semaphore, CompletableFuture. Нативные методы Object для синхронизации — ловушка для условий гонки.
JV-7.6 new Thread(...) напрямую
Только ExecutorService — обычно Executors.newFixedThreadPool(N), ForkJoinPool.commonPool(), или Executors.newVirtualThreadPerTaskExecutor() для I/O-bound.
ExecutorService executor = // PREFER
Executors.newVirtualThreadPerTaskExecutor();
new Thread(() -> doWork()).start(); // AVOID
JV-7.7 java.io.Serializable для wire-формата
JSON (Jackson), Protobuf, Avro для inter-process данных. Serializable — только если действительно нужно (RMI, кэши JVM-уровня), и тогда с осторожностью.
JV-7.8 String для типизированных значений
Перечисление → enum. Дата → java.time.LocalDate / Instant. Деньги → BigDecimal или long копеек. ID → именованный value-object (UserId, OrderId).
public void process(OrderId orderId, Status status) { ... } // PREFER
public void process(String orderId, String status) { ... } // AVOID
JV-7.9 Boxed primitives в hot-path и счётчиках циклов
Long, Integer, Double в счётчиках циклов и hot-path — autoboxing создаёт объекты, GC pressure. Для коллекций и generics неизбежно, но осознанно.
long sum = 0L; // PREFER
for (int i = 0; i < n; i++) sum += data[i];
Long sum = 0L; // AVOID — n autoboxing операций
for (Integer i = 0; i < n; i++) sum += data[i];
JV-7.10 float / double для денег
BigDecimal (с MathContext или явным setScale + RoundingMode) или long копеек. Бинарная плавающая точка не представляет десятичные дроби точно.
BigDecimal total = price.multiply(BigDecimal.valueOf(qty)) // PREFER
.setScale(2, RoundingMode.HALF_UP);
double total = price * qty; // AVOID
JV-7.11 Public mutable static state
Global mutable = тестовый ад + race conditions. Если общий доступ нужен — Spring bean (DI) или явный singleton с immutable интерфейсом.
public class Config {
public static String dbUrl; // AVOID
}
@Component // PREFER
@ConfigurationProperties("app")
public class AppConfig { ... }
JV-7.12 null как значимое возвращаемое значение
Optional<T> для «может не быть», пустая коллекция для «список нулевой длины». null остаётся только для устаревших API (Map.get), и там сразу заворачивается в Optional.ofNullable.
Optional<User> findByEmail(String email); // PREFER
User findByEmail(String email); // returns null if not found // AVOID