However, I don't understand the result of example 3 and 4.

Okay, let's look at them individually.

Example 3

//Example 3 prints Rectangle:add(Rectangle). Expected Square:add(Square) rs.add(new Square());

The important parts are the compile-time types of the expressions rs and new Square() .

rs is only declared as Rectangle , so the compiler will look at the methods declared by Rectangle and its superclasses:

public void add(Figure f) public void add(Rectangle r)

The type of the expression new Square() is Square , so both methods are applicable - but the second one is more specific.

So the compiler will call add(Rectangle) on the object that rs refers to. That's it for the compile-time side.

At execution time, the value of rs refers to an instance of Square - but Square doesn't override add(Rectangle) so the method picked is the implementation in Rectangle :

public void add(Rectangle r){ System.out.println("Rectangle:add(Rectangle)"); }

Example 4

//Example 4 prints Rectangle:add(Rectangle). Expected Square:add(Figure) Square ss = new Square(); ss.add(rs);

Again, let's consider the compile-time types involved... ss is of type Square , and rs is of type Rectangle (compile-time types, remember).

The methods declared by Square and its superclasses are:

public void add(Figure f) public void add(Rectangle r) public void add(Square s)

As the compile-time type of rs is only Rectangle (not Square ), the first two methods are applicable, but the third isn't. Therefore, again, add(Rectangle) is picked at compile time (as it's more specific than add(Figure) ).

Again, the execution time type of ss is Square , which doesn't override add(Rectangle) , so the implementation in Rectangle is used.

Let me know if anything in here is confusing - if you can be specific about which part, that would be great.