0

I'm playing around with objects right now and noticed something when I override toString() for readability. Observe these classes and the results.

    class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x; this.y = y;
        }
        public String toString() {
            return String.format("(%d,%d)",x,y);
        }
    }
    class Line extends Point {
        int l;
        Line(int x, int y, int l) {
            super(x,y);
            this.l = l;
        }
        public String toString() {
            return String.format("Length is %d", this.l);
        }
    }
JShell REPL:
> new Point(0,0)
==> (0,0)

> new Line(0,1,4)
==> Length is 4

> Point p = new Line(0,1,3);
p ==> Length is 3

> p.x
==> 0

> p.y
==> 1

> p.l
| Error    //Expected, p is a Point reference

> ((Line) p).l
==> 3

> Line l = (Line) p
l ==> Length is 3

> l.x
==> 0

> l.y
==> 1    //referencing x and y this way were surprising, but it makes sense

> ((Line) p).x
==> 0

> l.l
==> 3

> ((Point) l).toString()
==> "Length is 3"

An object instance must use the correct reference to get the methods in the desired class. So, why is it that toString() is treated differently? It appears that toString() is called for the class that it's constructed with, regardless of what type of reference I'm using.

Edit: Since toString() is being Overridden twice, how can I ever invoke Point.toString() through type casting?

  • 1
    Show us a [mcve]. It's hard to understand, where you have problems. – Seelenvirtuose Apr 19 '20 at 07:09
  • 1
    Welcome to SO! As an unrelated side note: IMHO it's a bad design to have Line inherit from Point. A line *has* two end points, but it's not generally considered a generalisation of a point. You should rather use composition in this case. You could also have an abstract Shape Classe with a position defined as Point and have Line extend that. – Axel Apr 19 '20 at 07:46
  • @Axel Like I said, I was only playing around to test an idea. I simply needed something to look at; the reality of points and lines is irrelevant. – Rayshaun Thompson Apr 19 '20 at 08:15
  • Once you override a method, the overridden one can never be called. – devgianlu Apr 19 '20 at 16:57
  • Thanks all. I'll just have to be clever some other way. – Rayshaun Thompson Apr 19 '20 at 20:21

3 Answers3

1

You've misunderstood.

ALL methods in java resolve to the one that it was constructed with.

Java uses a two-hit model to figure out what actual method to invoke:

  1. Bind to the right signature (static).
  2. Find the right overload to call (dynamic dispatch).

Bind to the right signature

Methods in java have a signature. The signature includes the name, the types of the parameters, and the return type (though that last one is usually irrelevant; javac won't compile any code with 2 methods that differ only in return type and nothing else). Note that generics are not taken into account here.

Example:

  • public void foo() [1]
  • public void foo(String[] x) [2]
  • public void foo(String... x) [2]
  • public void foo(List<String> x) [3]
  • public void foo(List<Integer> x) [3]
  • public void foo(List<String> x, boolean whatever) [4]

here each entry with the same number has the same signature: The [3]s are considered the same signature because generics are wiped out first, and the [2]s are considered the same signature because varargs is implemented as an array.

Java will use the type of the expression you are invoking the method on to figure out which signature you're attempting to invoke. This is entirely a compile time affair!

Example:

class Parent {
    public void foo(Object arg) { System.out.println("Parent"); }
}

class Child extends Parent {
    public void foo(String arg) { System.out.println("Child"); }
    // note: not the same signature!
}

Parent p = new Child();
p.foo("Hello");

javac (the compiler) looks at the line p.foo("Hello"), and will do the 'which signature is this?' dance: p is of type Parent (the fact that it is pointing at an object of type Child is irrelevant; the compiler cannot know this and doesn't take it into account), looks at all methods that the type Parent has, sees only one foo, and thus, that call is using the signature: void foo(Object). This code will print Parent if run.

Dynamic dispatch

Once the signature is figured out by the compiler, that is encoded in the class file. When that class file is executed, dynamic dispatch is used: The actual object's type is checked, and the most specific version is invoked. Example:

class Parent {
    public void foo(Object arg) { System.out.println("Parent"); }
}

class Child extends Parent {
    public void foo(Object arg) { System.out.println("Child"); }
    // note: now it IS the same signature!
}

Parent p = new Child();
p.foo("Hello");

note how it is the same example, except now the signature of the child method matches the one of the parent - it is now 'the same method'. Thus, whilst the compilation of the p.foo("Hello") call didn't change at all (in fact, you can recompile JUST Child.java and not recompile the code that invokes p.foo, and you'll observe this: Now it prints Child. That part is entirely a runtime affair.

I strongly advise you to use the @Override annotation anytime you intend to 'override' a method, so, the foo(Object) in the Child.java file should have that annotation. That annotation does only one thing: IF you put it on a method that isn't overriding anything (as in, there is no parent type that has the same method (signature and all!) as this), it's a compiler error, otherwise, it has no effect. Compiler-checked documentation is a good thing.

Had you put @Override on public void foo(String arg), you'd have gotten an error. Whilst it has the same name as a method in parent, it's not the same signature.

TL;DR: In java, 'method names' include all the generics-free types of the params, so if the param lists don't match up, it's not the same method. But if they do, dynamic dispatch is always used, you can't opt out of this in java: The method invoked will be the one of the actual object you're invoking it on.

NB: If methods are static, there is no dynamic dispatch at all. It's all static (determined at compile time).

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
1

You are not overloading the toString() method but you are overriding it in the child class.

If you still have any questions then do post your code that you have written inside the public static void main(String args[]){} method.

Shahid Sarwar
  • 1,209
  • 14
  • 29
1

It appears that toString() is called for the class that it's constructed with, regardless of what type of reference I'm using.

Yes. This is what dynamic dispatch is all about in Java. Check What is dynamic method dispatch and how does it relate to inheritance? to learn more about it.

Demo:

public class Main {
    public static void main(String[] args) {
        Point p1 = new Point(10, 20);
        Object p2 = new Point(5, 15);

        Point l1 = new Line(3, 8, 7);
        Line l2 = new Line(10, 20, 20);
        Object l3 = new Line(5, 15, 25);

        System.out.println(p1);
        System.out.println(p2);
        System.out.println(l1);
        System.out.println(l2);
        System.out.println(l3);
    }
}

Output:

(10,20)
(5,15)
Length is 7
Length is 20
Length is 25
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110