Javaの型推論(var)と継承に関する注意点を徹底解説!初心者でも理解できる基本知識
生徒
「Javaでvarを使ったらエラーが出ました。どうしてですか?」
先生
「varは型推論を行う便利な機能ですが、いくつか制約があります。特に継承を扱うときには注意が必要です。一緒に見ていきましょう!」
生徒
「なるほど、継承のときにどう注意すればいいのか知りたいです!」
先生
「それでは、具体例を使って説明していきますね。」
1. var型と型推論の仕組み
varはJava 10で導入されたローカル変数の型推論機能です。varを使うと、右辺の値から型を推論して変数を宣言できます。
public class VarExample {
public static void main(String[] args) {
var number = 10; // 推論される型はint
System.out.println(number); // 出力: 10
}
}
このように便利な機能ですが、型推論はコンパイル時に行われるため、変数の型が固定されます。これが後述する継承に関する問題の原因になります。
2. 継承と型推論の注意点
次に、継承を含むコードでvarを使った場合の注意点を説明します。以下のコードを考えてみましょう。
class Parent {
public void display() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
public void display() {
System.out.println("Child");
}
}
public class VarInheritanceExample {
public static void main(String[] args) {
var obj = new Parent();
obj = new Child(); // エラー: 型が固定されている
obj.display();
}
}
このコードでは、varによって推論された型がParentに固定されるため、Childのインスタンスを代入しようとするとコンパイルエラーが発生します。
正しい解決方法
継承を扱う場合は、明示的に親クラスやインターフェースの型を指定する必要があります。
public class CorrectInheritanceExample {
public static void main(String[] args) {
Parent obj = new Parent();
obj = new Child(); // OK
obj.display(); // 出力: Child
}
}
このように型を明示的に指定することで、継承を活用した柔軟なコードを書くことができます。
3. 試験対策: var型推論の基本ルール
試験や実務で役立つvarの基本ルールをまとめました。
- 型推論はコンパイル時に行われ、右辺の値から型を推論します。
- 推論された型は変数の宣言時に固定されます。
- 継承を使用する場合、
varではなく親クラスやインターフェースの型を明示的に指定するのが一般的です。 varはローカル変数専用であり、フィールドやメソッド引数には使用できません。
これらのポイントを押さえておけば、varを安全かつ効果的に利用できます。
4. varが使えないケースとよくあるエラー
varは便利ですが、どこでも使えるわけではありません。特に初心者がつまずきやすい「使えないケース」を知っておくと、エラーの原因がすぐに分かるようになります。
varはローカル変数にしか使えません(フィールドや引数には使えない)。- 右辺がない宣言(初期化なし)はできません。
nullだけを代入して型推論することはできません。- 配列の要素型を省略した書き方では推論できないケースがあります。
たとえば、次のような書き方はコンパイルエラーになります。
public class VarErrorExample {
// var field = 10; // エラー: フィールドには使用できない
public static void main(String[] args) {
// var x; // エラー: 初期化が必要
// var y = null; // エラー: nullだけでは型推論できない
var number = 10; // OK: intに推論される
System.out.println(number);
}
}
このように、varは「右辺の情報から型を決める」仕組みなので、右辺が曖昧だと推論できずエラーになります。
5. varとポリモーフィズムの関係
継承のメリットのひとつに「ポリモーフィズム(多態性)」があります。親クラス型で受け取り、実体は子クラスとして動作させることで、柔軟な設計ができます。
ただし、varを使うと「宣言時の右辺」で型が決まるため、ポリモーフィズムを意識したコードでは注意が必要です。
class Parent {
public void display() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
public void display() {
System.out.println("Child");
}
public void childOnly() {
System.out.println("Child Only");
}
}
public class VarPolymorphismExample {
public static void main(String[] args) {
Parent p = new Child(); // 親型で受け取る(ポリモーフィズム)
p.display(); // 出力: Child
// p.childOnly(); // エラー: Parent型には存在しない
var v = new Child(); // 推論される型はChild
v.display(); // 出力: Child
v.childOnly(); // OK: Child型なので呼び出せる
}
}
このように、varは宣言時点で具体クラス(例: Child)に推論されるため、結果として「子クラス固有のメソッドが呼べてしまう」ケースもあります。
設計として親型で扱いたい場合は、varを使わずに親クラスやインターフェース型で宣言するのが安全です。
6. インターフェースとvarを組み合わせるときの注意点
継承だけでなく、実務ではインターフェースを使った設計もよく登場します。ここでもvarは「右辺の具体クラス」で型が固定されるため、意図とズレないように注意しましょう。
interface Service {
void execute();
}
class ServiceImpl implements Service {
@Override
public void execute() {
System.out.println("ServiceImpl");
}
public void implOnly() {
System.out.println("Impl Only");
}
}
public class VarInterfaceExample {
public static void main(String[] args) {
Service s = new ServiceImpl(); // インターフェース型で受け取る
s.execute(); // 出力: ServiceImpl
// s.implOnly(); // エラー: Service型には存在しない
var v = new ServiceImpl(); // 推論される型はServiceImpl
v.execute(); // 出力: ServiceImpl
v.implOnly(); // OK: 実装クラス型なので呼べる
}
}
インターフェース型で受け取る設計にしている場合、varを使うと実装クラス型になり、実装依存のメソッドを呼べてしまいます。
「インターフェースとして扱う」ことが目的なら、varではなくインターフェース型を明示して宣言するのが基本です。
まとめ
この記事では、Javaの型推論機能varを継承と組み合わせた際に注意すべき点について詳しく解説しました。varは非常に便利な機能ですが、継承のような複雑な場面では型推論の仕組みを正しく理解することが重要です。
特に、varで宣言された変数はコンパイル時に型が固定されるため、親クラスと子クラス間で代入を行う場合には注意が必要です。以下は今回学んだ主なポイントです。
- 型推論はコンパイル時に決定する: 変数の型は右辺の値から決定され、その後変更できません。
- 継承を活用する場合: 明示的に親クラスやインターフェースの型を指定することで、柔軟な代入が可能になります。
- varの適用範囲: ローカル変数専用であり、フィールドやメソッド引数では使用できません。
以下はまとめとして、正しい型の扱い方を示すサンプルコードです。
class Animal {
public void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Meow");
}
}
public class InheritanceExample {
public static void main(String[] args) {
Animal animal = new Dog();
animal.sound(); // 出力: Bark
animal = new Cat();
animal.sound(); // 出力: Meow
}
}
このコードでは、親クラスAnimalを明示的に型として指定することで、子クラスDogやCatのインスタンスを柔軟に扱うことができています。
生徒
「varが便利だけど、継承と一緒に使うときには注意が必要なんですね!」
先生
「その通りです。特に型が固定される性質を理解していないと、思わぬエラーが発生することがあります。」
生徒
「親クラスを型として指定する方法を知って、より柔軟にコードが書けるようになりました!」
先生
「それは良かったですね。今回の知識を活用して、さらに複雑なコードにも挑戦してみてください。」
生徒
「はい、次回も楽しみにしています!」