raw 型で書いてしまう
─ Box box = new Box("hello"); のように <> を省略すると raw 型 扱いになり、getValue() の戻り値が Object になってしまいます。コンパイラは警告を出すだけで止めてはくれませんが、現代の Java では絶対に書かないのが鉄則です
レッスンの本文で注意している点を、つまずきの側から42件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。
─ 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() を呼ぶのが定番の回避策
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 自体は継承できません。「これ以上拡張させたくない」という意思表示の機能なので、ライブラリ作者が意図的に付けることが多いです