Javaのコード例を検索すると、public static void main(String[] args) から始まる入門用のサンプルと、実務での書き分けを扱う解説が別々に出てきます。この記事では両方を1本にまとめ、最初の1本を動かすところから、見た目が同じでも結果が変わりやすい文字列比較・ループ・コレクションの書き方まで、実行結果つきで並べました。
実行結果を示したコード例は、Eclipse Temurin 25.0.4.1(LTS)とJava 27(Zulu 27)で実行して確かめています。2026年10月時点の最新版はJava 27(2026年9月15日リリース)ですが、27で正式化した言語機能はないため、どちらでも結果は同じです。構文ごとに使えるバージョンを書いているので、Java 8や17の現場でもそのまま判断に使えます。
まとめ
- Java 25以降は、クラス宣言を省いた
void main() { IO.println("Hello"); }をjava Hello.javaで実行できます。Java 24以前の環境では従来のpublic static void main(String[] args)で書きます(21〜24はプレビュー機能としてのみ存在)。 ScannerのnextInt()の直後にnextLine()を呼ぶと空文字が返ります。数値もnextLine()で1行読み、Integer.parseIntで変換します。- 文字列の比較は
==ではなくequalsを使います。null対策はObjects.equalsか、リテラルを左に置く書き方です。 - 拡張forの途中で要素を削除すると
ConcurrentModificationExceptionになります。ArrayListでは、末尾から2番目を消したときだけ例外が出ず、最後の要素が読み飛ばされます。条件で消すならremoveIfを使います。 Arrays.asListが返すのは固定長のリストで、不変ではありません。変更不可を保証したいならList.of(Java 9以降)です。- 命名とインデントはGoogle Java Style Guideに寄せ、フォーマッタの設定で固定すると、整形の差分がレビューに出なくなります。
Hello Worldのコード例と実行のしかた
Javaのプログラムは、ソースファイル(.java)をコンパイルしてクラスファイル(.class)にし、JVMで実行します。Java 25で書き方が大きく変わったため、従来の形と新しい形を両方示します。
従来の書き方:classとmainメソッド
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Hello.java という名前で保存し、javac Hello.java でコンパイルしてから java Hello で実行すると Hello, World! と表示されます。publicクラスの名前とファイル名が違うと、javacは class Hello is public, should be declared in a file named Hello.java というエラーで止まります。
Java 11以降は、コンパイルを省いて java Hello.java とソースファイルを直接渡しても動きます(JEP 330)。Java 22以降はこの実行方法で、同じディレクトリにある別のソースファイルのクラスも読み込まれます(JEP 458)。動作確認だけならビルドツールを用意する必要はありません。
Java 25のコンパクトソースファイル:void mainとIO.println
void main() {
IO.println("Hello, World!");
}
Java 25で正式化されたJEP 512により、クラス宣言・public static・String[] args を省いた「コンパクトソースファイル」が書けるようになりました。IO は java.lang パッケージのクラスなので import は要りません。java Hello.java で実行できます。
コンパクトソースファイルでは java.base モジュールが自動で import されます。List や ArrayList、LocalDate も import 文なしで使えます。
void main() {
var names = new ArrayList<>(List.of("sato", "suzuki"));
names.add("takahashi");
IO.println(names.size() + " " + LocalDate.of(2026, 10, 25).getDayOfWeek());
}
// 出力: 3 SUNDAY
注意したいのは、プレビュー段階(Java 23・24)の解説に残っている println("Hello") だけの書き方です。当時は java.io.IO のメソッドが自動でimportされていました(JEP 477)。JEP 512は正式化にあたって「IOクラスのstaticメソッドはコンパクトソースファイルに暗黙にimportされなくなった」と明記しており、Java 25では cannot find symbol のコンパイルエラーになります。IO.println とクラス名から書いてください。
従来形とコンパクト形の使い分け
コンパクト形が向くのは、学習用のコードと、1ファイルで完結する使い捨てのスクリプトです。チームで保守するコードは従来形で書きます。JEP 512の定義では、コンパクトソースファイルのクラスは無名パッケージに置かれるfinalクラスで、ほかのクラスから名前で参照できません。ライブラリやアプリケーションの部品にはできない作りです。
本番環境がJava 24以前なら、コンパクト形は使えません(21〜24ではプレビュー機能扱いです)。javac --release 21 でコンパイルすると implicitly declared classes are not supported in -source 21 と表示されます(日本語環境では「暗黙的に宣言されたクラスは-source 21でサポートされていません」)。開発環境の用意から始める場合は、IntelliJ IDEAでJava開発を始める手順|インストール・JDK設定・日本語化【2026.2対応】か、VS CodeのJava拡張「Extension Pack for Java」の使い方|自動ビルドの停止と6拡張の構成・JDK設定【2026年10月】を参照してください。
変数・メソッド・条件分岐のコード例
変数の型とメソッドの定義
public class Basics {
static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
int count = 3;
String label = "合計";
var total = add(count, 4); // var はJava 10以降。型は int になる
System.out.println(label + ": " + total); // 合計: 7
System.out.println(total / 2 + " " + total / 2.0); // 3 3.5
}
}
メソッドは「戻り値の型 名前(引数の型 引数名)」の順で宣言します。var は右辺から型を推論する書き方で(JEP 286)、型が無くなるわけではありません。入門の例で見落とされやすいのが割り算です。int同士の 7 / 2 は小数点以下が切り捨てられて3になり、片方を 2.0 にすると3.5になります。
条件分岐の基本形(Java 21以降で動く例)
条件分岐は、Java 14のswitch式とJava 16以降のパターンマッチで書き方が変わった分野です。
public class Cond {
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
static String grade(int score) {
return switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
default -> "D";
};
}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square(double side) -> side * side;
};
}
public static void main(String[] args) {
System.out.println(grade(95) + grade(80) + grade(42)); // ABD
Object o = "hello";
if (o instanceof String str && !str.isEmpty()) {
System.out.println(str.length()); // 5
}
System.out.printf("%.2f %.1f%n", area(new Circle(1)), area(new Square(3))); // 3.14 9.0
}
}
switch式で値を返す(Java 14)
矢印 -> のswitchは、break の書き忘れで次のcaseへ処理が流れ込む事故が起きません。switch全体が値を返す式になるので、変数への代入やreturnにそのまま使えます(JEP 361)。Java 8〜13では従来の case 9: ... break; で書きます。
instanceofとswitchのパターンマッチ(Java 16/21)
o instanceof String str は、型の判定とキャストを1回で済ませる書き方です(JEP 394、Java 16)。&& の右側では変数 str をそのまま使えます。
switchで型ごとに分岐するのはJava 21以降です(JEP 441)。case Square(double side) のようにrecordの成分を取り出す書き方(recordパターン)も同じJava 21で正式化しました(JEP 440)。sealed で実装クラスを限定しておくと default が要りません。後から Triangle を permits に足してcaseを書き忘れると、the switch expression does not cover all possible input values というコンパイルエラーで漏れが見つかります。型が増える分岐は、if-elseの連鎖ではなくこの形で書くべきです。sealedとrecordの設計上の意味はオブジェクト指向とは?三本柱の実装手順とrecord・sealedまでJavaコードで解説で扱っています。
キーボード入力のコード例:ScannerとIO.readln
入門書の定番は Scanner ですが、数値と文字列を交互に読むと、次に示す空文字の問題が起きます。Java 25からは IO.readln も使えます。
nextIntの直後のnextLineが空になる理由
import java.util.Scanner;
public class Input {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("年齢: ");
int age = sc.nextInt();
System.out.print("名前: ");
String name = sc.nextLine();
System.out.println("[" + name + "] " + age);
}
}
// 「30」「田中」と入力すると 出力: [] 30
nextInt() は数字の部分だけを読み、行末の改行を入力に残します。Scannerのjavadocは nextLine() を「現在の行の残りを返す」と説明しているため、残った改行までの空文字がそのまま返ります。「田中」は読まれません。数字以外を入力すると InputMismatchException で落ちる点も、入門のコード例ではよく省かれます。
直し方は、すべて nextLine() で1行ずつ読み、数値は自分で変換することです。
int age = Integer.parseInt(sc.nextLine().trim());
String name = sc.nextLine();
// 出力: [田中] 30
Java 25のIO.readlnと、Scannerと混ぜてはいけない理由
void main() {
String name = IO.readln("名前: ");
int age = Integer.parseInt(IO.readln("年齢: ").trim());
IO.println(name + "さんは" + age + "歳です");
}
// 出力: 田中さんは30歳です
IO.readln(String) はプロンプトの表示と1行の読み込みをまとめて行います。IOクラスのjavadocによると、入力の終わりに達して1文字も読めなかったときの戻り値は null です。パイプやファイルから入力を流し込む使い方では、null判定を入れてから変換してください。
同じjavadocは、readlnだけで標準入力を読み、ほかの読み方と混ぜない使い方を想定していると書いています。パイプで2行をまとめて流し込んだ状態で、Scanner で1行読んだあとに IO.readln() を呼ぶと、2行目が残っていても null が返りました。Scannerが届いている入力を先読みしてバッファに取り込むためです。2行目を3秒遅らせて送ると読めたので、端末で1行ずつ打つと症状が出ないこともあります。readlnを呼んだ後に System.in を別の方法で使ったときの動作は「規定しない」とも書かれています。1つのプログラムではどちらか一方に揃えます。
文字列比較のコード例:==とequalsの違い
文字列の比較は、見た目が同じでも結果が変わるため誤りが入りやすい箇所です。結論から言えば、文字列の内容を比べるときは常に equals を使います。
==が参照比較になる理由と文字列リテラルのプール
String a = "example";
String b = "example";
String c = new String("example");
System.out.println(a == b); // true(同じリテラルなので同一参照)
System.out.println(a == c); // false(別インスタンス)
System.out.println(a.equals(c)); // true(内容の比較)
== はオブジェクトの参照、つまり同じインスタンスかどうかを比べます。同じ文字列リテラルはJVMの中で1つのインスタンスにまとめられるため、a == b はtrueになります。この挙動に頼ったコードは、入力値やDBから読んだ文字列と比べた瞬間にfalseになります。テストでは通り、本番だけで失敗する典型例です。文字列を組み立てる側の書き方はJavaのStringBuilderとは:使い方とStringBufferとの違い、+連結との使い分けをコードで解説で扱っています。
NullPointerExceptionを避ける2つの書き方
String input = request.getParameter("mode");
String expected = "example";
// 1: 定数を左に置く。input が null でも false を返す
if ("example".equals(input)) {
System.out.println("match");
}
// 2: どちらが null でもよい場合。両方 null なら true
if (Objects.equals(input, expected)) {
System.out.println("equal");
}
input.equals("example") と書くと、inputがnullのときに NullPointerException が出ます。比較相手が定数なら1、両方が変数なら Objects.equals(Java 7以降)を使います。大文字と小文字を区別しない比較は equalsIgnoreCase です。
longとLongで結果が変わる境界
Long x = 127L;
Long y = 127L;
System.out.println(x == y); // true(仕様上 a == b が保証される範囲)
Long p = 128L;
Long q = 128L;
System.out.println(p == q); // false(OpenJDKの既定。仕様は同一性を規定していない)
System.out.println(p.equals(q)); // true
System.out.println(p.longValue() == q.longValue()); // true
Java言語仕様5.1.7は、ボクシングされる値が-128〜127の整数定数式なら a == b が常にtrueになると定めています。範囲外については同一性を一切仮定してはならないとしており、128でfalseになるのはOpenJDKの実装による結果です。境界の数値を覚えるのではなく、ラッパー型は equals か、プリミティブに戻してから比べてください。
ループのコード例:拡張for・インデックスfor・Streamの使い分け
反復の書き方は3通りあります。既定は拡張forで、インデックスが必要なときだけ通常のforに戻し、集計や変換はStreamで書きます。
拡張forの書き方とコロンの意味
List<String> names = List.of("sato", "suzuki", "takahashi");
for (String name : names) {
System.out.println(name);
}
String[] codes = {"A", "B", "C"};
for (String code : codes) {
System.out.println(code);
}
拡張forのコロンは「右側のコレクションまたは配列から、要素を1つずつ左側の変数に取り出す」という意味です(Java 5以降)。配列にも Iterable を実装したコレクションにも使え、範囲外アクセスの心配がありません。
配列・List・Mapそれぞれの反復と検索
// names は前の例の List.of("sato", "suzuki", "takahashi")
// 配列の最大値
int[] scores = {58, 91, 74};
int max = Arrays.stream(scores).max().getAsInt(); // 91。引数なしの orElseThrow() はJava 10以降
// List から条件に合う最初の要素を探す
Optional<String> found = names.stream()
.filter(n -> n.startsWith("s"))
.findFirst();
// Map の反復は entrySet
Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 12);
stock.put("banana", 5);
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
Mapを keySet で回して get(key) で値を取る書き方は、キーごとに探索をもう1回行います。キーと値の両方が要るなら entrySet です。重複を除いた集合や順序つきの集合が要る場面は、【Java】TreeSetとHashSetの違いと使い分け|順序・速度・null・範囲検索を実行結果で解説で比べています。
ループ中の削除が例外になる条件
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// 例外になる書き方
for (String s : list) {
if ("a".equals(s)) {
list.remove(s);
}
}
// 条件で削除するなら removeIf(Java 8以降)
list.removeIf(s -> "a".equals(s));
拡張forはイテレータで回っているため、ループ中に list.remove でリストを変更すると、次の反復で ConcurrentModificationException が出ます。例外になるかどうかは、どの要素を消したかで変わります。上の例の "a" を末尾から2番目の "b" に変えると例外は出ず、"c" が一度も処理されないまま [a, c] でループが終わります。ArrayListのイテレータが「残りがあるか」を要素数との比較だけで判断するためです。エラーにならない分、こちらのほうが発見しにくい不具合になります。
インデックスforを残すべき場面
次の4つの場面では、拡張forやStreamより for (int i = 0; i < n; i++) のほうが素直に書けます。
- 隣り合う要素を比べる(i番目とi+1番目)
- 逆順に走査する
- 2つの配列を同じ添字で並行に読む
- 反復の途中で添字を飛ばす・戻す
コレクション初期化のコード例:Arrays.asList・List.of・Stream.toList
初期値つきのリストを作る方法は複数あり、返ってくるリストの性質がそれぞれ違います。
Arrays.asListは不変ではなく固定長
String[] source = {"a", "b", "c"};
List<String> fixed = Arrays.asList(source);
fixed.set(0, "z"); // 通る
System.out.println(source[0]); // z(元の配列にも書き戻る)
fixed.add("d"); // UnsupportedOperationException
fixed.remove("b"); // UnsupportedOperationException
// 変更可能なリストが欲しい場合
List<String> mutable = new ArrayList<>(Arrays.asList("a", "b", "c"));
Arrays.asListのjavadocは、戻り値を「指定された配列に連動する固定サイズのリスト」と説明しています。要素の置き換えはでき、その変更は元の配列にも反映されます。サイズを変える操作だけが例外になります。「不変リストを作るメソッド」という説明は誤りです。
List.of(Java 9以降)とcopyOfの保証
List<String> immutable = List.of("a", "b", "c");
Map<String, Integer> config = Map.of("retry", 3, "timeout", 5000);
Set<String> roles = Set.of("admin", "editor");
immutable.set(0, "z"); // UnsupportedOperationException
List.of("a", null); // NullPointerException
List.of と List.copyOf が返すリストは、要素の追加・削除・置き換えがすべて例外になり、null要素も受け付けません。メソッドの引数で受け取ったリストを外から書き換えられたくない場合は、フィールドに入れる前に List.copyOf で写しておくと安全です。
Stream.toListとCollectors.toUnmodifiableListの違い
// names は前の例の List.of("sato", "suzuki", "takahashi")
// Java 16以降。null要素はそのまま残る
List<String> a = names.stream().filter(n -> n.length() > 4).toList();
// Java 10以降。null要素があれば NullPointerException
List<String> b = names.stream()
.collect(Collectors.toUnmodifiableList());
どちらも変更できないリストを返しますが、nullの扱いが違います。Arrays.asList("abcde", null) から作ったStreamで試すと、toList() は [abcde, null] を返し、Collectors.toUnmodifiableList() は NullPointerException を投げました。nullを混ぜたくない処理は後者でエラーにしたほうが、不具合の発生箇所に近いところで止まります。
Stream APIのコード例:フィルタ・変換・集計
Stream APIはJava 8で導入されました。forループで書いていた絞り込み・変換・集計を、処理の意図が読める形で書けます。
filter・mapと終端操作の基本形
List<String> fruits = List.of("apple", "banana", "cherry", "date");
List<String> longNames = fruits.stream()
.filter(s -> s.length() > 5)
.map(String::toUpperCase)
.toList(); // Java 16以降。Java 8〜15は collect(Collectors.toList())
System.out.println(longNames); // [BANANA, CHERRY]
Map<Integer, List<String>> byLength = fruits.stream()
.collect(Collectors.groupingBy(String::length));
// {4=[date], 5=[apple], 6=[banana, cherry]}
String::toUpperCase の :: はメソッド参照で、s -> s.toUpperCase() と同じ意味です。Streamは終端操作(toList・collect・findFirst など)を呼ぶまで何も実行されません。途中の filter や map だけを並べても処理は走らない点に注意してください。
平均値・合計・最大値の1行集計
List<Integer> scores = List.of(58, 91, 74);
double average = scores.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
IntSummaryStatistics stats = scores.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println(stats.getMax() + " " + stats.getSum() + " " + stats.getAverage());
// 91 223 74.33333333333333
平均・合計・最大を別々に求めるなら summaryStatistics() で1回にまとめられます。average() は空のStreamで値を持たない OptionalDouble を返すため、orElse で既定値を決めておきます。
クラス設計のコード例:record・コンストラクタインジェクション・委譲
メソッド単位の書き方の次に差が出るのが、クラスの組み立て方です。値の入れ物、依存の受け取り方、機能の再利用の3点を示します。
値を運ぶクラスはrecordで書く(Java 16以降)
record Point(int x, int y) {}
void main() {
var p = new Point(1, 2);
IO.println(p + " " + p.equals(new Point(1, 2)) + " " + p.x());
}
// 出力: Point[x=1, y=2] true 1
1行の宣言で、コンストラクタ・アクセサ・equals・hashCode・toString が生成されます(JEP 395)。フィールドはfinalなので、作った後に書き換えられません。getterとsetterを手で並べたクラスより、値の受け渡しで起きる不具合が減ります。コンストラクタでの検証や継承できない理由はJavaのレコードクラス(record)とは?使い方・コンストラクタ・継承できない理由をJava 25で実測で扱っています。
コンストラクタインジェクションを既定にする理由
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void cancel(long orderId) {
repository.markCancelled(orderId);
}
}
依存をコンストラクタで受け取ると、フィールドをfinalにでき、生成した時点で必要な部品が揃っていることが保証されます。Spring Frameworkのリファレンスは、Beanのコンストラクタが1つだけなら @Autowired の注釈は不要と明記しています。フィールドに @Autowired を付ける書き方は、new で組み立てるときに依存を渡す口がなく、テストではMockitoのリフレクション注入かDIコンテナに頼ることになるので避けます。DIの仕組みそのものはSpringのDI(依存性注入)とは?仕組みと注入方法を動くコードでわかりやすく解説を参照してください。
JUnit 5・6(Jupiter)とMockitoでの差し替え
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository repository;
@InjectMocks
private OrderService service;
@Test
void cancelMarksOrderCancelled() {
service.cancel(1L);
Mockito.verify(repository).markCancelled(1L);
}
}
JUnit 5以降(2025年9月に出たJUnit 6も同じJupiter API)で使うのは @ExtendWith(MockitoExtension.class) です。@RunWith(MockitoJUnitRunner.class) はJUnit 4用で、Jupiterのテストに付けても無視されます。@Mock と @InjectMocks が初期化されず、テストは NullPointerException で失敗します。テストの書き方全般はJUnitとは?Javaの単体テストフレームワークの基本と書き方【2026年版】にまとめています。
継承ではなく委譲を選ぶ判断基準
ログ出力を使いたいだけで PaymentService extends AuditLogger と書くと、決済サービスが「監査ロガーの一種」という意味になり、親クラスの変更がすべての子に波及します。「AはBの一種か」に自然に「はい」と答えられないなら、private final AuditLogger logger; をフィールドに持ち、上のOrderServiceと同じくコンストラクタで受け取る委譲を選びます。
命名規則とインデントのコード例
命名と整形は好みの問題に見えますが、大半は言語仕様と広く使われるスタイルガイドで決まっています。読みやすさの原則そのもの(関数の長さ・早期return・責務の分け方)は良いコードの書き方|可読性・保守性を高める12の原則とJavaの実例で扱っているので、ここではJava固有の規則に絞ります。
パッケージ名は逆ドメインの小文字
package com.example.billing.invoice;
Java言語仕様6.1は、一意なパッケージ名をインターネットのドメイン名を逆順にした接頭辞で作り、第1要素は全小文字のASCII文字で書くという慣例を示しています。Google Java Style Guideの5.2.1も、パッケージ名は小文字と数字だけでアンダースコアを使わず、複数の語はそのまま連結すると規定しています。ドメインを持たない社内システムでも、社名から逆ドメイン形式を決めておくと、後でライブラリとして切り出すときに名前が衝突しません。
クラス・メソッド・変数・定数の書き分け
| 対象 | 記法 | 例 |
|---|---|---|
| クラス・インターフェース・record | UpperCamelCase | OrderService |
| メソッド・変数 | lowerCamelCase | calculateTotal |
| 定数(static final) | 大文字とアンダースコア | MAX_RETRY_COUNT |
| パッケージ | 小文字と数字のみ | com.example.billing |
| 型変数 | 1文字または末尾T | T、E、RequestT |
変数名では、booleanを isShipped や hasChildren のような述語にすることと、単位を名前に含めること(timeout ではなく timeoutMillis)の2点で、値の取り違えを防げます。英単語の選び方や品詞の揃え方は命名規則を英語で書く方法|ケース・品詞・対義語と公式ガイドの規定を参照してください。
インデント・桁数・コメントの決め方
Google Java Style Guideは4.2でブロックが開くたびにインデントを2スペース増やすと定め、4.4で1行の上限を100文字としています。この記事のコード例は4スペースで書いていますが、重要なのは幅の値ではなくリポジトリ全体で揃っていることです。フォーマッタの設定ファイルをリポジトリに置き、ビルドで検証まで通しておけば、整形の議論がレビューに上がらなくなります。
// 決済APIが同一注文IDの二重送信を拒否するため、リトライ前に必ず状態を再取得する
Order latest = orderRepository.findById(order.getId());
コメントに書く価値があるのは、何をしているかではなく、その書き方を選んだ理由と外部仕様に由来する制約です。他チームが呼ぶ公開APIには引数・戻り値・例外をJavadocで書きます。タグの使い分けはJavadocの書き方|コメント・タグの記述例と生成方法を解説で整理しています。
バージョンで書き方が変わるJava構文の対応表
検索で見つかるJavaのコード例には、Java 8前提のものとJava 25前提のものが混在しています。自分の現場のバージョンで動くかを、次の表で確かめてください。
導入バージョン早見表
| 機能 | 使えるバージョン | 根拠 |
|---|---|---|
| 拡張for文・ジェネリクス | Java 5 | 言語仕様 |
| try-with-resources・Objects.equals | Java 7 | 言語仕様・javadoc |
| ラムダ式・Stream API・removeIf | Java 8 | 言語仕様・javadoc |
| List.of・Map.of・Set.of | Java 9 | javadoc |
| var・Collectors.toUnmodifiableList | Java 10 | JEP 286・javadoc |
| java Hello.java(ソースから直接実行) | Java 11 | JEP 330 |
| switch式 | Java 14 | JEP 361 |
| テキストブロック | Java 15 | JEP 378 |
| record・instanceofのパターンマッチ・Stream.toList | Java 16 | JEP 395・JEP 394・javadoc |
| sealedクラス | Java 17 | JEP 409 |
| switchのパターンマッチ・recordパターン・仮想スレッド | Java 21 | JEP 441・JEP 440・JEP 444 |
| 複数ファイルのソース直接実行 | Java 22 | JEP 458 |
| void main・IO.println・import module | Java 25 | JEP 512・JEP 511 |
Java 8で止まっている現場では、表の3行目までがそのまま使える範囲です。List.of の代わりは Collections.unmodifiableList(new ArrayList<>(...))、toList() の代わりは collect(Collectors.toList()) です。
LTSとサポート期限の選び方
長期サポート版(LTS)はJava 8、11、17、21、25で、2026年10月時点の最新LTSは25、最新の機能リリースは27です。次のLTSは2027年9月予定のJava 29です。OracleのJava SEサポートロードマップでは、Premier Supportの期限がJava 17は2026年9月(終了済み)、21は2028年9月、25は2030年9月、非LTSの27は2027年3月です。この期限はOracleの有償サポートのもので、Temurinなど配布元ごとの更新期間は別に確認が要ります。
新しく書くコードはJava 25を基準にしてください。17を使っている現場は、Oracleの延長サポート(2029年9月まで)を契約していない限り、移行の計画を立てる時期に入っています。Adoptiumのサポート表でも、Temurin 17の更新が約束されているのは2027年10月までです。版ごとの差分はJava 21と25の違い|正式化した新機能・サポート期限・移行で止まる箇所を実機で比較、最新の27はJava 27とは|非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響、Java 8からの段階的な移行はJava近代化の進め方|Java 8から25へ上げる手順と実機で落ちる箇所【2026年10月】、配布元の選び方はOpenJDKは商用利用できる?無料の範囲とOracle JDKとの違い・ライセンス・サポート期限【2026年10月】で扱っています。古いJavaのまま動いている業務システムを作り直す場合は、一創のフルスクラッチ開発でも相談を受けています。
避けたいコード例と、その理由
検索で見つかるコード例に今も残っているものの、避けるべき書き方を2つ挙げます。getterとsetterを全フィールドに並べる書き方の代わりは、前述のrecordです。
ループ1回の計測で速さを主張するコード
// 1回きりの計測。JITコンパイル前の実行と後の実行が区別できない
long start = System.nanoTime();
for (Map.Entry<String, Integer> e : stock.entrySet()) {
total += e.getValue();
}
System.out.println(System.nanoTime() - start);
JVMは実行中にコードを機械語へコンパイルし直します。Temurin 25の既定ではメソッドが200回呼ばれるとC1、5000回でC2コンパイルの対象になるため(-XX:+PrintFlagsFinal の Tier3InvocationThreshold=200・Tier4InvocationThreshold=5000)、最初の実行と後の実行では速さが大きく違います。計算結果を使わないループは最適化で消されることもあります。「この書き方のほうが速い」と書くなら、ウォームアップと複数回の測定を自動で行うJMH(Java Microbenchmark Harness)で測った数字を示してください。
例外を握りつぶすcatch
try {
service.cancel(orderId);
} catch (Exception e) {
// 何もしない
}
失敗した事実がログにも呼び出し元にも残らず、障害の調査が止まります。回復できる例外だけを具体的な型で捕まえ、それ以外はログを残して再送出するか、捕まえずに呼び出し元へ任せます。Exception を一括で捕まえるのは、プログラムの最上位で落ちないようにする箇所に限ります。
よくある質問
Javaで文字列を比較するときは==とequalsのどちらを使いますか?
equals です。== は同じインスタンスかどうかを比べるため、内容が同じでも別に作られた文字列ではfalseになります。nullの可能性がある変数は、"定数".equals(変数) か Objects.equals(a, b) で比べます。
Javaのコードを一番手軽に動かす方法は何ですか?
JDKをインストールし、Hello.java を保存して java Hello.java と実行する方法です。Java 11以降ならコンパイルの手順を省けます。Java 25以降なら、クラス宣言を省いた void main() { IO.println("Hello"); } だけのファイルでも動きます。
拡張for文のコロンはどういう意味ですか?
for (String s : list) のコロンは「listの要素を1つずつ取り出してsに入れる」という意味です。英語で読むと「for each s in list」にあたります。配列とIterableを実装したコレクションに使えます。
Arrays.asListで作ったリストは変更できますか?
要素の置き換え(set)はでき、元の配列にも反映されます。要素の追加と削除は UnsupportedOperationException になります。自由に変更したいなら new ArrayList<>(Arrays.asList(...)) で写し、変更させたくないなら List.of を使います。
今から書くならJava 8と最新版のどちらの書き方に合わせますか?
新規のコードはJava 25(現行のLTS)に合わせます。Java 8はOracleのPremier Supportが2022年3月に終わっており、record・switch式・List.of などが使えません。本番環境がJava 8のままなら、この記事の対応表で使える構文を確かめつつ、移行の計画を並行して立ててください。