Java 中級:コレクション・Stream・例外処理のよくある間違い

レッスンの本文で注意している点を、つまずきの側から168件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。

ArrayList を使う

レッスンを読む

import を忘れる

ArrayList を使うには import java.util.ArrayList; が必要です。これを忘れると cannot find symbol というエラーが出ます。IDE なら自動補完でやってくれますが、手書きの試験などでは注意しましょう

ArrayList を使うで詳しく読む

要素アクセスで list[0] と書いてしまう

ArrayList はクラスなので、配列の [] 記法は使えません。必ず list.get(0) のようにメソッド呼び出しで取り出します。逆に、取り出した値の型 (Integer) を int 変数に入れるときは int x = list.get(0); のように自動で変換されます (オートアンボクシング)

ArrayList を使うで詳しく読む

raw 型で書いてキャストが必要になる

ArrayList list = new ArrayList(); のようにジェネリクスを省くと、get(0) の戻り値は Object になります。Integer x = (Integer) list.get(0); のように明示的なキャストが必要になり、安全性も下がります。型は必ず <> で指定しましょう

ArrayList を使うで詳しく読む

List を走査する

レッスンを読む

list.size と書いてしまう

要素数は list.size() です。配列の arr.length はカッコなしのフィールドですが、ArrayListsize() は メソッド なので必ず () が必要です。list.size と書くとコンパイルエラーになります

List を走査するで詳しく読む

intInteger を取り違える

ArrayList<Integer> には基本型の int をそのまま入れているように見えますが、実はラッパクラス Integer に自動変換 (auto-boxing) されています。list.remove(5) と書くと「インデックス 5 番目を削除」、list.remove(Integer.valueOf(5)) と書くと「値が 5 の要素を削除」になり、結果がまったく違うので注意してください

List を走査するで詳しく読む

走査中に remove する

上で書いた ConcurrentModificationException の話です。拡張 for ループ中に list.remove(...) を呼ぶと例外が飛びます。安全に消すなら Iterator.remove() を使うか、別 List に集めてからまとめて削除します

List を走査するで詳しく読む

要素の削除と検索

レッスンを読む

remove(int)remove(Object) の混同

特に List<Integer> で頻発します。位置で消すのか、値で消すのかを意識し、値で消したい場合は Integer.valueOf(...) で包みましょう

要素の削除と検索で詳しく読む

null を渡したときの挙動

contains(null)remove(null) は実行時例外にはならず、null を要素として比較します。null を入れないリストを作りたいなら、追加前に if (value != null) でガードします

要素の削除と検索で詳しく読む

拡張 for 文の中で remove を呼ぶ

for (String s : list) { list.remove(s); } のように書くと ConcurrentModificationException で落ちます。走査中に消したいときは Iterator.remove を使うか、list.removeIf(s -> 条件) のような書き方に切り替えてください

要素の削除と検索で詳しく読む

HashMap の基本

レッスンを読む

null をキーに使ってしまう

HashMap は技術的には null キーを 1 つだけ許容しますが、可読性が落ちるうえ ConcurrentHashMap では NPE を投げます。実務では null キーは使わないと決めておきましょう

HashMap の基本で詳しく読む

auto-boxing を忘れて型エラー

Map<String, Integer>intput するときは自動で Integer に箱詰め (auto-boxing) されますが、逆に int x = map.get("apple") のように受け取るとき、もし null が返ると NPE になります。Integer x = ... で受けるか getOrDefault でデフォルト値を指定するのが安全です

HashMap の基本で詳しく読む

順序が保たれていると勘違いする

HashMapput した順番を保持しません。順番に処理したいときは LinkedHashMap を使うか、キーをリスト化してソートしてから回します

HashMap の基本で詳しく読む

Map を走査する

レッスンを読む

HashMap の順序を期待してしまう

put した順や key のソート順が出てくると思い込むのは典型的なバグです。順序が必要なら LinkedHashMapTreeMap を選ぶ

Map を走査するで詳しく読む

Map.Entrynull チェックしてしまう

for-each が回している Map.Entry 自体は null にはなりません。null チェックすべきは中身の value (e.getValue() == null) であって、Entry そのものではありません

Map を走査するで詳しく読む

map.entrySet() の戻り型を勘違いする

entrySet()Set<Map.Entry<K,V>> を返します。List でも Map でもなく Set なので、インデックスでアクセスはできません。あくまで「ペアの集合」をループするためのものだと覚えましょう

Map を走査するで詳しく読む

Set で重複を排除

レッスンを読む

HashSet の順序を期待してしまう

HashSet は要素の並びを保証しません。for で回したときの順番がプログラムを再起動したら変わる、ということも普通に起きます。順序が大事なら LinkedHashSetTreeSet を使いましょう

Set で重複を排除で詳しく読む

equals / hashCode の実装を忘れる

自作クラスを HashSet に入れるときは、equalshashCode を両方ともオーバーライドする必要があります。片方だけだと「同じはずの 2 つが別物扱いされる」事故が起きます

Set で重複を排除で詳しく読む

null の扱いを誤解する

HashSetLinkedHashSetnull を 1 個だけ入れられます。TreeSetnull を入れようとすると NullPointerException で落ちます。Set ならどれでも同じ、と思い込まないようにしましょう

Set で重複を排除で詳しく読む

Collections クラスの便利メソッド

レッスンを読む

Arrays.asList で返るリストは固定長

Arrays.asList(1, 2, 3) の戻り値は addremove を呼ぶと UnsupportedOperationException が飛びます。サイズを変えたいなら new ArrayList<>(Arrays.asList(...)) のように ArrayList で ラップ してください

Collections クラスの便利メソッドで詳しく読む

Arrays.asList(int[]) は要素 1 個のリストになる

int 配列を Arrays.asList(arr) に渡すと、List<int[]> (要素数 1 の、配列を 1 個だけ持つリスト) になってしまいます。Collections.sort も期待どおりに動きません。プリミティブ配列を List 化したいときは for ループか Arrays.stream(arr).boxed() を使う必要があります

Collections クラスの便利メソッドで詳しく読む

Collections.sortComparator なしで呼ぶ条件

Collections.sort(list) が引数 1 つで動くのは、要素が Comparable を実装している型 (Integer String Double など) のときだけです。自作クラスのリストを sort したい場合は、クラスに implements Comparable<MyClass> を書くか、Collections.sort(list, comparator)Comparator を渡す必要があります

Collections クラスの便利メソッドで詳しく読む

ジェネリッククラスを作る

レッスンを読む

raw 型で書いてしまう

Box box = new Box("hello"); のように <> を省略すると raw 型 扱いになり、getValue() の戻り値が Object になってしまいます。コンパイラは警告を出すだけで止めてはくれませんが、現代の Java では絶対に書かないのが鉄則です

ジェネリッククラスを作るで詳しく読む

プリミティブ型を T に入れようとする

Box<int> Box<double> はコンパイルエラーです。型パラメータには必ず参照型 (クラス) を渡す必要があるので、intIntegerdoubleDoublebooleanBoolean というラッパー型を指定します

ジェネリッククラスを作るで詳しく読む

Tstatic フィールドに使う

static T defaultValue; のような書き方はコンパイルエラーになります。static はクラス全体で共有される領域で、インスタンスごとに T が変わるという前提と矛盾するためです。static メソッドにジェネリクスを使いたいときは、メソッド側に <T> を書く「ジェネリックメソッド」という別の仕組みを使います

ジェネリッククラスを作るで詳しく読む

ジェネリックメソッド

レッスンを読む

raw 型のリストを渡してしまう

List raw = new ArrayList(); のようなジェネリクスなしのリストを渡すと、TObject 扱いになり警告が出ます。raw 型は禁忌 と覚えて、必ず List<String> のように型を指定してください

ジェネリックメソッドで詳しく読む

型推論が効かない場面で <> を省く

var x = Util.firstOf(List.of()); のように空のリストを直接渡すと推論できず Object になります。そういう時は Util.<String>firstOf(List.of()) のように明示的に型を指定する必要があります

ジェネリックメソッドで詳しく読む

境界型パラメータ

レッスンを読む

extendsimplements と書いてしまう

普段クラスで implements Comparable と書いている習慣で、つい <T implements Comparable<T>> と書きたくなりますが、これは文法エラーです。ジェネリクスでは、相手がクラスでもインターフェイスでも、境界はすべて extends で揃えます

境界型パラメータで詳しく読む

境界に int などのプリミティブを書こうとする

<T extends int> のような書き方はできません。ジェネリクスはそもそも 参照型 (クラス・インターフェイス) しか扱えないので、整数なら Integer、小数なら Double のように ラッパー型 を境界に指定します

境界型パラメータで詳しく読む

境界を付けないまま .doubleValue() 等を呼ぶ

<T> だけだと、コンパイラは TObject 相当としか見られないので、Number のメソッドは呼べません。「数値専用の処理を書きたい」と思ったら、まず <T extends Number> を疑うクセを付けると安全です

境界型パラメータで詳しく読む

ワイルドカード ?

レッスンを読む

?T を同じものだと思ってしまう

メソッドの型パラメータ <T> は「呼び出し時に決まる具体的な型」を表すので、引数と戻り値で同じ T を使うとリンクされます。一方 ? は「何か未知の型」というだけで、複数の ? は別々の未知です。「引数を読み取って同じ型のまま返したい」なら <T> T pick(List<T> list) のように T を使い、「中身は何でもいい、件数だけ知りたい」なら ? を使う、と使い分けましょう

ワイルドカード ?で詳しく読む

List<?>add しようとする

List<?> は「中身の本当の型が分からないリスト」なので、安全に追加できる値は null だけです。list.add(10) と書くと capture of ? というエラーメッセージで止まります。add したいときは <? super T> を使うか、具体的な型 <Integer> を使ってください

ワイルドカード ?で詳しく読む

PECS の方向を逆に覚えてしまう

extends で書き込めそう」「super で読み取れそう」と直感で考えると逆向きに覚えてしまいがちです。Producer (生産者 = 出す側) は extendsConsumer (消費者 = 受け取る側) は super という言葉ごと暗記してしまうのが早道です

ワイルドカード ?で詳しく読む

Pair<K,V> を作る

レッスンを読む

型パラメータの順序を逆にする

Pair<K, V>new Pair<Integer, String>(30, "Alice") のように逆に渡してしまうと、getKey()IntegergetValue()String になります。Map<K, V> も同じ順序で、Key → Value が世界共通の並びだと覚えておきましょう

Pair<K,V> を作るで詳しく読む

equals を書かずに HashSet HashMap で使う

自作 PairSet に入れて重複排除しようとしても、equalshashCode を実装していないと別物として扱われます。値クラスは Record で書くか、equalshashCode を必ずセットで実装しましょう

Pair<K,V> を作るで詳しく読む

フィールドを可変にしてしまう

key valuefinal にせず、setKey のような setter を生やしてしまうと、Pair がいつでも変わる箱になってしまいます。HashMap のキーとして使った後に値を書き換えると、ハッシュ値が変わってキーを見失う、という最悪のバグを引き起こします。値クラスは必ず 不変 (イミュータブル) にしましょう

Pair<K,V> を作るで詳しく読む

ジェネリクス まとめクイズ

レッスンを読む

List<Integer>List<Number> のサブ型ではない

IntegerNumber のサブ型でも、コレクションは 不変 (invariant) なのでサブ型関係が崩れる。柔軟にしたいときに List<? extends Number> を使う

ジェネリクス まとめクイズで詳しく読む

try-catch で例外をつかむ

レッスンを読む

catch (Exception e) で握りつぶす

─ 何でもキャッチして catch ブロックが空っぽ ({} だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でも e.printStackTrace(); か業務ログ出力を入れる、というのが鉄則です

try-catch で例外をつかむで詳しく読む

stacktrace を捨てる

catch (Exception e) { throw new RuntimeException("失敗"); } のように、元の e を渡さずに新しい例外を作るパターン。これだと「失敗」とだけ書かれた例外が飛んで、本当の原因 (どのファイルの何行目で何が起きたか) が消えます。必ず throw new RuntimeException("失敗", e); のように原因を引き継ぎましょう

try-catch で例外をつかむで詳しく読む

catch の順序を間違える

─ 複数の catch を並べるときは、catch (Exception e) のような広いものを後ろに書きます。先頭に書くと、その後ろの catch (NumberFormatException e) は「絶対に到達できないコード」とみなされてコンパイルエラーになります。狭い型から広い型へ、の順番が原則です

try-catch で例外をつかむで詳しく読む

複数の catch ブロック

レッスンを読む

Exception を一番上に書いて到達不可コードを作る

catch (Exception e) を最初に置き、その下に catch (NumberFormatException e) を書くと、後者には永遠に到達しません。Java はビルドエラー (exception has already been caught) で止めてくれますが、原因がピンと来ないと数十分溶かしがちです。順序は 具体 → 抽象 が鉄則です

複数の catch ブロックで詳しく読む

catch でもみ消して原因が分からなくなる

catch (Exception e) { /* 何もしない */ } のようにブロックを空にすると、例外が握り潰されて呼び出し側が異常に気付けません。最低でも System.err.println(e) でログを出すか、別の例外で再 throw するのが鉄則です

複数の catch ブロックで詳しく読む

finally の中で return してしまう

finally の中で return を書くと、trycatch 内で書いた return が 上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、returntry/catch 側にまとめておくのが安全です

複数の catch ブロックで詳しく読む

throws と例外の伝播

レッスンを読む

throws を書き忘れて検査例外を投げる

FileReader などを使うコードを書いて throws IOException を忘れると、unreported exception IOException; must be caught or declared to be thrown というエラーが出ます。コンパイラのメッセージのとおり、try-catchthrows のどちらかを必ず書きましょう

throws と例外の伝播で詳しく読む

catch (Exception e) {} で握り潰す

例外を catch しただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でも e.printStackTrace() を呼ぶか、throw new RuntimeException(e) のように包み直して再送出するのが鉄則です

throws と例外の伝播で詳しく読む

非検査例外に throws を書く

throws NullPointerException のように非検査例外を throws 句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadoc@throws タグでドキュメントに書く方が読みやすいです

throws と例外の伝播で詳しく読む

独自の例外を作る

レッスンを読む

Exception を継承して検査例外まみれになる

何の気なしに extends Exception と書くと、その例外を投げるメソッドには throws InsufficientFundsException の宣言が必要になります。呼び出し元、その呼び出し元、と上に向かってずっと宣言が伝染し、ラムダや Stream の中で投げられなくなる、という悲劇が起きがちです。意図的に検査例外にしたいのでなければ RuntimeException 継承にしましょう

独自の例外を作るで詳しく読む

super(message) を呼び忘れて getMessage()null になる

コンストラクタの中で super(message) を呼ばないと、親クラスにメッセージが伝わりません。後で e.getMessage() を見たら null で原因が全く分からない、というハマり方をします。引数で受け取った message は必ず super(message) (もしくは super(message, cause)) で渡してください

独自の例外を作るで詳しく読む

catch (Exception e) {} で握り潰す

カスタム例外を作っておきながら、上の層で catch (Exception e) {} のように何もしないと、例外を作った意味が完全に消えます。最低でも e.printStackTrace() か、構造化されたロガー (SLF4Jlogger.error(...)) で出力するクセを付けましょう。投げる例外と捕まえる例外は、必ずペアで設計するのが鉄則です

独自の例外を作るで詳しく読む

try-with-resources

レッスンを読む

implements AutoCloseable を忘れる

try (Resource r = ...)Resource クラスが AutoCloseable を実装していないと、required type that implements AutoCloseable というコンパイルエラーになります。class Resource implements AutoCloseable と必ず書きましょう

try-with-resourcesで詳しく読む

close() の中で重い処理や例外を派手に投げる

close() は「後始末」のメソッドです。ここでネットワーク通信をしたり、独自の例外を派手に投げたりすると、デバッグが非常に難しくなります。close() は冪等 (idempotent) に、つまり何回呼んでも安全になるよう書くのが鉄則です

try-with-resourcesで詳しく読む

try() の中で複数リソースを書く順番を間違える

close() は宣言と逆順で呼ばれるので、依存関係 (たとえば ConnectionStatementResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSetStatementConnection の順に閉じられる、ということです

try-with-resourcesで詳しく読む

finally ブロック

レッスンを読む

finally の中で return を書いて例外を握りつぶす

try から飛んできた例外を catch していないのに、finallyreturn してしまうと、本来呼び出し元に伝わるはずだった例外が消えてしまいます。デバッグ時に「エラーがどこかで握りつぶされている」と気づくのが非常に難しくなるので、finally の中で return するのは原則禁止と覚えておきます

finally ブロックで詳しく読む

finally で参照する変数を初期化していない

try ブロックの中だけで宣言した変数は、finally のスコープ外です。reader のようなリソース変数は try の 外側 で宣言し、null で初期化しておき、finallyif (reader != null) reader.close(); のようにチェックしてから使うのが定石です

finally ブロックで詳しく読む

リソース解放を try の最後に書いて、例外時に呼ばれない

reader.close()try ブロックの最後の行に書けば良い」と思いがちですが、try 中で例外が出るとそこから先は実行されません。close() が呼ばれず、ファイルハンドルやコネクションがリークします。リソース解放は finally、もしくは try-with-resources に必ず移す のが鉄則です

finally ブロックで詳しく読む

Stream を作る

レッスンを読む

Stream を使い回そうとする

前述の通り、ターミナル操作を呼んだ Stream は閉じます。Stream<Integer> s = ...; s.count(); s.forEach(...); のように 2 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です

Stream を作るで詳しく読む

CollectionStream を混同する

list.stream() は新しい Stream を返すだけで、元の list は変わりません。list.stream().filter(...) の結果をどこにも代入せずに、「list から偶数だけ消えたはず」と思い込むのはよくあるミスです。Stream は元データを変更しません

Stream を作るで詳しく読む

ターミナル操作を忘れる

Arrays.stream(arr).filter(n -> n > 0); と書いて「実行された」と思い込んでも、ターミナル操作がないので 何も起きません。これは 遅延評価 のせいで、filter を呼んだだけでは実際の処理が走らないからです。count() collect() forEach() などを最後に必ず付けましょう

Stream を作るで詳しく読む

filter で絞り込む

レッスンを読む

filter の中で副作用を書く

.filter(n -> { System.out.println(n); return n > 0; }) のようにログ出力やフィールド更新を filter の中に書くのは避けるべきです。filter は純粋に判定だけをするのが約束で、副作用を入れるとデバッグが難しくなり、並列ストリームでは予期しない動作になります。ログを入れたいときは専用の peek を使うか、デバッグ用に一時的に挟むだけにしましょう

filter で絞り込むで詳しく読む

複数 filter を分けて書きすぎる

.filter(n -> n > 0).filter(n -> n < 100).filter(n -> n % 2 == 0) のように filter をひたすら連結する書き方はパフォーマンスは大きく変わりませんが、可読性は条件によって落ちることがあります。論理 AND でつながる条件は n -> n > 0 && n < 100 && n % 2 == 0 のように 1 つにまとめるか、逆に意味の単位で分割するかを意識すると読みやすくなります

filter で絞り込むで詳しく読む

null 要素が混ざる可能性を忘れる

Stream<String> のように参照型を扱う場合、要素に null が混ざることがあります。.filter(s -> s.length() > 0) と書くと、null 要素で NullPointerException が出てしまいます。先に .filter(s -> s != null)null を弾いてから本来の条件を書く、または .filter(Objects::nonNull) を使うのが定石です

filter で絞り込むで詳しく読む

map で変換する

レッスンを読む

map の中で副作用を書く

map(x -> { System.out.println(x); return x; }) のように、変換のついでに println やフィールド書き換えをするのは Stream の作法に反します。表示やログ出力は peek か終端操作の forEach で行うのがルールです。副作用入りの map は並列ストリームで予期しない順序で動いて、バグの温床になります

map で変換するで詳しく読む

型が合わずコンパイルエラー

IntStreammapint -> int しか受け付けません。IntStream.of(1, 2, 3).map(x -> "num=" + x) のように String を返そうとすると怒られます。型を変えたいときは mapToObj を使うのが正解です

map で変換するで詳しく読む

null を返す map

map(x -> something(x)) の中で null を返すと、後続の処理で NullPointerException が出やすくなります。null を返す可能性があるなら、Optional でラップするか filter(Objects::nonNull) で前処理するのが安全です

map で変換するで詳しく読む

reduce で集約する

レッスンを読む

identity を省略して Optional を扱い忘れる

reduce(BinaryOperator) のように引数 1 つ版もありますが、こちらは空 Stream の可能性があるため戻り値が Optional<T> になります。int product = stream.reduce((a, b) -> a * b); と書いてもコンパイルすら通らず、Optional<Integer>int に代入できないと怒られます。今回のように空配列でも 1 を返したい場合は、identity を渡す 2 引数版を使うのが圧倒的に楽です

reduce で集約するで詳しく読む

accumulator に副作用を入れる

reduce の関数の中で System.out.println を呼んだり、外側のフィールドを書き換えたりするのは禁じ手です。並列化したときに表示順がぐちゃぐちゃになったり、ConcurrentModificationException が出たりして、デバッグ困難なバグの温床になります。accumulator は「ab を受け取って新しい値を返すだけの純粋関数」に保つ、というのが鉄則です

reduce で集約するで詳しく読む

結合則を破る演算を使う

reduce(0, (a, b) -> a - b) のように引き算で書くと、順次実行では 0 - 1 - 2 - 3 = -6 になりますが、並列で (0 - 1) と (2 - 3) を計算して合成すると結果が変わってしまいます。減算や除算は結合的ではないので、reduceaccumulator には使わないのが原則です。どうしても引き算したいときは、map(x -> -x).reduce(0, Integer::sum) のように加算に変換するか、for ループで書くかを検討します

reduce で集約するで詳しく読む

collect で集める

レッスンを読む

toMap のキー重複

toMap(keyMapper, valueMapper) でキーが重複すると IllegalStateException: Duplicate key が飛びます。マージ方法を決めるには 3 引数版の toMap(k, v, (a, b) -> a) で「重複したら先勝ち」のように指定するか、そもそも groupingByList にまとめるのが定石です

collect で集めるで詳しく読む

joiningdelimiter 指定忘れ

Collectors.joining() を引数なしで呼ぶと区切り文字なしで全部くっつきます。CSV のつもりで joining() と書いてしまうと "246" のようになるので、joining(",") を渡すのを忘れないようにしましょう

collect で集めるで詳しく読む

toList() の戻り値を変更しようとする

Java 16 から増えた Stream.toList() (引数なし) は immutableList を返します。後で add したいなら Collectors.toList() を使うか、new ArrayList<>(immutableList) で詰め替えてください

collect で集めるで詳しく読む

sorted で並び替え

レッスンを読む

sorted(null) のように Comparatornull を渡す

Java 8 の仕様では、sorted(Comparator)null を渡すと NullPointerException が飛びます。sorted() (引数なし) は自然順を意味するので、「自然順がほしい」なら 引数を渡さない ことが正解です。null をデフォルト扱いにしてはいけません

sorted で並び替えで詳しく読む

自然順 (Comparable) を実装していない型で sorted() を呼ぶ

自作クラス (たとえば class Point {}) のリストを sorted() (引数なし) で並べ替えようとすると、実行時に ClassCastException: ... cannot be cast to class java.lang.Comparable が出ます。自作クラスを自然順で並べたいなら implements Comparable<Point> を書いて compareTo を実装するか、sorted(Comparator.comparing(...)) を渡してください

sorted で並び替えで詳しく読む

Comparator に副作用を持ち込む

sorted((a, b) -> { counter++; return a - b; }) のように Comparator の中で外部変数を書き換えると、ソート結果が並列実行や JIT の最適化で予測不能になります。Comparator は「同じ入力に同じ出力を返す純粋関数」として書く、というのが鉄則です

sorted で並び替えで詳しく読む

distinct で重複排除

レッスンを読む

自作クラスで equals / hashCode を実装し忘れる

前述のとおり、自作クラスを distinct() に通しても参照比較になり、重複排除されません。record を使うか、IDE の自動生成で equalshashCode を必ずペアで書きましょう

distinct で重複排除で詳しく読む

Stream を 2 回使い回そうとする

Stream は一度終端操作を呼ぶと閉じられ、IllegalStateException が出ます。Stream<String> s = list.stream(); を 2 回使い回すのではなく、必要に応じて list.stream() を再度呼び直すのが正解です

distinct で重複排除で詳しく読む

順序保証を勘違いする

distinct() は元の Stream の出現順を維持します。{3, 1, 2, 1, 3} を流すと {3, 1, 2} の順で残ります。並べ替えたいときは別途 sorted() を繋いでください。逆に parallelStream() では順序保証が崩れる場合があるので、順序が重要なら stream() を使うか forEachOrdered を選びます

distinct で重複排除で詳しく読む

limit と skip

レッスンを読む

skip → limitlimit → skip の順序を取り違える

stream.skip(2).limit(3) は「先頭 2 件を捨ててから 3 件取る」(3, 4, 5 番目を取得)、stream.limit(3).skip(2) は「先頭 3 件に絞ってから 2 件捨てる」(結果は 1 件) と挙動が変わります。ページネーションは必ず skip → limit の順番で書きます

limit と skipで詳しく読む

無限 Stream に limit を付け忘れる

Stream.iterateStream.generate から作った無限 Stream を forEachcount に直接渡すと、プログラムが止まらなくなります。途中で必ず limittakeWhile などで打ち切りましょう

limit と skipで詳しく読む

limit(-1) skip(-1) のように負の引数を渡す

limitskip も負の値はサポートしておらず、実行時に IllegalArgumentException を投げます。page - 1 を計算するときに、page0 や負の値になっていないかを必ず検証してください

limit と skipで詳しく読む

並列ストリーム

レッスンを読む

parallel() を付ければ順序が保たれていると思い込む

forEach の出力順、reduce の集合体の組み立て順は 実行のたびに変わる と思って書く必要があります。順序が必要なら forEachOrderedCollectors.toList() を選びましょう

並列ストリームで詳しく読む

非結合的 な reducer を渡してしまう

引き算や、文字列を結合順序つきで作る処理は、並列だと結果が変わります。reduce("", String::concat) を並列で回すと、繋がる順番が毎回違うので、最終の文字列がランダムに並び替わったように見えます

並列ストリームで詳しく読む

小さな配列に parallel() を付けて逆に遅くなる

配列の要素数が数百〜数千程度では、並列化のオーバーヘッドのほうが大きく、for のほうが速いことが普通です。並列はデータが多くて、要素 1 個の計算が重いとき だけが効きます

並列ストリームで詳しく読む

ラムダ式の基本

レッスンを読む

式形式とブロック形式を混同する

n -> n * 2return を書かない、n -> { return n * 2; } は書く、というルールです。n -> { n * 2; } のように return を抜くとコンパイルエラーになります。{} を付けたら必ず return を書く、と覚えてください

ラムダ式の基本で詳しく読む

ローカル変数を後から書き換えてしまう

ラムダの中で参照する外側のローカル変数は final または effectively final (代入が 1 度きり) でなければなりません。int x = 10; Function<Integer,Integer> f = n -> n + x; まではよくても、その後 x = 20; と書き換えるとエラーになります。インスタンスフィールドなら自由に書き換えできますが、ローカル変数は固定です

ラムダ式の基本で詳しく読む

return を書ける場所と書けない場所を混同する

式形式は値そのものを返すので return を書きません。ブロック形式は普通のメソッドと同じく return で値を返します。式形式の右側にうっかり return n * 2 と書くとコンパイルエラーになるので注意してください

ラムダ式の基本で詳しく読む

メソッド参照 ::

レッスンを読む

引数の数が合わない

BiFunction<Integer, Integer, Integer> に対して Integer::parseInt を渡すとコンパイルエラーです。parseInt は 1 引数なので、2 引数のインターフェースには使えません。ラムダで書いたときに渡している引数の数と、メソッドの引数の数を必ず一致させることを意識しましょう

メソッド参照 ::で詳しく読む

戻り値の型が一致しない

Function<String, Integer> のところに Function<String, Long> を返すメソッド参照を渡そうとすると incompatible types のエラーになります。Long::parseLongInteger::parseInt を混同しがちなので、戻り値の型まで揃っているか確認してください

メソッド参照 ::で詳しく読む

コンストラクタ参照を ClassName.new と書く

正しくは ClassName::new です。.new は内部クラスのインスタンス生成用の別文法なので、ここで使うと意味が変わってしまいます。::new という記号の並びをそのまま覚えるのが安全です

メソッド参照 ::で詳しく読む

Function インターフェース

レッスンを読む

FunctionConsumer を混同する

Function<T, R> は「値を返す」関数、Consumer<T> は「値を受け取って戻り値なし (void) 」の関数です。System.out.println を渡したいだけなら Consumer 、結果を返してほしいなら Function 、と用途で選び分けます。間違って Function<Integer, Void> のような無理な型を作る前に、Consumer の存在を思い出してください

Function インターフェースで詳しく読む

ラムダ式の型推論に失敗する

var f = x -> x * 2; のように var でいきなり受けようとすると、コンパイラが「ラムダ式単体では型を決められない」と言ってエラーになります。必ず Function<Integer, Integer> f = x -> x * 2; のように 左辺で型を宣言 するか、メソッド引数として渡してその場で文脈から型を決めるかのどちらかにします

Function インターフェースで詳しく読む

null 引数を素通しさせる

Function<String, Integer> parser = Integer::parseInt;null を渡すと NullPointerException が出ます。Function 自体は null を弾く仕組みを持たないので、null の可能性があるときは事前に null チェックするか、Optional<T> を組み合わせるかで身を守る必要があります

Function インターフェースで詳しく読む

Predicate インターフェース

レッスンを読む

test の戻り値型を勘違いする

Predicate<Integer> を作るときに、つい n -> n * 2 のような「整数を返すラムダ」を書いてしまうケースです。Predicate.test は必ず boolean を返す約束なので、n -> n * 2Predicate<Integer> としては成立せず、incompatible types というコンパイルエラーになります。整数を別の整数に変換したいときは Predicate ではなく Function<Integer, Integer> を使うのが正解です

Predicate インターフェースで詳しく読む

null 判定を手書きしすぎる

参照型を扱う Predicate<String> などでは、要素に null が混ざることがあります。素直に s -> s != null && s.length() > 0 と書いてもよいのですが、null チェック専用のメソッド参照 Objects::nonNulljava.util.Objects に用意されています。Predicate<String> notNull = Objects::nonNull; のように書くと、意図が一発で伝わるのでおすすめです

Predicate インターフェースで詳しく読む

negate の順序を間違える

isPositive.and(isEven).negate()isPositive.and(isEven.negate()) は、見た目は似ていますが意味がまるで違います。前者は「正かつ偶数」の否定 (= 負または奇数)、後者は「正かつ奇数」です。negate がどの Predicate にかかっているかを意識して、必要なら (...) で明示するか、いったん変数に入れて段階的に組み立てるのが安全です

Predicate インターフェースで詳しく読む

Consumer と Supplier

レッスンを読む

Consumer の中で return を書いてしまう

Consumer<String> c = s -> { return s.length(); }; のように戻り値を書くとコンパイルエラーになります。accept の戻り値は void なので、ラムダの中も値を返してはいけません。値を返したいのは Function<T, R> の役割です

Consumer と Supplierで詳しく読む

Supplier.get() を毎回呼ぶのに副作用付きのラムダを書いてしまう

Supplier<Integer> next = () -> counter++; のように、内側でフィールドを書き換える Supplier は危険です。同じ Supplier を get() するたびに別の値が返ってくるので、ライブラリ側がいつ呼ぶかわからない (orElseGet Stream.generate 等) 文脈で予測不能なバグになります。Supplier は基本的に副作用なしで書きましょう

Consumer と Supplierで詳しく読む

hello.get のようにかっこを忘れて参照だけ書く

helloSupplier<String> ですが、String s = hello; と書くと型が違うとエラーになります。値を取り出すには必ず hello.get() のようにかっこ付きで get() を呼びます。Consumer も同様で、呼び出すときは printer.accept("x") の形が必要です

Consumer と Supplierで詳しく読む

BiFunction と 2 引数の関数

レッスンを読む

型パラメータの数を間違える

BiFunction<Integer, Integer> のように 2 個しか書かないと「型パラメータが足りない」エラーになります。BiFunction は引数 2 個 + 戻り値 1 個の 合計 3 個 が必須です。逆に Function<T, R> は 2 個、Predicate<T> は 1 個、と数が違うので、ペアで覚えるのがコツです

BiFunction と 2 引数の関数で詳しく読む

3 引数以上を BiFunction で書こうとする

標準ライブラリには BiFunction までしかありません。TriFunctionimport しようとして cannot find symbol で立ち止まる人が多いですが、これは 自作するか、引数をまとめる のが正解です。java.util.function.TriFunction というクラスは存在しません

BiFunction と 2 引数の関数で詳しく読む

1 引数の Function と混同する

Function<Integer, Integer> f = (x, y) -> x + y; は文法エラーです。Function は引数 1 個なので、ラムダも (x) -> ...x -> ... の形である必要があります。2 引数を受け取りたいなら型を BiFunction に変えるか、ラムダの引数を 1 つにします

BiFunction と 2 引数の関数で詳しく読む

String.format で書式化

レッスンを読む

%dString を渡してしまう

String.format("%d", "100") のように、文字列を %d の位置に入れると、IllegalFormatConversionException という実行時例外が飛びます。整数なら %d、文字列なら %s、と型を必ず合わせましょう。逆に %s には int でも double でも何でも入れられる (自動で toString() される) ので、迷ったときの保険として %s は便利です

String.format で書式化で詳しく読む

\n%n を取り違える

改行を入れたいとき、\n は環境にかかわらず常に LF (0x0A) になりますが、%n は OS ごとの改行 (Windows なら CRLF、Linux/macOS なら LF) に展開されます。ログや帳票を Windows でも Linux でも揃えたいときは %n を、HTML などプラットフォーム非依存で必ず LF にしたいときは \n を、というふうに使い分けます

String.format で書式化で詳しく読む

桁あふれと意図しない丸め

%.2f の丸めは HALF_UP(いわゆる四捨五入)です。ただし double/float0.10.2 などを二進浮動小数点で正確に表現できないため、表示する前の値そのものに微小な誤差が乗ります。お金の計算など厳密な精度が必要なときは BigDecimalsetScale(2, RoundingMode.HALF_UP) を使うのが安全です。String.format%.2f はあくまで「画面表示用の見た目を整える」ためのもの、と割り切りましょう

String.format で書式化で詳しく読む

StringBuilder で連結

レッスンを読む

toString() の呼び忘れ

String result = sb; と書くとコンパイルエラーです。String result = sb.toString(); のように明示的に呼び出してください。println のように Object#toString を内部で使ってくれるメソッドなら不要ですが、String 型の変数に入れるときや、API の引数として渡すときは必ず変換が必要です

StringBuilder で連結で詳しく読む

ループ外で + 連結を 1 回だけする場合に StringBuilder を持ち出す

String full = first + " " + last; のように、定数回 (2〜3 回) の連結なら、コンパイラが自動で StringBuilder 相当のコードに変換してくれます。乱用するとかえって読みにくくなるので、for while などのループ内で繰り返し連結するときに使うのが本来の使いどころです

StringBuilder で連結で詳しく読む

初期容量を指定せず巨大な文字列を組み立てる

new StringBuilder() のデフォルト容量は 16 文字です。1 万文字以上を作ると分かっているなら、new StringBuilder(10_000) のように指定しておくと内部配列の再確保が発生せず高速です。性能を気にする場面では覚えておきましょう

StringBuilder で連結で詳しく読む

正規表現の基本

レッスンを読む

\\ のエスケープ忘れ

Java の文字列で \d \w \s を書きたいときは必ず \\d \\w \\s のようにバックスラッシュ 2 つです。1 つしか書かないとコンパイルエラー (illegal escape character) になります

正規表現の基本で詳しく読む

matches を部分一致だと思い込む

String.matches は 完全一致 です。"foo bar".matches("foo")false を返します。部分的に含まれているかを調べたいなら s.contains("foo") を使うのが手軽です

正規表現の基本で詳しく読む

greedylazy の混同

* + ? はデフォルトで greedy (できるだけ長くマッチ) です。最小マッチにしたいときは *? +? のように ? を後ろに付けます。今回の課題のような単純な完全一致では気にしなくて構いません

正規表現の基本で詳しく読む

split と join

レッスンを読む

split の引数に正規表現特殊文字をそのまま渡す

"." "|" "?" などはエスケープしないと意図どおりに動きません。split("\\.") split("\\|") のようにバックスラッシュ 2 つでエスケープするか、Pattern.quote を使いましょう

split と joinで詳しく読む

空文字列を split した結果を空配列だと思い込む

"".split(",") は要素 1 個の配列 [""] を返します。空入力に対して空文字列を返したいときは、csv.isEmpty() で先に判定するのが安全です

split と joinで詳しく読む

末尾の空要素が消える仕様を知らない

"a,b,,".split(",") は 2 要素しか返しません。CSV の列数を厳密に揃えたいときは split(",", -1) を使って末尾の空要素を保持してください

split と joinで詳しく読む

LocalDate で日付を扱う

レッスンを読む

java.util.Date を使ってしまう

IDE の自動 importDate を選ぶと、java.util.Date が候補に出ます。今からの新規コードでは必ず java.time.LocalDate の方を選びましょう。import 文を確認する癖をつけると安全です

LocalDate で日付を扱うで詳しく読む

月を 0 始まりだと思い込む

Calendar 時代の名残で「5 月4」と書いてしまう人がいます。LocalDate.of(2026, 5, 19)5 はそのまま 5 月 です。0 を渡すと DateTimeException が出ます

LocalDate で日付を扱うで詳しく読む

戻り値を捨てる

date.plusDays(1); と書いても、戻り値を受け取らないと意味がありません。LocalDate は不変なので、必ず date = date.plusDays(1); のように再代入するか、別の変数に受けてください

LocalDate で日付を扱うで詳しく読む

LocalDateTime で時刻も扱う

レッスンを読む

yyyyYYYY を取り違える

小文字 yyyy が普通の暦年 (2026)、大文字 YYYYweek-based-year という年またぎ週の特殊な年を表します。たとえば 2025-12-29 (月) のように年末が月曜から始まる週だと、YYYY2026 を返すことがあります。請求書や領収書を作るときに混入すると、「なぜか 1 件だけ翌年扱い」というバグになるので、 を出したいときは必ず小文字 yyyy を使います

LocalDateTime で時刻も扱うで詳しく読む

MM (月) と mm (分) を取り違える

yyyy-mm-dd hh:MM のように書いてしまうと、月のはずの位置に分が、分のはずの位置に月が入り、2026-30-19 02:05 のような壊れた文字列ができあがります。日本語で書くと「月は大文字 MM、分は小文字 mm」、と語呂で覚えるのが安全です

LocalDateTime で時刻も扱うで詳しく読む

SimpleDateFormat を使ってしまう

旧 API の java.text.SimpleDateFormat は今でも import できますが、スレッドセーフでないため、ServletSpring のように複数リクエストが同時に走る環境で共有変数として持つと、たまに壊れた文字列を返すという最悪のバグになります。新規コードでは必ず DateTimeFormatter を使い、SimpleDateFormat は触らないのが鉄則です

LocalDateTime で時刻も扱うで詳しく読む

Duration で時間差を計算

レッスンを読む

Duration に年や月の長さを聞かない

Duration は最大単位が「日」ですが、toDays() の戻り値も「秒数を 86400 で割った値」にすぎず、Duration 自体は「年 / 月」の概念を持っていません。Duration.ofDays(60).toMonths() のようなメソッドはなく、書こうとしてもコンパイルエラーになります。年・月単位の差を取りたいときは Period または ChronoUnit.MONTHS.between を使いましょう

Duration で時間差を計算で詳しく読む

Period で秒や時間を扱おうとする

逆に、Period.between(localTime1, localTime2) は 型エラー になります。PeriodLocalDate 専用で、LocalTimeLocalDateTime を渡せません。「ストップウォッチ的な短い時間」を測りたいときは必ず Duration を使います

Duration で時間差を計算で詳しく読む

between の引数順を逆にして符号がおかしくなる

ChronoUnit.DAYS.between(start, end)end - start を返します。start のほうが未来だと 負の値 になります。「絶対値が欲しい」のか「向きを保ちたいのか」を意識して書きましょう。Math.abs(...) で絶対値を取るのは簡単ですが、「過去か未来か」の情報を捨てる前に、本当にそれでいいかを一度立ち止まって考えるのが大事です

Duration で時間差を計算で詳しく読む

日付の書式化とパース

レッスンを読む

yyyyYYYY を混同する

小文字の yyyy は普通の暦年ですが、大文字の YYYY週ベースの年 を意味します。年末年始をまたぐ ISO 週で結果がズレるため、Windows のメモ帳で書いた仕様書をコピペしたら 1 月 1 日が 2027 年扱いになる、というバグが起きやすいです。日付 を出すときはほぼ常に小文字の yyyy が正解です

日付の書式化とパースで詳しく読む

MMmm の取り違え

大文字の MM は月、小文字の mm は分。これを逆に書くと、yyyy/mm/dd というパターンが「年/分/日」と解釈されます。LocalDate には時刻情報がないため分は常に 0 となり、2026/00/19 のような意図しない出力になります。コンパイルエラーにならない分、見つけにくい不具合です

日付の書式化とパースで詳しく読む

HHhh の取り違え

HH/hhLocalDateTimeLocalTime など時刻を持つ型で使うパターンです。LocalDate には時刻フィールドがないため、このミスが実害として現れるのは時刻を扱う型が対象のときです。24 時間制で表示したいのに hh と書くと 12 時間制 (112) になります。午後 3 時が 03 と表示されてしまい、AM/PM の区別 (a パターン) を入れ忘れると 03:00 が午前か午後か分からなくなります。業務アプリでは原則 HH を使うと覚えてください

日付の書式化とパースで詳しく読む

日付の加減算

レッスンを読む

戻り値を捨ててしまう

today.plusDays(10); と書いただけで、戻り値を変数に代入し忘れるのが一番多いミスです。LocalDate は不変なので、戻り値を捨てると計算結果はどこにも残りません。必ず LocalDate later = today.plusDays(10); のように受け取りましょう

日付の加減算で詳しく読む

月またぎを手で計算しようとする

5/305 日足したいから「日 = 30 + 5 = 355 月は 31 日までだから 4 を引いて 6/4」のような計算は不要です。plusDays(5) に任せれば一発です。月またぎ・年またぎ・うるう年は API がすべて面倒を見てくれます

日付の加減算で詳しく読む

不変オブジェクトを誤解する

Calendar 時代の感覚で today.plusDays(10) を呼んだら today 自身が変わると思い込むパターンです。Stringreplace と同じく、LocalDateplus* minus* with* はすべて新しいインスタンスを返します。元のオブジェクトは絶対に変わりません

日付の加減算で詳しく読む

Files でファイルに書き込む

レッスンを読む

close() を呼び忘れる / 古い書き方をする

new FileWriter(...) 系の古い API を使うと、finallyclose() を書かないとリソースが残ります。try-with-resources (try (var w = new FileWriter(...)) { ... }) を必ず使うか、いっそ Files.writeString に置き換えるのが正解です

Files でファイルに書き込むで詳しく読む

エンコーディングを指定し忘れる

古い new FileWriter("a.txt") は OS のデフォルトエンコーディングで書き込むので、Windows (Shift_JIS) と Linux (UTF-8) で結果が変わることがあります。Files.writeString は常に UTF-8 なので安全です

Files でファイルに書き込むで詳しく読む

IOException を握りつぶす

ファイル操作は失敗する可能性が常にあります (権限・容量・パスの存在)。catch (IOException e) {} のように空ブロックで握りつぶすと、トラブルが起きても原因が分からなくなるので、必ず e.printStackTrace()throw new RuntimeException(e) で見える形にしましょう

Files でファイルに書き込むで詳しく読む

ファイルを行単位で読む

レッスンを読む

巨大ファイルに readAllLines を使う

readAllLines は全行を一気にメモリへロードします。1GB のログを読もうとすると OutOfMemoryError で死にます。サイズ感が分からない場合は最初から Files.lines + Stream で書く癖を付けるのが安全です

ファイルを行単位で読むで詳しく読む

エンコーディングを指定せず化ける

Java 8 以降は引数なしで UTF-8 ですが、ファイル自体が Shift_JIS で保存されていれば文字化けします。読み込みが化けたら、まずファイルのエンコーディングを疑い Charset.forName("Shift_JIS") を渡してみましょう

ファイルを行単位で読むで詳しく読む

空ファイルで NullPointerException を期待する

Files.readAllLines は空ファイルでも null ではなく 空の List を返します。if (lines == null) のような防衛は不要で、代わりに if (lines.isEmpty()) で判定するのが正解です

ファイルを行単位で読むで詳しく読む

Path でファイルパスを扱う

レッスンを読む

String+ でパスを連結する

dir + "/" + name のように書くと Windows では /\ が混ざり、name 側に余計な / が含まれていると // ができます。必ず dir.resolve(name) を使いましょう

Path でファイルパスを扱うで詳しく読む

OS 依存の区切り文字を直書きする

"C:\\Users\\taro" のような書き方は Linux で動きません。マルチプラットフォームで動かしたいコードでは Paths.get("C:", "Users", "taro") のように分割して渡すか、設定ファイルから読み込みます

Path でファイルパスを扱うで詳しく読む

resolve+ を同じ感覚で使う

resolve に絶対パスを渡すと「左辺を捨てて絶対パスを返す」という挙動になります。これは仕様であり、+ の単純連結とは違うので注意してください。期待しない上書きを避けたい場合は、事前に child.isAbsolute() を確認します

Path でファイルパスを扱うで詳しく読む

BufferedReader と Stream API

レッスンを読む

Stream を閉じ忘れる

Files.lines(p).mapToInt(...).sum() のようにワンライナーで書いてしまうと、Streamclose されずファイルディスクリプタがリークします。少量なら気づきにくいですが、ループの中で何回も呼ぶと「Too many open files」エラーで突然落ちます。必ず try (Stream<String> lines = Files.lines(p)) { ... } の形にしましょう

BufferedReader と Stream APIで詳しく読む

空行のパースで NumberFormatException

Integer.parseInt("") は実行時例外を投げます。改行コードの違いや末尾改行で空文字列が混ざることがあるので、filter(s -> !s.isEmpty()) を挟むか、s -> s.trim().isEmpty() のように空白だけの行も弾く処理を入れると安心です

BufferedReader と Stream APIで詳しく読む

エンコーディングの指定を忘れる

Files.lines(p) は引数 1 つだと UTF-8 を仮定します。日本語を含む Shift_JIS のファイルなどを読もうとすると MalformedInputException が出ます。明示的に指定したいときは Files.lines(p, StandardCharsets.UTF_8) のように 2 引数版を使い、入力が Shift_JIS なら Charset.forName("MS932") を渡します

BufferedReader と Stream APIで詳しく読む

ファイルの存在確認と削除

レッスンを読む

exists の戻り値を盲信する (TOCTOU)

Files.exists(p)true を取った直後に別のプロセスがファイルを消すと、続く Files.delete(p)NoSuchFileException を投げます。これは Time Of Check / Time Of Use の頭文字を取って TOCTOU 問題と呼ばれ、確実に避けたいケースでは Files.deleteIfExists(p) を使うか、例外を try で受け止めるのが定石です

ファイルの存在確認と削除で詳しく読む

Files.deleteFiles.deleteIfExists を混同する

前者はファイルが無いと NoSuchFileException、後者は無くても静かに false を返します。「無いなら無いで OK」のときは後者、「絶対あるはず」のときは前者、と用途で使い分けましょう

ファイルの存在確認と削除で詳しく読む

Files.delete でディレクトリを消そうとして失敗する

中身が残っているディレクトリは DirectoryNotEmptyException で消せません。再帰的に消したいときは Files.walk(p) で全ファイルを取り出してから逆順に Files.delete する、もしくは Files.walkFileTree を使うのが定石です

ファイルの存在確認と削除で詳しく読む

extends で継承する

レッスンを読む

extendsimplements の混同

クラスを継承するときは extendsインタフェース を実装するときは implements を使います。class Dog implements Animal のように書いてしまうと、Animal がクラスの場合はコンパイルエラーになります。逆に interface Walkerextends しようとしてもエラーです。「クラスから機能を引き継ぐなら extends、契約だけ約束するなら implements」と覚えましょう

extends で継承するで詳しく読む

すべてのクラスが暗黙で Object を継承していることを忘れる

Java では extends を書かないクラスでも、自動的に java.lang.Object を継承しています。だから toString()equals()hashCode() がどんなクラスでも呼べるわけです。Animal extends Object と明示しても同じ意味ですが、わざわざ書かないのが普通です

extends で継承するで詳しく読む

final クラスを継承しようとする

String Integer LocalDate などの標準クラスや、自分で final class と宣言したクラスは継承できません。class MyString extends String と書くと cannot inherit from final class String というコンパイルエラーになります。継承させたくないクラスにはあえて final を付ける、というのも設計上のテクニックです

extends で継承するで詳しく読む

super で親を呼ぶ

レッスンを読む

super(...) を 2 行目以降に書いてしまう

super(...)this(...) は コンストラクタの 1 行目 に限ります。前にログ出力やバリデーションを入れたくなりますが、それをするとコンパイルエラーです。どうしても前処理したいときは static メソッドに切り出して、super(prepare(arg)) のように引数の中で呼びましょう

super で親を呼ぶで詳しく読む

オーバーライドした子で super なしの自己呼び出しをしてしまう

Child.greet() の中で greet() と書くと、this.greet() 扱いで自分自身が呼ばれ、無限再帰で StackOverflowError になります。親を呼びたいなら必ず super.greet() と書くこと

super で親を呼ぶで詳しく読む

final クラス / メソッドを継承しようとする

String Math などの final クラスは extends できません。エラーメッセージ cannot inherit from final が出たら、設計を見直して「継承」ではなく「コンポジション (フィールドとして持つ)」に切り替える方が筋がよいケースが多いです

super で親を呼ぶで詳しく読む

メソッドのオーバーライド

レッスンを読む

@Override を忘れてオーバーロードになる

─ 親が area() なのに子で area(int x) と書いてしまうと、別メソッドが追加されただけで親の area() はそのまま生きてしまいます。Shape s = new Circle(10); s.area(); を呼んでも親の 0 が返ってきて「なんで動かないんだ?」と悩むパターンです。 @Override を付けておけばコンパイラが「親に一致するメソッドがない」と弾いてくれます

メソッドのオーバーライドで詳しく読む

アクセス修飾子を絞ってしまう

─ 親の public int area() を子で protected int area() と書くと、コンパイルエラーになります。これは前述のルールどおりで、「子は親より広く、または同じ」可視性しか許されません。逆向き (protectedpublic) はオッケーです

メソッドのオーバーライドで詳しく読む

static メソッドを「オーバーライド」しているつもり

─ 親と子で同名の static メソッドを書くと、文法上は通りますがこれは オーバーライド ではなく 隠蔽 (hiding) と呼ばれる別の現象です。動的ディスパッチ の対象外で、呼び先はコンパイル時の型で決まります。 Shape.foo()Circle.foo() がそれぞれ独立に存在し、Shape s = new Circle(); s.foo(); は親の foo を呼んでしまいます。 static メソッドは オーバーライド できないと覚えておきましょう

メソッドのオーバーライドで詳しく読む

abstract で抽象クラス

レッスンを読む

abstract メソッドに本体を書いてしまう

public abstract double area() { return 0; } のように { } を付けるとコンパイルエラーです。エラーメッセージは abstract methods cannot have a body などになります。本体を書きたいなら abstract を外し、サブクラスで @Override するスタイルにします

abstract で抽象クラスで詳しく読む

抽象クラスを new しようとする

new Shape("hoge") はコンパイルエラーです。設計図を直接実体化しようとしているサインなので、TriangleCircle のような具象サブクラスを new し、変数の型だけを Shape にする (Shape s = new Triangle(4, 5);) のが正解です

abstract で抽象クラスで詳しく読む

finalabstract を同時に書く

final は「これ以上継承させない」、abstract は「サブクラスで上書きさせる」という真逆の意味を持つので、同じクラス/メソッドに付けるとコンパイルエラーになります。どちらか一方だけにします

abstract で抽象クラスで詳しく読む

interface で契約を定義

レッスンを読む

extendsimplements の取り違え

クラスがクラスを継承するときは extends、クラスが interface を実装するときは implements です。class MyRunner extends Runnable2 と書くと「Runnable2 クラスを継承しようとしている」と解釈され、Runnable2interface の場合は no interface expected here 系のエラーになります

interface で契約を定義で詳しく読む

interface のフィールドを変数のつもりで書く

interface の中に int count = 0; と書くと、Java は黙ってそれを public static final int count = 0; と解釈します。インスタンスごとの状態は持てず、後から代入もできません。状態を持たせたいなら interface ではなく abstract class を選びましょう

interface で契約を定義で詳しく読む

実装メソッドを public 以外で書く

契約上のメソッドは暗黙的に public abstract 扱いなので、実装側を privatepackage-private (修飾子なし) にすると「アクセス修飾子を弱められない」エラーになります。@Override public String name() の形を体に染み込ませてください

interface で契約を定義で詳しく読む

default メソッド

レッスンを読む

default だらけで interface が肥大化する

便利だからといって、振る舞いの 90% を default で書いてしまうと、もはや interface ではなく abstract class のような重い存在になります。interface は「実装すべきメソッドの契約」を表すのが基本なので、default は「後から追加する後方互換ヘルパー」「複数の実装が共通して使う簡単な定型処理」に絞るのがコツです

default メソッドで詳しく読む

多重継承の衝突を放置する

2 つの interface が同じ default を持つと、コンパイルエラーが必ず出ます。Greeter.super.greet(name) 構文を覚えて、衝突したときは実装クラス側で明示的に override する習慣を付けましょう。エラーメッセージは class X inherits unrelated defaults のように出ます

default メソッドで詳しく読む

abstract class と混同して フィールド を持たせようとする

interface には public static final の定数しか書けません。private int count; のようにフィールドを書こうとするとコンパイルエラーになります。状態を持ちたいなら abstract class を選ぶか、実装クラス側にフィールドを置いてください

default メソッドで詳しく読む

instanceof と型パターン

レッスンを読む

instanceofequals を混同する

instanceof は「型が同じ (またはサブクラス) か」を見る演算子で、equals は「値が等しいか」を見るメソッドです。"hello" instanceof Stringtrue ですが、"hello".equals("hello")true、意味はまったく違います。型を見たいときは instanceof、値を見たいときは equals、と覚えてください

instanceof と型パターンで詳しく読む

null に対する instanceof は常に false

null instanceof String(Object) null instanceof Object も、結果はすべて false です。これは Java の仕様で、null には実型が存在しないためです。null を扱いたい場合は if (o == null) { ... } のように別途分岐するか、Java 21 なら switchcase null を使います

instanceof と型パターンで詳しく読む

ダウンキャストの ClassCastException

instanceof で確認せずにいきなり (String) o のようにキャストすると、実型が違っていれば実行時に ClassCastException が飛びます。パターンマッチングを使えばこのミスは構造的に防げるので、Java 16 以降のコードベースでは積極的に切り替えましょう

instanceof と型パターンで詳しく読む

ポリモーフィズムを活かす

レッスンを読む

キャストが必要だと思い込む

Shape s = new Circle(2); のあと s.area(); を呼ぶのに、((Circle) s).area(); のように書く必要はありません。子型に固有のメソッドを呼びたいときだけ ダウンキャスト が必要です。共通メソッドを呼ぶだけなら親型のままで十分です

ポリモーフィズムを活かすで詳しく読む

static メソッドはポリモーフィズムの対象外

static メソッドはクラスに紐づくため、@Override ではなく 隠蔽 (hiding) されるだけです。Shape s = new Circle(2); s.staticMethod(); と書いても、呼ばれるのは変数型 Shapestatic メソッドです。インスタンスメソッドだけが動的ディスパッチの対象だと覚えてください

ポリモーフィズムを活かすで詳しく読む

final メソッドはオーバーライド禁止

親クラスのメソッドに final が付いていると、子クラスで @Override しようとした瞬間にコンパイルエラーになります。同様に final class 自体は継承できません。「これ以上拡張させたくない」という意思表示の機能なので、ライブラリ作者が意図的に付けることが多いです

ポリモーフィズムを活かすで詳しく読む