Java | Java 詳細・モダン文法:設計・実務視点 – 実務でよくある罠

Java Java
スポンサーリンク

「実務でよくある罠」とは何か

ここでいう「罠」は、コンパイルは通るし一見正しく動いているように見えるのに、
本番や長期運用の中でじわじわ効いてきて、バグ・障害・保守地獄につながるポイントのことです。

初心者のうちは「文法的に正しいか」「動くか」に意識が向きがちですが、
実務では「長く運用しても壊れないか」「他の人が読んで理解できるか」「変更に耐えられるか」が重要になります。
そのギャップの中に、罠がたくさん潜んでいます。


罠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 では、HashSetHashMap にオブジェクトを入れるとき、
equalshashCode の実装が非常に重要になります。

例えば、次のようなクラスがあります。

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 のつもり
Java

equals を実装していないと、
これは「別のオブジェクト」として扱われ、重複してしまいます。

さらに厄介なのは、「途中でフィールドを書き換える」パターンです。

User user = new User("u1", "Alice");
Set<User> users = new HashSet<>();
users.add(user);

user.setId("u2"); // ハッシュ値が変わる可能性がある変更

boolean contains = users.contains(user); // false になることがある
Java

hashCode の計算に使っているフィールドを後から変えると、
ハッシュテーブルの中で「見つからないオブジェクト」になってしまいます。

実務では、
「コレクションに入れるオブジェクトはイミュータブルにする」
「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 にして共有しよう」
「とりあえずシングルトンにしよう」
という発想が、スレッドセーフティの罠を生みます。

対策としては、
状態を持つオブジェクトをむやみに共有しない、
必要なら AtomicIntegersynchronized を使う、
そもそもイミュータブルにする、
といった設計が必要です。


罠6:「とりあえず便利そうなライブラリ」を無制限に入れる

依存地獄とバージョン衝突

実務の Java プロジェクトでは、
Maven や Gradle で大量のライブラリを使います。

初心者がやりがちなのは、
「便利そうだから」「Qiita に書いてあったから」という理由で、
深く考えずにライブラリを追加していくことです。

結果として、
依存関係が雪だるま式に増え、
バージョン衝突やセキュリティリスクが発生します。

例えば、
A というライブラリが古い Jackson に依存していて、
B というライブラリが新しい Jackson に依存していると、
どちらかが中途半端な状態で動くことになります。

ライブラリ選定は、
「本当に必要か」「メンテされているか」「みんなが使っているか」
を冷静に見る必要があります。
これはもう、設計の一部です。


罠7:「動いたから OK」で終わらせる

実務では「たまたま動いている」は危険信号

初心者のうちは、
「コンパイルが通った」「テストで一回通った」「画面で動いた」
=「終わり」と思いがちです。

でも実務では、
「たまたまそのケースで動いただけ」
という状態が非常に多いです。

例えば、
境界値(0、空文字、最大値)、
異常系(null、例外、タイムアウト)、
並行実行、
長時間運用時のメモリリーク、
などは、軽く動かしただけでは見えてきません。

「動いたから OK」ではなく、
「どういう条件なら壊れるか」を少しでも想像してみる。
その視点を持てるかどうかが、
実務での成長の分かれ目になります。


まとめ:実務でよくある罠を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「実務でよくある罠とは、
null の扱い、equals/hashCode、ログと例外、テストなしリファクタリング、
スレッドセーフでない共有、ライブラリ依存地獄、『動いたから OK』思考など、
コンパイルも通るし一見動くのに、
長期運用やチーム開発の中で大きな問題を生むポイントのこと。

これらの罠を避けるには、
“今動けばいい” ではなく、
“長く運用しても壊れないか”“他の人が読んで理解できるか”
という視点でコードを見ることが大事。」

タイトルとURLをコピーしました