継承、およびJavaのような言語でのメンバー/属性およびメソッドへの動的アクセス


7

JavaのようなOOプログラミング言語での継承について質問があります。メソッドとその呼び出しをコンパイルする方法を説明したとき、それは私のコンパイラクラスで思い付きました。コンパイルするソース言語の例としてJavaを使用していました。

次に、このJavaプログラムについて考えてみましょう。

class A {
  public int x = 0;
  void f () { System.out.println ( "A:f" ); } }

class B extends A {
  public int x = 1;
  void f () { System.out.println ( "B:f" ); } }

public class Main {
  public static void main ( String [] args ) {
    A a = new A ();
    B b = new B ();
    A ab = new B ();

    a.f();
    b.f();
    ab.f();
    System.out.println ( a.x );
    System.out.println ( b.x );
    System.out.println ( ab.x ); } }

実行すると、次の結果が得られます。

A:f
B:f
B:f
0
1 
0

興味深いのは、動的abなstatic型のオブジェクトで発生するケースです。プリントアウトとしてABab.f()

B:f

つまり、メソッドの呼び出しは、メソッドの呼び出しに使用されたオブジェクトのコンパイル時のタイプの影響を受けません。ただし、が System.out.println ( ab.x )出力される0ため、メンバーアクセスはコンパイル時の型の影響を受けます。

学生はこの違いについて質問しました:メンバーのアクセス方法とメソッドは互いに一致してはいけませんか?「それがJavaのセマンティクスだ」よりも良い答えを思いつくことができませんでした。

メンバーとメソッドがこの意味で異なるのは、はっきりとした概念的な理由を知っていますか?生徒に何か教えてもらえますか?

編集:さらなる調査の結果、これはJavaの特異性のようです:C ++とC#の動作は異なります。たとえば、以下のSaeed Amiriのコメントを参照してください。

編集2:対応するScalaプログラムを試してみました。

class A {
  val x = 0;
  def f () : Unit = { System.out.println ( "A:f" ); } }

class B extends A {
  override val x = 1;
  override def f () : Unit = { System.out.println ( "B:f" ); } }

object Main {
  def main ( args : Array [ String ] ) = {
    var a : A = new A ();
    var b : B = new B ();
    var ab : A = new B ();
    a.f();
    b.f();
    ab.f();
    System.out.println ( "a.x = " + a.x );
    System.out.println ( "b.x = " + b.x );
    System.out.println ( "ab.x = " + ab.x ); } }

そして驚いたことに、これは

A:f
B:f
B:f
a.x = 0
b.x = 1
ab.x = 1

overrise修飾子が必要であることに注意してください。ScalaはJVMにコンパイルされ、さらに、Scalaコンパイラ/ランタイムを使用してJavaプログラムを最上部でコンパイルおよび実行すると、Javaプログラムのように動作するので、これは私を驚かせます。


3
これはプログラミング言語に関連し、変数の初期化をどのように扱うか、C#で行う場合:ideone.com/q6KpKk別の結果が得られ、問題はオーバーライドさfx、明示的にこれを言及せずに、IMO C#バージョンの方が優れています(これを行う代わりに、OOの観点でより受け入れやすい)を仮想として定義するとxfそれらをでオーバーライドすることにより、整合性の結果が得られますB。しかし、Stackoverflowでこれを尋ねると、より多くの注目が集まり、きっと素晴らしい答えになるでしょう。

@SaeedAmiriはC#に感謝します。C#でも同じだと思っていました。ここで良い答えが得られない場合は、Stackoverflowにクロスポストします。
Martin Berger

クロスポストするときはリンクを貼ってください。
Nejc 2012年

1
@SaeedAmiri:Javaのフィールドを上書きすることはできません。影を付けることができるのはフィールドだけです。
Dave Clarke

私は間違っているかもしれませんが、Javaは「メソッドのオーバーライド」のみを許可し、「フィールドのオーバーライド」はサポートされていないことを最近読みました。したがって、クラスAのインスタンスを作成すると、フィールドに対して、クラスAで見つかるエンティティxの最も近い定義、つまり結果が検索されます。私がここで間違っている場合、人々は私を修正することができます。
Rajat Saxena

回答:


6

これはJavaのフィールドのシャドウイングと密接に関連していると思います。

派生クラスを書くとき、あなたの例のように、基本クラスののx定義を隠すフィールドを書くことができますx。つまり、派生クラスでは、元の定義にはnameを介してアクセスできなくなりますx。Javaがこれを許可しているのは、派生クラスの実装が基本クラスの実装の詳細にあまり依存しないため、脆弱な基本クラスの問題が(部分的にのみ)回避されるためです。たとえば、基本クラスに元々フィールドがなかったxが、その後にフィールドが導入された場合、Javaのセマンティクスは、これが派生クラスの実装を壊すことを避けます。

変数x派生クラスでは、基本クラス内の変数とは完全に異なる型を持つことができます-私はこれをテストし、それが動作します。これは、型が十分に互換性がある必要がある(別名、同じ)メソッドのオーバーライドとはまったく対照的です。したがってA、あなたの例にタイプの変数がある場合、派生できる唯一のタイプは、クラスA(またはそれ以上)で指定されたタイプです。これはクラスBで宣言されたタイプとは異なる場合があるためx、クラスからフィールドの値を返すのはタイプセーフのみAです。(当然のことながら、クラスのフィールドのタイプに関係なく、セマンティクスは同じでなければなりませんB。)


こんにちは@DaveClarkeあなたの説明は、なぜab.x返す必要がありますAの属性はx説得力があります。しかし、そのまったく同じ理由は、型のメンバーにも当てはまりA、実際にC#とC ++はのAメソッドfを呼び出しab.f()ます。
Martin Berger、

2
歴史的に、C ++は動的(仮想)と静的ディスパッチの両方を許可していました。Java設計者はこれを複雑すぎると見なし、EiffelやSmalltalkなどの言語では、メソッドディスパッチのセマンティクスのみをサポートしていました。繰り返しますが、これはより単純でより自然なことです。
Dave Clarke

@DaveClarke様こんにちは。すべてのメソッドの仮想化がより簡単であることに同意します。しかし、メンバーも仮想化されていれば(さらには任意の型を持つことができなければ)、さらに均一になります。結局のところ、メソッドは高次のメンバーと見なすことができます。これが正しいことだと言っているわけではありませんが、メソッドとメンバーをこのように大幅に区別するための、概念的な理由が欠けているのではないかと思います。
Martin Berger、

最終的にメンバー/フィールドを仮想化しても、実際には何の意味もありません。xクラス階層全体に対して1つのフィールドがあります。オーバーライドするとはどういう意味xですか?単にそれに新しい値を与えるためです。だから問題は本当に、なぜJavaはすべての仮想メソッドフィールドのシャドーイングを選択したのですか?
Dave Clarke

しかし、上のScalaの例である@DaveClarkeは、Scalaが実際にメンバーを仮想化していることを示しているように見えます(overrideタイプ可能性を実現するにはメンバーが仮想化する必要があります。このスペースで?
マーティン・バーガー

4

Javaでは、「オブジェクトの静的タイプ」はありません-オブジェクトのタイプがあり、参照のタイプがあります。すべてのJavaメソッドは「仮想」(C ++の用語を使用するため)です。つまり、オブジェクトのタイプに基づいて解決されます。そしてところで。'@Override'は、コンパイルされたコードにはまったく影響を与えません。注釈が付けられたメソッドがスーパークラスのメソッドをオーバーライドしない場合、コンパイラエラーを生成するのは単なる保護手段です。

一方、フィールドアクセスが動的に解決されることはありません。言い換えると、Javaはフィールドにアクセスするときにポリモーフィズムを使用しません。サブクラスにスーパークラスフィールドと同じ名前のフィールドxがある(病理学的)場合、サブクラスには2つのフィールド 'x'があります。明らかに、名前のシャドーイングがあります。名前「x」は、特定のコンテキストでこれらのフィールドの1つにのみバインドできます。

名前解決構文を入力 します-bxを書き込むと、コンパイラーはこれをサブクラスで宣言されたフィールドに解決しますが、ab.xを書き込むと、コンパイラーはそれをAで宣言されたフィールドに解決します-両方のフィールドはクラスBのメンバーです。 super.xを使用して、クラスBのコードから両方のフィールドにアクセスできます。

クラスB {... void g(){System.out.println( "both:" + x + "and" + super.x);}}

したがって、実行時に、サブクラスフィールドの名前が「x」でも「y」でも、違いはありません。どちらの場合も、サブクラスには2つの異なるフィールドがあり、それらの間に関係はありません。


あなたは正しい@Arnoですが、なぜ Javaがこれらの選択を行うのか、なぜC#とScalaは異なる選択をするのですか?これには明確な理由がありますか?
Martin Berger、

1

継承はB"is a"の形式であることを思い出してくださいA。ただし、機能Aが拡張される可能性がある場合を除き、継承されたメンバー変数はオブジェクトの属性と見なすことができます。直感的に、継承について教えられた方法(確かにJava風の観点から)、メンバー変数は属性のようなものであり、共通の属性は拡張されることを意図しておらず、機能だけが拡張されています。

たとえば、Personクラスがあり、人物に名前変数があるとします。とに拡張Personしても、名前は直感的に変更されません。ただし、たとえば「宿題」を行うために、メソッドは拡張されますが、クラスではでテストを採点する場合があります。StudentProfessorStudent.do_work()ProfessorProfessor.do_work()

もちろん、この直感を壊すことができ、メンバー変数をプロパティと同等にしたり、オーバーライドできるゲッターを使用したりすることができますが、私が上で言ったことは、このようにすることの直感だったと思います。実際、派生クラスで同じ名前のメンバー変数を使用することは、ほとんどの場合型破りだと思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.