import を忘れる
ArrayList を使うには import java.util.ArrayList; が必要です。これを忘れると cannot find symbol というエラーが出ます。IDE なら自動補完でやってくれますが、手書きの試験などでは注意しましょう
レッスンの本文で注意している点を、つまずきの側から168件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。
import を忘れるArrayList を使うには import java.util.ArrayList; が必要です。これを忘れると cannot find symbol というエラーが出ます。IDE なら自動補完でやってくれますが、手書きの試験などでは注意しましょう
list[0] と書いてしまうArrayList はクラスなので、配列の [] 記法は使えません。必ず list.get(0) のようにメソッド呼び出しで取り出します。逆に、取り出した値の型 (Integer) を int 変数に入れるときは int x = list.get(0); のように自動で変換されます (オートアンボクシング)
ArrayList list = new ArrayList(); のようにジェネリクスを省くと、get(0) の戻り値は Object になります。Integer x = (Integer) list.get(0); のように明示的なキャストが必要になり、安全性も下がります。型は必ず <> で指定しましょう
list.size と書いてしまう要素数は list.size() です。配列の arr.length はカッコなしのフィールドですが、ArrayList の size() は メソッド なので必ず () が必要です。list.size と書くとコンパイルエラーになります
int と Integer を取り違えるArrayList<Integer> には基本型の int をそのまま入れているように見えますが、実はラッパクラス Integer に自動変換 (auto-boxing) されています。list.remove(5) と書くと「インデックス 5 番目を削除」、list.remove(Integer.valueOf(5)) と書くと「値が 5 の要素を削除」になり、結果がまったく違うので注意してください
remove する上で書いた ConcurrentModificationException の話です。拡張 for ループ中に list.remove(...) を呼ぶと例外が飛びます。安全に消すなら Iterator.remove() を使うか、別 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 -> 条件) のような書き方に切り替えてください
null をキーに使ってしまうHashMap は技術的には null キーを 1 つだけ許容しますが、可読性が落ちるうえ ConcurrentHashMap では NPE を投げます。実務では null キーは使わないと決めておきましょう
Map<String, Integer> に int を put するときは自動で Integer に箱詰め (auto-boxing) されますが、逆に int x = map.get("apple") のように受け取るとき、もし null が返ると NPE になります。Integer x = ... で受けるか getOrDefault でデフォルト値を指定するのが安全です
HashMap は put した順番を保持しません。順番に処理したいときは LinkedHashMap を使うか、キーをリスト化してソートしてから回します
HashMap の順序を期待してしまうput した順や key のソート順が出てくると思い込むのは典型的なバグです。順序が必要なら LinkedHashMap か TreeMap を選ぶ
Map.Entry を null チェックしてしまうfor-each が回している Map.Entry 自体は null にはなりません。null チェックすべきは中身の value (e.getValue() == null) であって、Entry そのものではありません
map.entrySet() の戻り型を勘違いするentrySet() は Set<Map.Entry<K,V>> を返します。List でも Map でもなく Set なので、インデックスでアクセスはできません。あくまで「ペアの集合」をループするためのものだと覚えましょう
HashSet の順序を期待してしまうHashSet は要素の並びを保証しません。for で回したときの順番がプログラムを再起動したら変わる、ということも普通に起きます。順序が大事なら LinkedHashSet か TreeSet を使いましょう
equals / hashCode の実装を忘れる自作クラスを HashSet に入れるときは、equals と hashCode を両方ともオーバーライドする必要があります。片方だけだと「同じはずの 2 つが別物扱いされる」事故が起きます
null の扱いを誤解するHashSet と LinkedHashSet は null を 1 個だけ入れられます。TreeSet は null を入れようとすると NullPointerException で落ちます。Set ならどれでも同じ、と思い込まないようにしましょう
Arrays.asList で返るリストは固定長Arrays.asList(1, 2, 3) の戻り値は add や remove を呼ぶと UnsupportedOperationException が飛びます。サイズを変えたいなら new ArrayList<>(Arrays.asList(...)) のように ArrayList で ラップ してください
Arrays.asList(int[]) は要素 1 個のリストになるint 配列を Arrays.asList(arr) に渡すと、List<int[]> (要素数 1 の、配列を 1 個だけ持つリスト) になってしまいます。Collections.sort も期待どおりに動きません。プリミティブ配列を List 化したいときは for ループか Arrays.stream(arr).boxed() を使う必要があります
Collections.sort を Comparator なしで呼ぶ条件Collections.sort(list) が引数 1 つで動くのは、要素が Comparable を実装している型 (Integer String Double など) のときだけです。自作クラスのリストを sort したい場合は、クラスに implements Comparable<MyClass> を書くか、Collections.sort(list, comparator) で Comparator を渡す必要があります
─ Box box = new Box("hello"); のように <> を省略すると raw 型 扱いになり、getValue() の戻り値が Object になってしまいます。コンパイラは警告を出すだけで止めてはくれませんが、現代の Java では絶対に書かないのが鉄則です
T に入れようとする─ Box<int> Box<double> はコンパイルエラーです。型パラメータには必ず参照型 (クラス) を渡す必要があるので、int は Integer、double は Double、boolean は Boolean というラッパー型を指定します
T を static フィールドに使う─ static T defaultValue; のような書き方はコンパイルエラーになります。static はクラス全体で共有される領域で、インスタンスごとに T が変わるという前提と矛盾するためです。static メソッドにジェネリクスを使いたいときは、メソッド側に <T> を書く「ジェネリックメソッド」という別の仕組みを使います
<T> を戻り値の後ろに書いてしまうpublic static T <T> firstOf(...) のように書くとコンパイルエラーです。必ず 戻り値型の直前 に <T> を置きましょう
List raw = new ArrayList(); のようなジェネリクスなしのリストを渡すと、T が Object 扱いになり警告が出ます。raw 型は禁忌 と覚えて、必ず List<String> のように型を指定してください
<> を省くvar x = Util.firstOf(List.of()); のように空のリストを直接渡すと推論できず Object になります。そういう時は Util.<String>firstOf(List.of()) のように明示的に型を指定する必要があります
extends を implements と書いてしまう普段クラスで implements Comparable と書いている習慣で、つい <T implements Comparable<T>> と書きたくなりますが、これは文法エラーです。ジェネリクスでは、相手がクラスでもインターフェイスでも、境界はすべて extends で揃えます
int などのプリミティブを書こうとする<T extends int> のような書き方はできません。ジェネリクスはそもそも 参照型 (クラス・インターフェイス) しか扱えないので、整数なら Integer、小数なら Double のように ラッパー型 を境界に指定します
.doubleValue() 等を呼ぶ<T> だけだと、コンパイラは T を Object 相当としか見られないので、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 (生産者 = 出す側) は extends、Consumer (消費者 = 受け取る側) は super という言葉ごと暗記してしまうのが早道です
Pair<K, V> を new Pair<Integer, String>(30, "Alice") のように逆に渡してしまうと、getKey() が Integer、getValue() が String になります。Map<K, V> も同じ順序で、Key → Value が世界共通の並びだと覚えておきましょう
equals を書かずに HashSet HashMap で使う自作 Pair を Set に入れて重複排除しようとしても、equals と hashCode を実装していないと別物として扱われます。値クラスは Record で書くか、equals と hashCode を必ずセットで実装しましょう
key value を final にせず、setKey のような setter を生やしてしまうと、Pair がいつでも変わる箱になってしまいます。HashMap のキーとして使った後に値を書き換えると、ハッシュ値が変わってキーを見失う、という最悪のバグを引き起こします。値クラスは必ず 不変 (イミュータブル) にしましょう
List<Integer> は List<Number> のサブ型ではないInteger は Number のサブ型でも、コレクションは 不変 (invariant) なのでサブ型関係が崩れる。柔軟にしたいときに List<? extends Number> を使う
<T super Number> は書けない下限境界はクラス / メソッド定義では使えず、ワイルドカードでのみ使える
ジェネリクス まとめクイズで詳しく読むnew T() は不可型消去のため。Class<T> をコンストラクタで受け取って clazz.getDeclaredConstructor().newInstance() を呼ぶのが定番の回避策
catch (Exception e) で握りつぶす─ 何でもキャッチして catch ブロックが空っぽ ({} だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でも e.printStackTrace(); か業務ログ出力を入れる、というのが鉄則です
stacktrace を捨てる─ catch (Exception e) { throw new RuntimeException("失敗"); } のように、元の e を渡さずに新しい例外を作るパターン。これだと「失敗」とだけ書かれた例外が飛んで、本当の原因 (どのファイルの何行目で何が起きたか) が消えます。必ず throw new RuntimeException("失敗", e); のように原因を引き継ぎましょう
catch の順序を間違える─ 複数の catch を並べるときは、catch (Exception e) のような広いものを後ろに書きます。先頭に書くと、その後ろの catch (NumberFormatException e) は「絶対に到達できないコード」とみなされてコンパイルエラーになります。狭い型から広い型へ、の順番が原則です
Exception を一番上に書いて到達不可コードを作るcatch (Exception e) を最初に置き、その下に catch (NumberFormatException e) を書くと、後者には永遠に到達しません。Java はビルドエラー (exception has already been caught) で止めてくれますが、原因がピンと来ないと数十分溶かしがちです。順序は 具体 → 抽象 が鉄則です
catch でもみ消して原因が分からなくなるcatch (Exception e) { /* 何もしない */ } のようにブロックを空にすると、例外が握り潰されて呼び出し側が異常に気付けません。最低でも System.err.println(e) でログを出すか、別の例外で再 throw するのが鉄則です
finally の中で return してしまうfinally の中で return を書くと、try や catch 内で書いた return が 上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、return は try/catch 側にまとめておくのが安全です
throws を書き忘れて検査例外を投げるFileReader などを使うコードを書いて throws IOException を忘れると、unreported exception IOException; must be caught or declared to be thrown というエラーが出ます。コンパイラのメッセージのとおり、try-catch か throws のどちらかを必ず書きましょう
catch (Exception e) {} で握り潰す例外を catch しただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でも e.printStackTrace() を呼ぶか、throw new RuntimeException(e) のように包み直して再送出するのが鉄則です
throws を書くthrows NullPointerException のように非検査例外を throws 句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadoc の @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() か、構造化されたロガー (SLF4J の logger.error(...)) で出力するクセを付けましょう。投げる例外と捕まえる例外は、必ずペアで設計するのが鉄則です
implements AutoCloseable を忘れるtry (Resource r = ...) の Resource クラスが AutoCloseable を実装していないと、required type that implements AutoCloseable というコンパイルエラーになります。class Resource implements AutoCloseable と必ず書きましょう
close() の中で重い処理や例外を派手に投げるclose() は「後始末」のメソッドです。ここでネットワーク通信をしたり、独自の例外を派手に投げたりすると、デバッグが非常に難しくなります。close() は冪等 (idempotent) に、つまり何回呼んでも安全になるよう書くのが鉄則です
try の () の中で複数リソースを書く順番を間違えるclose() は宣言と逆順で呼ばれるので、依存関係 (たとえば Connection → Statement → ResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSet → Statement → Connection の順に閉じられる、ということです
finally の中で return を書いて例外を握りつぶすtry から飛んできた例外を catch していないのに、finally で return してしまうと、本来呼び出し元に伝わるはずだった例外が消えてしまいます。デバッグ時に「エラーがどこかで握りつぶされている」と気づくのが非常に難しくなるので、finally の中で return するのは原則禁止と覚えておきます
finally で参照する変数を初期化していないtry ブロックの中だけで宣言した変数は、finally のスコープ外です。reader のようなリソース変数は try の 外側 で宣言し、null で初期化しておき、finally で if (reader != null) reader.close(); のようにチェックしてから使うのが定石です
try の最後に書いて、例外時に呼ばれない「reader.close() を try ブロックの最後の行に書けば良い」と思いがちですが、try 中で例外が出るとそこから先は実行されません。close() が呼ばれず、ファイルハンドルやコネクションがリークします。リソース解放は finally、もしくは try-with-resources に必ず移す のが鉄則です
前述の通り、ターミナル操作を呼んだ Stream は閉じます。Stream<Integer> s = ...; s.count(); s.forEach(...); のように 2 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です
Collection と Stream を混同するlist.stream() は新しい Stream を返すだけで、元の list は変わりません。list.stream().filter(...) の結果をどこにも代入せずに、「list から偶数だけ消えたはず」と思い込むのはよくあるミスです。Stream は元データを変更しません
Arrays.stream(arr).filter(n -> n > 0); と書いて「実行された」と思い込んでも、ターミナル操作がないので 何も起きません。これは 遅延評価 のせいで、filter を呼んだだけでは実際の処理が走らないからです。count() collect() forEach() などを最後に必ず付けましょう
filter の中で副作用を書く.filter(n -> { System.out.println(n); return n > 0; }) のようにログ出力やフィールド更新を filter の中に書くのは避けるべきです。filter は純粋に判定だけをするのが約束で、副作用を入れるとデバッグが難しくなり、並列ストリームでは予期しない動作になります。ログを入れたいときは専用の peek を使うか、デバッグ用に一時的に挟むだけにしましょう
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 つにまとめるか、逆に意味の単位で分割するかを意識すると読みやすくなります
null 要素が混ざる可能性を忘れるStream<String> のように参照型を扱う場合、要素に null が混ざることがあります。.filter(s -> s.length() > 0) と書くと、null 要素で NullPointerException が出てしまいます。先に .filter(s -> s != null) で null を弾いてから本来の条件を書く、または .filter(Objects::nonNull) を使うのが定石です
map の中で副作用を書くmap(x -> { System.out.println(x); return x; }) のように、変換のついでに println やフィールド書き換えをするのは Stream の作法に反します。表示やログ出力は peek か終端操作の forEach で行うのがルールです。副作用入りの map は並列ストリームで予期しない順序で動いて、バグの温床になります
IntStream の map は int -> int しか受け付けません。IntStream.of(1, 2, 3).map(x -> "num=" + x) のように String を返そうとすると怒られます。型を変えたいときは mapToObj を使うのが正解です
mapmap(x -> something(x)) の中で null を返すと、後続の処理で NullPointerException が出やすくなります。null を返す可能性があるなら、Optional でラップするか filter(Objects::nonNull) で前処理するのが安全です
identity を省略して Optional を扱い忘れるreduce(BinaryOperator) のように引数 1 つ版もありますが、こちらは空 Stream の可能性があるため戻り値が Optional<T> になります。int product = stream.reduce((a, b) -> a * b); と書いてもコンパイルすら通らず、Optional<Integer> を int に代入できないと怒られます。今回のように空配列でも 1 を返したい場合は、identity を渡す 2 引数版を使うのが圧倒的に楽です
accumulator に副作用を入れるreduce の関数の中で System.out.println を呼んだり、外側のフィールドを書き換えたりするのは禁じ手です。並列化したときに表示順がぐちゃぐちゃになったり、ConcurrentModificationException が出たりして、デバッグ困難なバグの温床になります。accumulator は「a と b を受け取って新しい値を返すだけの純粋関数」に保つ、というのが鉄則です
reduce(0, (a, b) -> a - b) のように引き算で書くと、順次実行では 0 - 1 - 2 - 3 = -6 になりますが、並列で (0 - 1) と (2 - 3) を計算して合成すると結果が変わってしまいます。減算や除算は結合的ではないので、reduce の accumulator には使わないのが原則です。どうしても引き算したいときは、map(x -> -x).reduce(0, Integer::sum) のように加算に変換するか、for ループで書くかを検討します
toMap のキー重複toMap(keyMapper, valueMapper) でキーが重複すると IllegalStateException: Duplicate key が飛びます。マージ方法を決めるには 3 引数版の toMap(k, v, (a, b) -> a) で「重複したら先勝ち」のように指定するか、そもそも groupingBy で List にまとめるのが定石です
joining の delimiter 指定忘れCollectors.joining() を引数なしで呼ぶと区切り文字なしで全部くっつきます。CSV のつもりで joining() と書いてしまうと "246" のようになるので、joining(",") を渡すのを忘れないようにしましょう
toList() の戻り値を変更しようとするJava 16 から増えた Stream.toList() (引数なし) は immutable な List を返します。後で add したいなら Collectors.toList() を使うか、new ArrayList<>(immutableList) で詰め替えてください
sorted(null) のように Comparator に null を渡すJava 8 の仕様では、sorted(Comparator) に null を渡すと NullPointerException が飛びます。sorted() (引数なし) は自然順を意味するので、「自然順がほしい」なら 引数を渡さない ことが正解です。null をデフォルト扱いにしてはいけません
Comparable) を実装していない型で sorted() を呼ぶ自作クラス (たとえば class Point {}) のリストを sorted() (引数なし) で並べ替えようとすると、実行時に ClassCastException: ... cannot be cast to class java.lang.Comparable が出ます。自作クラスを自然順で並べたいなら implements Comparable<Point> を書いて compareTo を実装するか、sorted(Comparator.comparing(...)) を渡してください
Comparator に副作用を持ち込むsorted((a, b) -> { counter++; return a - b; }) のように Comparator の中で外部変数を書き換えると、ソート結果が並列実行や JIT の最適化で予測不能になります。Comparator は「同じ入力に同じ出力を返す純粋関数」として書く、というのが鉄則です
equals / hashCode を実装し忘れる前述のとおり、自作クラスを distinct() に通しても参照比較になり、重複排除されません。record を使うか、IDE の自動生成で equals と hashCode を必ずペアで書きましょう
Stream は一度終端操作を呼ぶと閉じられ、IllegalStateException が出ます。Stream<String> s = list.stream(); を 2 回使い回すのではなく、必要に応じて list.stream() を再度呼び直すのが正解です
distinct() は元の Stream の出現順を維持します。{3, 1, 2, 1, 3} を流すと {3, 1, 2} の順で残ります。並べ替えたいときは別途 sorted() を繋いでください。逆に parallelStream() では順序保証が崩れる場合があるので、順序が重要なら stream() を使うか forEachOrdered を選びます
skip → limit と limit → skip の順序を取り違えるstream.skip(2).limit(3) は「先頭 2 件を捨ててから 3 件取る」(3, 4, 5 番目を取得)、stream.limit(3).skip(2) は「先頭 3 件に絞ってから 2 件捨てる」(結果は 1 件) と挙動が変わります。ページネーションは必ず skip → limit の順番で書きます
limit を付け忘れるStream.iterate や Stream.generate から作った無限 Stream を forEach や count に直接渡すと、プログラムが止まらなくなります。途中で必ず limit か takeWhile などで打ち切りましょう
limit(-1) skip(-1) のように負の引数を渡すlimit も skip も負の値はサポートしておらず、実行時に IllegalArgumentException を投げます。page - 1 を計算するときに、page が 0 や負の値になっていないかを必ず検証してください
parallel() を付ければ順序が保たれていると思い込むforEach の出力順、reduce の集合体の組み立て順は 実行のたびに変わる と思って書く必要があります。順序が必要なら forEachOrdered か Collectors.toList() を選びましょう
非結合的 な reducer を渡してしまう引き算や、文字列を結合順序つきで作る処理は、並列だと結果が変わります。reduce("", String::concat) を並列で回すと、繋がる順番が毎回違うので、最終の文字列がランダムに並び替わったように見えます
parallel() を付けて逆に遅くなる配列の要素数が数百〜数千程度では、並列化のオーバーヘッドのほうが大きく、for のほうが速いことが普通です。並列はデータが多くて、要素 1 個の計算が重いとき だけが効きます
n -> n * 2 は return を書かない、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::parseLong と Integer::parseInt を混同しがちなので、戻り値の型まで揃っているか確認してください
ClassName.new と書く正しくは ClassName::new です。.new は内部クラスのインスタンス生成用の別文法なので、ここで使うと意味が変わってしまいます。::new という記号の並びをそのまま覚えるのが安全です
Function と Consumer を混同するFunction<T, R> は「値を返す」関数、Consumer<T> は「値を受け取って戻り値なし (void) 」の関数です。System.out.println を渡したいだけなら Consumer 、結果を返してほしいなら Function 、と用途で選び分けます。間違って Function<Integer, Void> のような無理な型を作る前に、Consumer の存在を思い出してください
var f = x -> x * 2; のように var でいきなり受けようとすると、コンパイラが「ラムダ式単体では型を決められない」と言ってエラーになります。必ず Function<Integer, Integer> f = x -> x * 2; のように 左辺で型を宣言 するか、メソッド引数として渡してその場で文脈から型を決めるかのどちらかにします
null 引数を素通しさせるFunction<String, Integer> parser = Integer::parseInt; に null を渡すと NullPointerException が出ます。Function 自体は null を弾く仕組みを持たないので、null の可能性があるときは事前に null チェックするか、Optional<T> を組み合わせるかで身を守る必要があります
test の戻り値型を勘違いするPredicate<Integer> を作るときに、つい n -> n * 2 のような「整数を返すラムダ」を書いてしまうケースです。Predicate.test は必ず boolean を返す約束なので、n -> n * 2 は Predicate<Integer> としては成立せず、incompatible types というコンパイルエラーになります。整数を別の整数に変換したいときは Predicate ではなく Function<Integer, Integer> を使うのが正解です
null 判定を手書きしすぎる参照型を扱う Predicate<String> などでは、要素に null が混ざることがあります。素直に s -> s != null && s.length() > 0 と書いてもよいのですが、null チェック専用のメソッド参照 Objects::nonNull が java.util.Objects に用意されています。Predicate<String> notNull = Objects::nonNull; のように書くと、意図が一発で伝わるのでおすすめです
negate の順序を間違えるisPositive.and(isEven).negate() と isPositive.and(isEven.negate()) は、見た目は似ていますが意味がまるで違います。前者は「正かつ偶数」の否定 (= 負または奇数)、後者は「正かつ奇数」です。negate がどの Predicate にかかっているかを意識して、必要なら (...) で明示するか、いったん変数に入れて段階的に組み立てるのが安全です
Consumer の中で return を書いてしまうConsumer<String> c = s -> { return s.length(); }; のように戻り値を書くとコンパイルエラーになります。accept の戻り値は void なので、ラムダの中も値を返してはいけません。値を返したいのは Function<T, R> の役割です
Supplier.get() を毎回呼ぶのに副作用付きのラムダを書いてしまうSupplier<Integer> next = () -> counter++; のように、内側でフィールドを書き換える Supplier は危険です。同じ Supplier を get() するたびに別の値が返ってくるので、ライブラリ側がいつ呼ぶかわからない (orElseGet Stream.generate 等) 文脈で予測不能なバグになります。Supplier は基本的に副作用なしで書きましょう
hello.get のようにかっこを忘れて参照だけ書くhello は Supplier<String> ですが、String s = hello; と書くと型が違うとエラーになります。値を取り出すには必ず hello.get() のようにかっこ付きで get() を呼びます。Consumer も同様で、呼び出すときは printer.accept("x") の形が必要です
BiFunction<Integer, Integer> のように 2 個しか書かないと「型パラメータが足りない」エラーになります。BiFunction は引数 2 個 + 戻り値 1 個の 合計 3 個 が必須です。逆に Function<T, R> は 2 個、Predicate<T> は 1 個、と数が違うので、ペアで覚えるのがコツです
BiFunction で書こうとする標準ライブラリには BiFunction までしかありません。TriFunction を import しようとして cannot find symbol で立ち止まる人が多いですが、これは 自作するか、引数をまとめる のが正解です。java.util.function.TriFunction というクラスは存在しません
Function と混同するFunction<Integer, Integer> f = (x, y) -> x + y; は文法エラーです。Function は引数 1 個なので、ラムダも (x) -> ... か x -> ... の形である必要があります。2 引数を受け取りたいなら型を BiFunction に変えるか、ラムダの引数を 1 つにします
%d に String を渡してしまうString.format("%d", "100") のように、文字列を %d の位置に入れると、IllegalFormatConversionException という実行時例外が飛びます。整数なら %d、文字列なら %s、と型を必ず合わせましょう。逆に %s には int でも double でも何でも入れられる (自動で toString() される) ので、迷ったときの保険として %s は便利です
\n と %n を取り違える改行を入れたいとき、\n は環境にかかわらず常に LF (0x0A) になりますが、%n は OS ごとの改行 (Windows なら CRLF、Linux/macOS なら LF) に展開されます。ログや帳票を Windows でも Linux でも揃えたいときは %n を、HTML などプラットフォーム非依存で必ず LF にしたいときは \n を、というふうに使い分けます
%.2f の丸めは HALF_UP(いわゆる四捨五入)です。ただし double/float は 0.1 や 0.2 などを二進浮動小数点で正確に表現できないため、表示する前の値そのものに微小な誤差が乗ります。お金の計算など厳密な精度が必要なときは BigDecimal と setScale(2, RoundingMode.HALF_UP) を使うのが安全です。String.format の %.2f はあくまで「画面表示用の見た目を整える」ためのもの、と割り切りましょう
toString() の呼び忘れString result = sb; と書くとコンパイルエラーです。String result = sb.toString(); のように明示的に呼び出してください。println のように Object#toString を内部で使ってくれるメソッドなら不要ですが、String 型の変数に入れるときや、API の引数として渡すときは必ず変換が必要です
+ 連結を 1 回だけする場合に StringBuilder を持ち出すString full = first + " " + last; のように、定数回 (2〜3 回) の連結なら、コンパイラが自動で StringBuilder 相当のコードに変換してくれます。乱用するとかえって読みにくくなるので、for while などのループ内で繰り返し連結するときに使うのが本来の使いどころです
new StringBuilder() のデフォルト容量は 16 文字です。1 万文字以上を作ると分かっているなら、new StringBuilder(10_000) のように指定しておくと内部配列の再確保が発生せず高速です。性能を気にする場面では覚えておきましょう
\\ のエスケープ忘れJava の文字列で \d \w \s を書きたいときは必ず \\d \\w \\s のようにバックスラッシュ 2 つです。1 つしか書かないとコンパイルエラー (illegal escape character) になります
matches を部分一致だと思い込むString.matches は 完全一致 です。"foo bar".matches("foo") は false を返します。部分的に含まれているかを調べたいなら s.contains("foo") を使うのが手軽です
greedy と lazy の混同* + ? はデフォルトで greedy (できるだけ長くマッチ) です。最小マッチにしたいときは *? +? のように ? を後ろに付けます。今回の課題のような単純な完全一致では気にしなくて構いません
split の引数に正規表現特殊文字をそのまま渡す"." "|" "?" などはエスケープしないと意図どおりに動きません。split("\\.") split("\\|") のようにバックスラッシュ 2 つでエスケープするか、Pattern.quote を使いましょう
split した結果を空配列だと思い込む"".split(",") は要素 1 個の配列 [""] を返します。空入力に対して空文字列を返したいときは、csv.isEmpty() で先に判定するのが安全です
"a,b,,".split(",") は 2 要素しか返しません。CSV の列数を厳密に揃えたいときは split(",", -1) を使って末尾の空要素を保持してください
java.util.Date を使ってしまうIDE の自動 import で Date を選ぶと、java.util.Date が候補に出ます。今からの新規コードでは必ず java.time.LocalDate の方を選びましょう。import 文を確認する癖をつけると安全です
0 始まりだと思い込むCalendar 時代の名残で「5 月 は 4」と書いてしまう人がいます。LocalDate.of(2026, 5, 19) の 5 はそのまま 5 月 です。0 を渡すと DateTimeException が出ます
date.plusDays(1); と書いても、戻り値を受け取らないと意味がありません。LocalDate は不変なので、必ず date = date.plusDays(1); のように再代入するか、別の変数に受けてください
yyyy と YYYY を取り違える小文字 yyyy が普通の暦年 (2026)、大文字 YYYY は week-based-year という年またぎ週の特殊な年を表します。たとえば 2025-12-29 (月) のように年末が月曜から始まる週だと、YYYY は 2026 を返すことがあります。請求書や領収書を作るときに混入すると、「なぜか 1 件だけ翌年扱い」というバグになるので、年 を出したいときは必ず小文字 yyyy を使います
MM (月) と mm (分) を取り違えるyyyy-mm-dd hh:MM のように書いてしまうと、月のはずの位置に分が、分のはずの位置に月が入り、2026-30-19 02:05 のような壊れた文字列ができあがります。日本語で書くと「月は大文字 MM、分は小文字 mm」、と語呂で覚えるのが安全です
SimpleDateFormat を使ってしまう旧 API の java.text.SimpleDateFormat は今でも import できますが、スレッドセーフでないため、Servlet や Spring のように複数リクエストが同時に走る環境で共有変数として持つと、たまに壊れた文字列を返すという最悪のバグになります。新規コードでは必ず DateTimeFormatter を使い、SimpleDateFormat は触らないのが鉄則です
Duration に年や月の長さを聞かないDuration は最大単位が「日」ですが、toDays() の戻り値も「秒数を 86400 で割った値」にすぎず、Duration 自体は「年 / 月」の概念を持っていません。Duration.ofDays(60).toMonths() のようなメソッドはなく、書こうとしてもコンパイルエラーになります。年・月単位の差を取りたいときは Period または ChronoUnit.MONTHS.between を使いましょう
Period で秒や時間を扱おうとする逆に、Period.between(localTime1, localTime2) は 型エラー になります。Period は LocalDate 専用で、LocalTime や LocalDateTime を渡せません。「ストップウォッチ的な短い時間」を測りたいときは必ず Duration を使います
between の引数順を逆にして符号がおかしくなるChronoUnit.DAYS.between(start, end) は end - start を返します。start のほうが未来だと 負の値 になります。「絶対値が欲しい」のか「向きを保ちたいのか」を意識して書きましょう。Math.abs(...) で絶対値を取るのは簡単ですが、「過去か未来か」の情報を捨てる前に、本当にそれでいいかを一度立ち止まって考えるのが大事です
yyyy と YYYY を混同する小文字の yyyy は普通の暦年ですが、大文字の YYYY は 週ベースの年 を意味します。年末年始をまたぐ ISO 週で結果がズレるため、Windows のメモ帳で書いた仕様書をコピペしたら 1 月 1 日が 2027 年扱いになる、というバグが起きやすいです。日付 を出すときはほぼ常に小文字の yyyy が正解です
MM と mm の取り違え大文字の MM は月、小文字の mm は分。これを逆に書くと、yyyy/mm/dd というパターンが「年/分/日」と解釈されます。LocalDate には時刻情報がないため分は常に 0 となり、2026/00/19 のような意図しない出力になります。コンパイルエラーにならない分、見つけにくい不具合です
HH と hh の取り違えHH/hh は LocalDateTime や LocalTime など時刻を持つ型で使うパターンです。LocalDate には時刻フィールドがないため、このミスが実害として現れるのは時刻を扱う型が対象のときです。24 時間制で表示したいのに hh と書くと 12 時間制 (1〜12) になります。午後 3 時が 03 と表示されてしまい、AM/PM の区別 (a パターン) を入れ忘れると 03:00 が午前か午後か分からなくなります。業務アプリでは原則 HH を使うと覚えてください
today.plusDays(10); と書いただけで、戻り値を変数に代入し忘れるのが一番多いミスです。LocalDate は不変なので、戻り値を捨てると計算結果はどこにも残りません。必ず LocalDate later = today.plusDays(10); のように受け取りましょう
5/30 に 5 日足したいから「日 = 30 + 5 = 35、5 月は 31 日までだから 4 を引いて 6/4」のような計算は不要です。plusDays(5) に任せれば一発です。月またぎ・年またぎ・うるう年は API がすべて面倒を見てくれます
Calendar 時代の感覚で today.plusDays(10) を呼んだら today 自身が変わると思い込むパターンです。String の replace と同じく、LocalDate の plus* minus* with* はすべて新しいインスタンスを返します。元のオブジェクトは絶対に変わりません
close() を呼び忘れる / 古い書き方をするnew FileWriter(...) 系の古い API を使うと、finally で close() を書かないとリソースが残ります。try-with-resources (try (var w = new FileWriter(...)) { ... }) を必ず使うか、いっそ Files.writeString に置き換えるのが正解です
古い new FileWriter("a.txt") は OS のデフォルトエンコーディングで書き込むので、Windows (Shift_JIS) と Linux (UTF-8) で結果が変わることがあります。Files.writeString は常に UTF-8 なので安全です
IOException を握りつぶすファイル操作は失敗する可能性が常にあります (権限・容量・パスの存在)。catch (IOException e) {} のように空ブロックで握りつぶすと、トラブルが起きても原因が分からなくなるので、必ず e.printStackTrace() か throw new RuntimeException(e) で見える形にしましょう
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()) で判定するのが正解です
String の + でパスを連結するdir + "/" + name のように書くと Windows では / と \ が混ざり、name 側に余計な / が含まれていると // ができます。必ず dir.resolve(name) を使いましょう
"C:\\Users\\taro" のような書き方は Linux で動きません。マルチプラットフォームで動かしたいコードでは Paths.get("C:", "Users", "taro") のように分割して渡すか、設定ファイルから読み込みます
resolve と + を同じ感覚で使うresolve に絶対パスを渡すと「左辺を捨てて絶対パスを返す」という挙動になります。これは仕様であり、+ の単純連結とは違うので注意してください。期待しない上書きを避けたい場合は、事前に child.isAbsolute() を確認します
Stream を閉じ忘れるFiles.lines(p).mapToInt(...).sum() のようにワンライナーで書いてしまうと、Stream が close されずファイルディスクリプタがリークします。少量なら気づきにくいですが、ループの中で何回も呼ぶと「Too many open files」エラーで突然落ちます。必ず try (Stream<String> lines = Files.lines(p)) { ... } の形にしましょう
NumberFormatExceptionInteger.parseInt("") は実行時例外を投げます。改行コードの違いや末尾改行で空文字列が混ざることがあるので、filter(s -> !s.isEmpty()) を挟むか、s -> s.trim().isEmpty() のように空白だけの行も弾く処理を入れると安心です
Files.lines(p) は引数 1 つだと UTF-8 を仮定します。日本語を含む Shift_JIS のファイルなどを読もうとすると MalformedInputException が出ます。明示的に指定したいときは Files.lines(p, StandardCharsets.UTF_8) のように 2 引数版を使い、入力が Shift_JIS なら Charset.forName("MS932") を渡します
exists の戻り値を盲信する (TOCTOU)Files.exists(p) で true を取った直後に別のプロセスがファイルを消すと、続く Files.delete(p) が NoSuchFileException を投げます。これは Time Of Check / Time Of Use の頭文字を取って TOCTOU 問題と呼ばれ、確実に避けたいケースでは Files.deleteIfExists(p) を使うか、例外を try で受け止めるのが定石です
Files.delete と Files.deleteIfExists を混同する前者はファイルが無いと NoSuchFileException、後者は無くても静かに false を返します。「無いなら無いで OK」のときは後者、「絶対あるはず」のときは前者、と用途で使い分けましょう
Files.delete でディレクトリを消そうとして失敗する中身が残っているディレクトリは DirectoryNotEmptyException で消せません。再帰的に消したいときは Files.walk(p) で全ファイルを取り出してから逆順に Files.delete する、もしくは Files.walkFileTree を使うのが定石です
extends と implements の混同クラスを継承するときは extends、インタフェース を実装するときは implements を使います。class Dog implements Animal のように書いてしまうと、Animal がクラスの場合はコンパイルエラーになります。逆に interface Walker を extends しようとしてもエラーです。「クラスから機能を引き継ぐなら extends、契約だけ約束するなら implements」と覚えましょう
Object を継承していることを忘れるJava では extends を書かないクラスでも、自動的に java.lang.Object を継承しています。だから toString() や equals() や hashCode() がどんなクラスでも呼べるわけです。Animal extends Object と明示しても同じ意味ですが、わざわざ書かないのが普通です
final クラスを継承しようとするString Integer LocalDate などの標準クラスや、自分で final class と宣言したクラスは継承できません。class MyString extends String と書くと cannot inherit from final class String というコンパイルエラーになります。継承させたくないクラスにはあえて final を付ける、というのも設計上のテクニックです
super(...) を 2 行目以降に書いてしまうsuper(...) と this(...) は コンストラクタの 1 行目 に限ります。前にログ出力やバリデーションを入れたくなりますが、それをするとコンパイルエラーです。どうしても前処理したいときは static メソッドに切り出して、super(prepare(arg)) のように引数の中で呼びましょう
super なしの自己呼び出しをしてしまうChild.greet() の中で greet() と書くと、this.greet() 扱いで自分自身が呼ばれ、無限再帰で StackOverflowError になります。親を呼びたいなら必ず super.greet() と書くこと
final クラス / メソッドを継承しようとするString Math などの final クラスは extends できません。エラーメッセージ cannot inherit from final が出たら、設計を見直して「継承」ではなく「コンポジション (フィールドとして持つ)」に切り替える方が筋がよいケースが多いです
@Override を忘れてオーバーロードになる─ 親が area() なのに子で area(int x) と書いてしまうと、別メソッドが追加されただけで親の area() はそのまま生きてしまいます。Shape s = new Circle(10); s.area(); を呼んでも親の 0 が返ってきて「なんで動かないんだ?」と悩むパターンです。 @Override を付けておけばコンパイラが「親に一致するメソッドがない」と弾いてくれます
─ 親の public int area() を子で protected int area() と書くと、コンパイルエラーになります。これは前述のルールどおりで、「子は親より広く、または同じ」可視性しか許されません。逆向き (protected → public) はオッケーです
static メソッドを「オーバーライド」しているつもり─ 親と子で同名の static メソッドを書くと、文法上は通りますがこれは オーバーライド ではなく 隠蔽 (hiding) と呼ばれる別の現象です。動的ディスパッチ の対象外で、呼び先はコンパイル時の型で決まります。 Shape.foo() と Circle.foo() がそれぞれ独立に存在し、Shape s = new Circle(); s.foo(); は親の foo を呼んでしまいます。 static メソッドは オーバーライド できないと覚えておきましょう
abstract メソッドに本体を書いてしまうpublic abstract double area() { return 0; } のように { } を付けるとコンパイルエラーです。エラーメッセージは abstract methods cannot have a body などになります。本体を書きたいなら abstract を外し、サブクラスで @Override するスタイルにします
new しようとするnew Shape("hoge") はコンパイルエラーです。設計図を直接実体化しようとしているサインなので、Triangle や Circle のような具象サブクラスを new し、変数の型だけを Shape にする (Shape s = new Triangle(4, 5);) のが正解です
final と abstract を同時に書くfinal は「これ以上継承させない」、abstract は「サブクラスで上書きさせる」という真逆の意味を持つので、同じクラス/メソッドに付けるとコンパイルエラーになります。どちらか一方だけにします
extends と implements の取り違えクラスがクラスを継承するときは extends、クラスが interface を実装するときは implements です。class MyRunner extends Runnable2 と書くと「Runnable2 クラスを継承しようとしている」と解釈され、Runnable2 が interface の場合は no interface expected here 系のエラーになります
interface のフィールドを変数のつもりで書くinterface の中に int count = 0; と書くと、Java は黙ってそれを public static final int count = 0; と解釈します。インスタンスごとの状態は持てず、後から代入もできません。状態を持たせたいなら interface ではなく abstract class を選びましょう
public 以外で書く契約上のメソッドは暗黙的に public abstract 扱いなので、実装側を private や package-private (修飾子なし) にすると「アクセス修飾子を弱められない」エラーになります。@Override public String name() の形を体に染み込ませてください
default だらけで interface が肥大化する便利だからといって、振る舞いの 90% を default で書いてしまうと、もはや interface ではなく abstract class のような重い存在になります。interface は「実装すべきメソッドの契約」を表すのが基本なので、default は「後から追加する後方互換ヘルパー」「複数の実装が共通して使う簡単な定型処理」に絞るのがコツです
2 つの interface が同じ default を持つと、コンパイルエラーが必ず出ます。Greeter.super.greet(name) 構文を覚えて、衝突したときは実装クラス側で明示的に override する習慣を付けましょう。エラーメッセージは class X inherits unrelated defaults のように出ます
abstract class と混同して フィールド を持たせようとするinterface には public static final の定数しか書けません。private int count; のようにフィールドを書こうとするとコンパイルエラーになります。状態を持ちたいなら abstract class を選ぶか、実装クラス側にフィールドを置いてください
instanceof と equals を混同するinstanceof は「型が同じ (またはサブクラス) か」を見る演算子で、equals は「値が等しいか」を見るメソッドです。"hello" instanceof String は true ですが、"hello".equals("hello") も true、意味はまったく違います。型を見たいときは instanceof、値を見たいときは equals、と覚えてください
null に対する instanceof は常に falsenull instanceof String も (Object) null instanceof Object も、結果はすべて false です。これは Java の仕様で、null には実型が存在しないためです。null を扱いたい場合は if (o == null) { ... } のように別途分岐するか、Java 21 なら switch の case null を使います
ClassCastExceptioninstanceof で確認せずにいきなり (String) o のようにキャストすると、実型が違っていれば実行時に ClassCastException が飛びます。パターンマッチングを使えばこのミスは構造的に防げるので、Java 16 以降のコードベースでは積極的に切り替えましょう
Shape s = new Circle(2); のあと s.area(); を呼ぶのに、((Circle) s).area(); のように書く必要はありません。子型に固有のメソッドを呼びたいときだけ ダウンキャスト が必要です。共通メソッドを呼ぶだけなら親型のままで十分です
static メソッドはポリモーフィズムの対象外static メソッドはクラスに紐づくため、@Override ではなく 隠蔽 (hiding) されるだけです。Shape s = new Circle(2); s.staticMethod(); と書いても、呼ばれるのは変数型 Shape の static メソッドです。インスタンスメソッドだけが動的ディスパッチの対象だと覚えてください
final メソッドはオーバーライド禁止親クラスのメソッドに final が付いていると、子クラスで @Override しようとした瞬間にコンパイルエラーになります。同様に final class 自体は継承できません。「これ以上拡張させたくない」という意思表示の機能なので、ライブラリ作者が意図的に付けることが多いです