Java Style Guide

Java Style Guide команды: 47 правил JS-N.M про инструментарий, дизайн, исключения, запреты, и 90 рецептов Effective Java как теоретический референс. Коды для AI-ревью.

Статья внедрена в скилл AI-агента ucp-java-style-review

Операционный свод правил 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.concurrentCountDownLatch, 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 для денег

BigDecimalMathContext или явным 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