Java中級:ジェネリクス・継承・ポリモーフィズムのよくある間違い

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

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

レッスンを読む

raw 型で書いてしまう

─ 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> を書く「ジェネリックメソッド」という別の仕組みを使います

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

ジェネリックメソッド

レッスンを読む

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

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> を作る

レッスンを読む

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

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

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

equals を書かずに HashSet HashMap で使う

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

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

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

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

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

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

レッスンを読む

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

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

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

extends で継承する

レッスンを読む

extends と implements の混同

クラスを継承するときは extends、インタフェース を実装するときは implements を使います。class Dog implements Animal のように書いてしまうと、Animal がクラスの場合はコンパイルエラーになります。逆に interface Walker を extends しようとしてもエラーです。「クラスから機能を引き継ぐなら 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() と書くと、コンパイルエラーになります。これは前述のルールどおりで、「子は親より広く、または同じ」可視性しか許されません。逆向き (protected → public) はオッケーです

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

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

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

final と abstract を同時に書く

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

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

interface で契約を定義

レッスンを読む

extends と implements の取り違え

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

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

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

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

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

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

契約上のメソッドは暗黙的に public abstract 扱いなので、実装側を private や package-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 と型パターン

レッスンを読む

instanceof と equals を混同する

instanceof は「型が同じ (またはサブクラス) か」を見る演算子で、equals は「値が等しいか」を見るメソッドです。"hello" instanceof String は true ですが、"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 なら switch の case 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(); と書いても、呼ばれるのは変数型 Shape の static メソッドです。インスタンスメソッドだけが動的ディスパッチの対象だと覚えてください

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

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

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

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