「実務でよくある罠」とは何か
ここでいう「罠」は、コンパイルは通るし一見正しく動いているように見えるのに、
本番や長期運用の中でじわじわ効いてきて、バグ・障害・保守地獄につながるポイントのことです。
初心者のうちは「文法的に正しいか」「動くか」に意識が向きがちですが、
実務では「長く運用しても壊れないか」「他の人が読んで理解できるか」「変更に耐えられるか」が重要になります。
そのギャップの中に、罠がたくさん潜んでいます。
罠1:null と Optional の中途半端な扱い
「とりあえず null 返しとけばいい」が後で爆発する
典型的なのが、戻り値として平気で null を返すメソッドです。
例えば、次のようなコードです。
User find(String id) {
if (existsInDb(id)) {
return loadFromDb(id);
}
return null;
}
Java呼び出し側はこう書きがちです。
User user = userService.find(id);
if (user.isActive()) { // ここで NullPointerException の可能性
...
}
Java「存在しないときは null を返す」という仕様が、
コード上はどこにも明示されていないのが問題です。
呼び出し側がそれを知らないと、簡単に NPE を踏みます。
本当に「ない」ことがあり得るなら、Optional を返す方が安全です。
Optional<User> find(String id) {
if (existsInDb(id)) {
return Optional.of(loadFromDb(id));
}
return Optional.empty();
}
Java呼び出し側は、値がない可能性を型で意識せざるを得ません。
userService.find(id)
.filter(User::isActive)
.ifPresent(this::process);
Java重要なのは、「null を返す/返さない」を場当たり的に決めないことです。
チームとして「戻り値に null は使わない」「Optional を使うのはここまで」といった方針を決めておかないと、
プロジェクト全体が「null 地雷原」になります。
罠2:equals と hashCode を正しく実装していない
コレクションに入れた瞬間におかしくなるパターン
Java では、HashSet や HashMap にオブジェクトを入れるとき、equals と hashCode の実装が非常に重要になります。
例えば、次のようなクラスがあります。
class User {
private String id;
private String name;
// getter / setter だけ。equals / hashCode 未実装
}
Javaこれを HashSet に入れて使うとします。
Set<User> users = new HashSet<>();
users.add(new User("u1", "Alice"));
users.add(new User("u1", "Alice")); // 同じ ID のつもり
Javaequals を実装していないと、
これは「別のオブジェクト」として扱われ、重複してしまいます。
さらに厄介なのは、「途中でフィールドを書き換える」パターンです。
User user = new User("u1", "Alice");
Set<User> users = new HashSet<>();
users.add(user);
user.setId("u2"); // ハッシュ値が変わる可能性がある変更
boolean contains = users.contains(user); // false になることがある
JavahashCode の計算に使っているフィールドを後から変えると、
ハッシュテーブルの中で「見つからないオブジェクト」になってしまいます。
実務では、
「コレクションに入れるオブジェクトはイミュータブルにする」
「equals / hashCode を必ずセットで実装する」
といったルールを決めておかないと、
原因不明のバグに延々と悩まされることになります。
罠3:ログと例外の扱いが雑すぎる
catch して握りつぶす
初心者がやりがちな悪い例がこれです。
try {
doSomething();
} catch (Exception e) {
e.printStackTrace();
}
Javaあるいは、もっとひどいとこうなります。
try {
doSomething();
} catch (Exception e) {
// 何もしない
}
Javaこれを本番でやるとどうなるか。
「何かがおかしい」「でもログには何も出ていない」「再現もしない」
という最悪の状況になります。
例外は「起きてほしくないことが起きた」サインです。
それを握りつぶすのは、
火災報知器の電池を抜くようなものです。
最低限、ログには残すべきです。
try {
doSomething();
} catch (Exception e) {
log.error("failed to doSomething", e);
throw e; // もしくはラップして再スロー
}
Java「どこで」「何が」「なぜ失敗したか」が分かるログは、
実務では命綱になります。
罠4:テストを書かずにリファクタリングする
「ちょっとメソッドを分けただけ」のつもりが…
レガシーコードをきれいにしたくて、
巨大メソッドを分割したり、クラスを整理したりするのは良いことです。
ただし、テストがない状態でやると、
「見た目はきれいになったけど、挙動が微妙に変わってバグになった」
ということが簡単に起こります。
例えば、こういうメソッドがあったとします。
int calculateFee(int price) {
int fee = price / 10;
if (fee < 100) {
fee = 100;
}
return fee;
}
Javaこれを「きれいにしよう」として、こう分割したとします。
int calculateFee(int price) {
int fee = calculateBaseFee(price);
return adjustMinimumFee(fee);
}
int calculateBaseFee(int price) {
return price / 10;
}
int adjustMinimumFee(int fee) {
if (fee <= 100) { // < を <= にしてしまった
return 100;
}
return fee;
}
Java見た目はきれいですが、price = 1000 のときの結果が変わってしまいました。
こういう「リファクタリング中の微妙な仕様変更」は、
テストがないとほぼ確実に見逃されます。
だからこそ、
「リファクタリングの前に、まずテストで今の挙動を捕まえる」
という順番が、実務では非常に重要です。
罠5:スレッドセーフでないものを平気で共有する
シングルトンや static フィールドに何でも突っ込む
例えば、次のようなコードです。
class Counter {
private int count = 0;
int next() {
return ++count;
}
}
static final Counter COUNTER = new Counter();
Javaこれを複数スレッドから呼ぶとどうなるか。++count は複数の操作の組み合わせなので、
同時に実行されると値が飛んだり、重複したりします。
実務では、
「とりあえず static にして共有しよう」
「とりあえずシングルトンにしよう」
という発想が、スレッドセーフティの罠を生みます。
対策としては、
状態を持つオブジェクトをむやみに共有しない、
必要なら AtomicInteger や synchronized を使う、
そもそもイミュータブルにする、
といった設計が必要です。
罠6:「とりあえず便利そうなライブラリ」を無制限に入れる
依存地獄とバージョン衝突
実務の Java プロジェクトでは、
Maven や Gradle で大量のライブラリを使います。
初心者がやりがちなのは、
「便利そうだから」「Qiita に書いてあったから」という理由で、
深く考えずにライブラリを追加していくことです。
結果として、
依存関係が雪だるま式に増え、
バージョン衝突やセキュリティリスクが発生します。
例えば、
A というライブラリが古い Jackson に依存していて、
B というライブラリが新しい Jackson に依存していると、
どちらかが中途半端な状態で動くことになります。
ライブラリ選定は、
「本当に必要か」「メンテされているか」「みんなが使っているか」
を冷静に見る必要があります。
これはもう、設計の一部です。
罠7:「動いたから OK」で終わらせる
実務では「たまたま動いている」は危険信号
初心者のうちは、
「コンパイルが通った」「テストで一回通った」「画面で動いた」
=「終わり」と思いがちです。
でも実務では、
「たまたまそのケースで動いただけ」
という状態が非常に多いです。
例えば、
境界値(0、空文字、最大値)、
異常系(null、例外、タイムアウト)、
並行実行、
長時間運用時のメモリリーク、
などは、軽く動かしただけでは見えてきません。
「動いたから OK」ではなく、
「どういう条件なら壊れるか」を少しでも想像してみる。
その視点を持てるかどうかが、
実務での成長の分かれ目になります。
まとめ:実務でよくある罠を自分の言葉で説明するなら
あなたの言葉で整理すると、こうなります。
「実務でよくある罠とは、
null の扱い、equals/hashCode、ログと例外、テストなしリファクタリング、
スレッドセーフでない共有、ライブラリ依存地獄、『動いたから OK』思考など、
コンパイルも通るし一見動くのに、
長期運用やチーム開発の中で大きな問題を生むポイントのこと。
これらの罠を避けるには、
“今動けばいい” ではなく、
“長く運用しても壊れないか”“他の人が読んで理解できるか”
という視点でコードを見ることが大事。」
