java-best-practices について
Java 開発者やコーディングエージェント向けの、Java 21 以降を対象とする実践スキルです。設計、実装、エラー処理、テストを扱い、古い書き方を避ける指針を示します。
主な機能
- 01Java 21 以降の指針
- 02コンポーズドメソッドとコンポジション
- 03レコードとシールド型
- 04null と例外の処理
- 05ストリーム、コレクション、テスト
活用シーン
- 01現代的な指針に沿った Java コードレビュー
- 02Java 21 以降の機能の実装
- 03メソッドとクラス設計の改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ virtuslab/ agent-skills --skill java-best-practicesインストール後、Claude Code で/java-best-practicesを入力して読み込まれたか確認します。
同梱ファイルexamples/ImmutableUser.java
詳細
VirtusLab による Java のベストプラクティス
独自の方針に基づく、モダンな Java スキルです。Java 21 以降を対象とし、時代遅れのパターンは明確に排除しています。Java コードを作成またはレビューする際は、これらのガイドラインに厳密に従ってください。
基本理念
- Composed Method パターンは、Java で最も大きな効果をもたらすプラクティスです。すべてのメソッドは、1 つの抽象化レベルで 1 つのことだけを行い、一目で読めるほど短くする必要があります。このパターンだけで、読みにくいコードをクリーンなコードに変えられます。
- **Joshua Bloch 著『Effective Java』**は、今なお権威あるリファレンスです。その原則は現在も有効です。迷ったときは、同書を参照してください。
- **ライブラリのコードを使いましょう。**標準ライブラリや定評のあるライブラリがすでに提供している機能を再実装してはいけません。これは最もよく破られ、かつ最も大きな効果をもたらす原則の 1 つです。
- **継承よりコンポジションを優先します。**常にそうしてください。深いクラス階層の代わりに、デフォルトメソッドを持つインターフェースと委譲を使います。
- オブジェクト指向設計では、GRASP の原則(General Responsibility Assignment Software Patterns)が指針となります:Information Expert、Creator、Controller、Low Coupling、High Cohesion、Polymorphism、Pure Fabrication、Indirection、Protected Variations。
モダンな Java(21 以降)— これらの機能を使う
モダンな Java の機能を積極的に使ってください。Java 17 より前のスタイルでコードを書いてはいけません。
データオブジェクトにはレコードを使う
不変なデータ保持オブジェクトにはすべて record を使ってください。データオブジェクトに Lombok の @Value、@Data、@Getter や、手書きの POJO を使ってはいけません。
// 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) {}
Lombok が許容されるのは、ビルダーパターンが本当に必要な非 record クラスで @Builder を使う場合に限ります。それ以外は、record またはプレーンな Java を使ってください。
ドメインモデリング(ADT)にはシールドクラスを使う
代数的データ型をモデル化するには、sealed クラスとインターフェースを使ってください。これにより、switch 式で網羅性をチェックできます。
public 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 {}
パターンマッチングを使った switch 式
パターンマッチングを使った switch 式(文ではなく)を使ってください。網羅的な switch を優先してください。
// 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 内での null 処理(Java 21 以降)
// 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);
}
テキストブロック
複数行の文字列(SQL、JSON、HTML など)にはテキストブロックを使ってください。
String query = """
SELECT u.id, u.name
FROM users u
WHERE u.active = true
ORDER BY u.name
""";
ローカル変数には var を使う
右辺から型が明らかな場合は、ローカル変数に var を使ってください。可読性が下がる場合は var を使わないでください。
// CORRECT — type is obvious
var users = userRepository.findAll();
var mapper = new ObjectMapper();
// WRONG — type is not obvious, use explicit type
var result = process(data);
レコードパターン(Java 21)
instanceof 式と switch 式で、レコードを直接分解してください。
// 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());
}
名前なし変数(Java 22)
未使用の変数、catch パラメーター、ラムダパラメーターには _ を使ってください。
// 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 used
不変コレクション
不変コレクションにはファクトリメソッドを使ってください。固定コレクションに Arrays.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"));
シーケンスコレクション(Java 21)
インデックス計算の代わりに、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);
文字列ユーティリティ(Java 11 以降)
モダンな 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)
Null 安全性
**システム内部で null は許容されません。**デフォルトでは、すべて非 null とします。システム境界(外部 API、データベースの結果、ユーザー入力)を越えて null を伝播させないでください。
- すべてに
@NonNullを付けてはいけません — コードが煩雑になります。代わりに、デフォルトですべて非 null として扱ってください。 @Nullableを使うのは、本当に null になり得る場合(まれな、システム境界に限る)だけです。- 外部ソースから null を受け取った場合は、境界で
Optional、デフォルト値、または早期の例外スローに変換してください。 - **メソッド引数として
nullを渡してはいけません。**メソッドからnullを返してはいけません。結果が存在しない可能性のあるメソッドにはOptionalを使ってください。
// 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
}
Optional
- 結果がない場合がある公開メソッドの戻り値には、
Optionalを使用します。意図が伝わり、「このメソッドは何も返さない場合があり、その場合に対処する必要がある」ことを示せます。 - フィールド型、メソッドのパラメーター、コレクションには、絶対に
Optionalを使用しないでください。 - 確認せずに絶対に
Optional.get()を呼び出さないでください。orElseThrow()、orElse()、map()、flatMap()、ifPresent()を使用します。
// CORRECT
userService.findByEmail(email)
.map(User::username)
.orElseThrow(() -> new UserNotFoundException(email));
// WRONG
User user = userService.findByEmail(email).get();
エラー処理
**Java の例外機構を使用してください。**エラー戻り値コードやResultラッパー型を使って、Java を Go や C のようにしようとしないでください。Java にはチェック例外と非チェック例外があります。それらを使用してください。
- プログラミングエラーや予期しない失敗には、
RuntimeExceptionを継承する非チェック例外を使用します。 - チェック例外は、呼び出し元が本当に失敗に対処する必要があり、妥当な復旧手段がある場合に限り、慎重に使用します。
- 例外を**絶対に握りつぶさないでください。**必ずログに記録するか、再スローします。
- 最上位のエラー境界(コントローラーアドバイス、グローバルハンドラーなど)を除き、
ExceptionやThrowableを汎用的に絶対にキャッチしないでください。 - 具体的な例外型を使用します。標準の例外型が適さない場合は、カスタムのドメイン例外を作成します。
- キャッチブロックの重複を避けるため、マルチキャッチを使用します。
// 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;
}
}
命名規則
- クラス:
PascalCase— 名詞(UserService、PaymentProcessor)。 - メソッド:
camelCase— 動詞(findByEmail、processPayment、calculateTotal)。 - 変数:
camelCase— 名詞(userId、orderItems)。 - 定数:
UPPER_SNAKE_CASE(MAX_RETRY_COUNT、DEFAULT_TIMEOUT)。 - パッケージ:小文字、ドット区切り(
com.company.project.domain.user)。 - ブール値:
is、has、should、canを接頭辞として付けます(isValid()、hasPermission()、shouldRetry())。 - コレクション:複数形の名前(
users、orders、products)。 - 公開 API では略語を使いません。
userRepoではなくuserRepository、txMgrではなくtransactionManagerとします。 - メソッドに予想外の副作用を持たせないでください。名前は、そのメソッドの処理内容を正確に表す必要があります。
コードスタイルとフォーマット
- プロジェクト全体で一貫したスタイルを1つ使用します。標準 Java コーディング規約か IntelliJ のデフォルト設定のいずれかを選びます。どちらを選ぶかは重要ではありません。一貫性が重要です。
- クラスは、デフォルトでパッケージプライベートにします。必要なものだけを
publicとして公開します。 - 不変性を意図する変数には
finalを適用します。 - 可能な場合はラムダよりメソッド参照を使用します。
u -> u.userId()ではなくUser::userIdとします。 - インポートは、標準ライブラリ、サードパーティ、内部パッケージの順に整理します。ワイルドカードインポートは使用しません。
- プロジェクトに Spotless が含まれている場合、コードを変更するたびに(編集したファイルのみ)フォーマッターを実行します。Gradle の場合:
./gradlew spotlessApply。Maven の場合:mvn spotless:apply。
ストリームとコレクション
コレクションの変換には Java Streams を使用します。filter()、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());
}
}
中間操作(map、filter、flatMapなど)で副作用を発生させないでください。中間操作は純粋関数にします。副作用を実行する場所としては、終端操作のforEachが適切です。中間操作が必要ない場合は、stream().forEach()よりIterable.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);
}
テスト
一貫したテスト命名
プロジェクト全体で1つの命名規則を選び、徹底します。推奨形式:
methodName_shouldExpectedBehavior_whenCondition
例:
@Test
void findByEmail_shouldReturnUser_whenUserExists() { ... }
@Test
void findByEmail_shouldThrowNotFoundException_whenUserDoesNotExist() { ... }
@Test
void processPayment_shouldReturnSuccess_whenBalanceSufficient() { ... }
同じプロジェクトでcamelCase、snake_case、given/when/then、shouldのスタイルを混在させないでください。特定の規則より一貫性が重要です。
テスト構成
- デフォルトのテストフレームワークとしてJUnit 5を使用します。
- Spock Framework(Groovy ベース)は、チームが BDD スタイルの仕様を好む場合に代替として使用できます。強力ですが、Groovy の習熟が必要です。
- Arrange-Act-Assert(Spock では Given-When-Then)に従います。
- 各テストでは1つの動作をテストします。関連のない動作を対象とする複数のアサーションを含むテストは避けます。
@DisplayNameは必要な場合のみ使用します。適切な名前のテストメソッドは、それ自体で内容が明確です。
TDD
テスト駆動開発では、より優れた API を設計できます。最初にテストを書くことで、実装前に公開インターフェースを設計せざるを得なくなります。厳密な TDD を実践しない場合でも、メソッドのシグネチャを書いたらすぐに、実装本体を書く前にテストを書いてください。
テストの独立性
テストは独立しており、繰り返し実行できるものでなければなりません。テスト間で変更可能な状態を共有してはいけません。実行順序への依存も避けてください。
並行処理
仮想スレッド(Project Loom)
I/O バウンドの並行処理には仮想スレッドを使用してください。ほとんどのユースケースで、リアクティブフレームワークが不要になります。
// 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();
不変性によって安全な並行処理を実現する
スコープを限定した小さなメソッド内で、小さく不変なオブジェクトを使う適切な OOP を実践することが、最善の並行処理戦略です。小さなオブジェクトはすぐに作成され、ガベージコレクションされるため、競合が軽減されます。
構造化並行性(Java 25)
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());
使用してはいけないもの
- RxJava は使用しないでください。 複雑すぎてデバッグが難しく、仮想スレッドがあれば不要です。RxJava を使用しているプロジェクトに参加する場合も、これ以上導入しないでください。
- 仮想スレッドを使って同じロジックを逐次的に表現できる場合は、
CompletableFutureのチェーンを避けてください。 - リアクティブなパターンを時期尚早に導入しないでください。バックプレッシャーが必要であることを確認できた場合にのみ使用してください。
モダンな I/O
Files の便利なメソッドと Path.of() ファクトリーを使用してください。単純な操作では、BufferedReader/Writer の定型コードを避けてください。
// 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");
HTTP 呼び出しには組み込みの HttpClient を使用してください。HttpURLConnection の定型コードを追加しないでください。
// 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.time(Java 8 以降)を使用してください。Date、Calendar、SimpleDateFormat は使用しないでください。
// 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 months
ロギング
- SLF4J(
org.slf4j.Logger)を使用してください。System.out.println()やSystem.err.println()は使用しないでください。 - パラメーター化ロギングを使用してください。文字列連結は避けてください。
// CORRECT
log.info("Processing order {} for user {}", orderId, userId);
// WRONG
log.info("Processing order " + orderId + " for user " + userId);
- ログレベル:
error— 対応が必要な例外や障害。warn— 復旧可能な問題や、動作の劣化。info— 重要なビジネスイベント(注文の確定、支払いの処理など)。debug— トラブルシューティング用の詳細な技術情報。trace— 非常に詳細な情報。通常、本番環境では無効にします。
プロジェクト構成
- パッケージ構成では、ドメインコードとインフラストラクチャコードを分離してください。
- 設定クラスは専用の
configパッケージに配置してください。 - 一貫したパッケージ命名に従ってください:
com.company.project.<domain>.<layer>(例:com.acme.shop.order.repository)。 - 1 ファイルにつき公開クラスは 1 つにしてください。密接に関連する内部クラスを除き、複数クラスを 1 つのファイルにまとめないでください。
JVM と GC
- 問題を計測して確認するまでは、GC パラメーターを調整しないでください。 デフォルトの GC(最新の JVM では G1GC)は、ほとんどのアプリケーションで十分に優れています。
- 小さく短命なオブジェクトを使う適切な OOP を実践するほうが、どのような GC チューニングよりも効果的です。
- 最適化の前にプロファイリングしてください。パフォーマンス分析には JFR(Java Flight Recorder)と JMC(Java Mission Control)を使用してください。
非推奨のパターン — 使用しないでください
これらのパターンは古く、新しいコードでは使用しないでください。
| 非推奨のパターン | 最新の置き換え |
|---|---|
データクラスでの Lombok @Value、@Data、@Getter、@Setter | record |
Arrays.asList() | List.of() |
new ArrayList<>(Arrays.asList(...)) | List.of() または new ArrayList<>(List.of(...)) |
Collections.unmodifiableList() | List.copyOf() または List.of() |
| 単純な I/O での RxJava / Project Reactor | 仮想スレッド |
break を使う古い switch 文 | switch 式 |
手動の instanceof + キャスト | パターンマッチングを使った instanceof |
複数行文字列での + による文字列連結 | テキストブロック |
| 関数型インターフェースでの匿名内部クラス | ラムダ式 / メソッド参照 |
単純なケースでの synchronized ブロック | java.util.concurrent のロックまたは仮想スレッド |
メソッドからの null の返却 | Optional または例外のスロー |
System.out.println() | SLF4J ロギング |
生の型(List<String> ではなく List) | 常にジェネリクスを使用 |
Date、Calendar | java.time API(LocalDate、Instant など) |
StringBuffer | StringBuilder(またはテキストブロック / formatted()) |
str.trim().isEmpty() | str.isBlank() |
String.format(...) | "...".formatted(...) |
list.get(list.size() - 1) | list.getLast() |
Collections.reverse(new ArrayList<>(list)) | list.reversed() |
catch (Exception ignored) | catch (Exception _) |
重複する catch ブロック | マルチキャッチ catch (A | B e) |
ファイル読み込みでの BufferedReader + ループ | Files.readString(Path.of(...)) |
Paths.get(...) | Path.of(...) |
HttpURLConnection | HttpClient(Java 11) |
| 構造化タスクでの手動のエグゼキューターライフサイクル管理 | StructuredTaskScope(Java 25) |