7

I have a question about inheritance in Java-like OO programming languages. It came up in my compiler class, when I explained how to compile methods and their invocation. I was using Java as example source language to compile.

Now consider this Java program.

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 ); } }

When you run it, you get the following result.

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

The interesting cases are those that happen with the object ab of static type A, which is B dynamically. As ab.f() prints out

B:f

it follows that method invocations are not affected by the compile-time type of the object the method is invoked with. But System.out.println ( ab.x ) prints out 0, so member access is affected by compile-time types.

A student asked about this difference: should not the access of members and methods be consistent with each other? I could not come up with a better answer than "that's the semantics of Java".

Would you know a crisp conceptual reason why members and methods are different in this sense? Something I could give my students?

Edit: Upon further investigation, this seems to be a Java idiosyncrasy: C++ and C# act differently, see e.g. Saeed Amiri's comment below.

Edit 2: I just tried out the corresponding Scala program:

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 ); } }

And to my surprise this results in

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

Note that the overrise modifiers are necessary. This surprises me because Scala compiles to the JVM, and moreover, when I compile and execute the Java program at the top using the Scala compiler/runtime, it behaves like the Java program.

Gilles 'SO- stop being evil'
  • 44,159
  • 8
  • 120
  • 184
Martin Berger
  • 8,358
  • 28
  • 47

3 Answers3

6

I suspect that this is intimately related to shadowing of fields in Java.

When writing a derived class, I can, as in your example, write a field x that shadows the definition of x in the base class. This means that in the derived class, the original definition is no longer accessible via name x. The reason Java allows this is so that derived class implementations are less dependent on the base class implementation details, thereby (only partially) avoiding the fragile base class problem. For instance, if the base class did not originally have a field x, but then subsequently introduced one, Java's semantics avoid this breaking the derived class implementation.

The variable x in the derived class can have a completely different type than the variable in the base class – I tested this, it works. This is in stark contrast to method overriding, where the type needs to be sufficiently compatible (aka, the same). So when I have a variable of type A in your example, the only type I can derive for it is the one specified in class A (or above). As this can be different from the type declared in class B, it is only type safe to return the value of the field x from class A. (And naturally, the semantics must be the same regardless of what the type of the field is in class B.)

Dave Clarke
  • 20,345
  • 4
  • 70
  • 114
4

In Java, there is no 'static type of an object' - there is the type of the object, and there is the type of the reference. All Java methods are 'virtual' (to use C++ terminology), i.e. they are resolved based on the object's type. And btw. '@Override' has no effect whatsoever on the compiled code - it is just a safeguard that generates a compiler error if the so annotated method does not override a superclass' method.

Field access on the other hand is never resolved dynamically - or, to put it differently, Java uses no polymorphism when accessing fields. In the (pathological) case that a subclass has a field x with the same name as a superclass field, the subclass has two fields 'x'. Obviously, there is name shadowing - the name 'x' can be bound to only one of these fields in a given context.

Enter name resolution syntax - when you write b.x, the compiler resolves this to the field declared in the subclasss, but when you write ab.x, the compiler resolves that to the field declared in A - both fields being members of class B. You can access both fields from code in class B using super.x:

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

So at runtime, it makes no difference at all whether the subclass' field was named 'x' or 'y' - in both cases, the subclass has two distince fields with no relationship between them.

Arno
  • 41
  • 1
1

Remembering that inheritance is a form of saying B "is a" A, except the functionality of A might be extended, inherited member variables can be considered attributes of the object. Intuitively, the way we are taught about inheritance (admittedly from a Java-ish point of view), member variables are like attributes, and common attributes are not meant to be extended, rather only functionality is.

For example, suppose you have a Person class, and the person has a name variable. The name intuitively won't get changed, even if we extend Person to Student and Professor. However methods, for example like Student.do_work() do get extended, to do "homework", while the Professor class might grade tests in Professor.do_work().

Of course you can break this intuition, and you can essentially make member variables equivalent to properties, or use getters, which can be overridden etc., but I think what I said above was the intuition of doing it this way. In fact, I'd say it would be unconventional in most cases to have a member variable of the same name, in a derived class.

Realz Slaw
  • 6,251
  • 33
  • 71