- JAVA
- 技術 💻
Javaの罠8選|Integerは128から==がfalse
Javaの文法は一通り覚えた。クラスも書ける。Streamも使える。
そこから先で待っているのが、「動くのに、なぜか変」という段階です。原因はたいてい、言語の仕様の中にある小さな約束ごと。今回は、書けるようになった人が次にぶつかる8つ——Java歴1〜3年の人にこそ効くものを集めました。1つでも知らなかったら儲けものです。
① Integerは、127までは == が使えて、128から使えません
いちばん有名で、いちばん踏まれる話から。
Integer a = 127, b = 127;
System.out.println(a == b); // true
Integer c = 128, d = 128;
System.out.println(c == d); // false !?
Javaは-128〜127のIntegerをあらかじめ作り置きして使い回す(IntegerCache)ので、127までは同じオブジェクト、128からは毎回別のオブジェクトになります。
== はオブジェクトの同一性、equalsは値の等しさ。参照型の比較は必ずequals——この事故は、テストデータが小さいときだけ通ってしまうのが怖いんです。
② equalsを書いたら、hashCodeも書かないと消えます
自作クラスをHashMapのキーやHashSetの要素にするとき、equalsだけ実装していませんか。
HashMapは、まずhashCodeで置き場所を決め、その場所の中でequalsで探します。hashCodeが違えば、equalsが真でも別の場所を探しに行く——つまり「入れたはずのものが見つからない」が起きます。
equalsとhashCodeは、必ずセットで実装する。現代なら、⑧のrecordを使えば両方自動生成されるので、こちらの方が安全なんです。
③ finallyの中でreturnすると、例外が消えます
知らないと、障害調査で地獄を見る仕様です。
try {
throw new RuntimeException("大事なエラー");
} finally {
return "OK"; // 例外が握りつぶされ、"OK"が返る
}
finallyでreturnすると、tryやcatchで発生した例外は破棄され、正常終了したように見えます。ログにも残りません。
finallyは後始末専用、returnは書かない。IDEが警告を出す理由が、これなんです。
④ List.of() には、nullを入れられません
Java 9以降の便利なファクトリ、実は性格がはっきりしています。
| List.of() / Map.of() | Arrays.asList() | |
|---|---|---|
| 要素の追加・削除 | できない(例外) | できない(例外) |
| 要素の上書き(set) | できない(例外) | できる |
| nullを入れる | その時点でNullPointerException | 入れられる |
List.of()は完全に不変、Arrays.asList()は「サイズ固定だがsetはできる」という中途半端な存在です。
「不変にしたいのか」「後で変えたいのか」を、作るときに選ぶ。この意識があるだけで、意図しない変更事故が減ります。
⑤ Streamは、終端操作を呼ぶまで1ミリも動きません
Streamの中間操作(filter、map…)は、予約を積んでいるだけです。
list.stream().map(x -> {
System.out.println("処理中"); // 何も出ない
return x * 2;
});
// collect や forEach を呼んで、初めて動く
これは遅延評価という設計で、無限ストリームやショートサーキットを可能にする良い性質です。
もう1つ大事なのが、Streamは一度使ったら再利用できないこと。同じStream変数を2回回すと IllegalStateException。使い捨ての川、と覚えると事故りません。
⑥ ラムダの中で、チェック例外は投げられません
Streamで処理を書いていて、IOExceptionを投げるメソッドを呼んだ瞬間にコンパイルエラー——通過儀礼です。
理由は、Streamに渡す関数型インターフェース(Function等)が、チェック例外を投げる宣言を持っていないから。ラムダの中身は、そのインターフェースの契約に縛られます。
対策は、ラムダ内でcatchして非チェック例外に包み直すか、そういう処理はStreamの外で普通のforに戻すか。
「全部Streamで書く」より「例外が絡むなら素直にfor」の方が、読みやすいことも多いんです。
⑦ switchは、式になりました
Java 14以降、switchは「文」だけでなく「値を返す式」として書けます。
String label = switch (day) {
case SAT, SUN -> "休日";
case MON, TUE, WED, THU, FRI -> "平日";
};
アロー(->)形式なら、break忘れによるフォールスルー事故が構造的に起きません。さらに、enumやsealedと組み合わせると「全ケースを網羅していないとコンパイルエラー」になります。
「switchにbreakを書き忘れる」という20年来のバグが、書き方ひとつで消えるんです。
⑧ recordは、「データを運ぶだけ」のクラスを1行にします
最後は、書く量が減る話です。
record Point(int x, int y) {}
これだけで、コンストラクタ・アクセサ・equals・hashCode・toStringが自動生成され、フィールドは不変になります。②のhashCode問題も、④で触れた不変性も、recordなら最初から正しい。
ただし不変なのは「フィールドの付け替え」まで。Listのような可変オブジェクトを持たせると、その中身は変更できてしまうので、List.copyOf()で包むのが定石です。
DTOやAPIのレスポンスクラスにgetterを何十行も書いていた時代は、終わっています。「このクラスの仕事はデータを運ぶだけか」——YESなら、record一択なんです。
番外編:知っておくと得する小ネタ
enum.values() は毎回コピーを作ります
呼ぶたびに新しい配列を生成するので、ループ内で何度も呼ぶと無駄が出ます。頻繁に使うなら、一度取って保持しておくんです。
Java 21は、堅い業界でも現役です
長期サポート版のJava 21は、record・switch式・仮想スレッドが全部使えます。最新の長期サポート版はJava 25で、2026年9月にはJava 27も登場しました。堅い業界のプロジェクトでも、21以降は現役の選択肢です。
varは「型を書かない」だけで「型がない」わけではない
var x = 1; のxは、あくまでint。コンパイル時に型は確定しています。動的型付けとは別物なんですね。
RJCでも、Javaと向き合っています
RJCの研修はJavaから始まり(Lv.01〜03で環境構築・Java・オブジェクト指向)、実際の案件でも金融業界向けのJava 21プロジェクトなどで開発しています。
社内のJava探求グループでは、まさに今日のような「仕様の細部」を持ち寄って掘り下げています。こういう話を、Techチームで共有しながら開発しています。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ①と②です。比較とハッシュの約束は、コレクションを使う全コードに関わるので、今日のレビューから効くんですよね。
Q. 古いJavaのプロジェクトなので、⑦⑧が使えません…
A. その環境なら①〜⑥だけでも十分効きます。そして新しい構文を知っていること自体が、バージョンアップの提案材料になります。
Q. Streamとforは、どう使い分ければいいですか?
A. 変換・絞り込みが主ならStream、例外や複雑な分岐が絡むならfor(⑥)。「読んだ人が一番早く理解できる方」が正解なんですよね。
Q. 未経験からでも、ここまで分かるようになりますか?
A. なります。RJCの研修でも、Javaの基礎から4ヶ月でSpring Bootまで進みます。今日の8つは、その途中で必ず出会う話ばかりなんです。
「動く」と「正しい」の間に、仕様があります
今日の8つに共通していたのは、これでした。
Javaは動いてしまう言語です。==で比較しても、finallyでreturnしても、コンパイルは通る。違いが出るのは、境界値や例外時、つまり本番の一番嫌なタイミングです。
仕様を知っている人は、そこで慌てない。中級の壁は、文法ではなく約束ごとの理解なんですよね。
みなさんがJavaで踏んだ「仕様どおりだった事故」、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。