30

Nuances of Overloading and Overriding in Java

 4 years ago
source link: https://www.tuicool.com/articles/QBjQne2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Z363u2r.jpg!web

I’ve been programming in Java for over half a decade, and thought I knew how overloading and overriding worked. It was only once I started thinking of and writing up the following corner cases, that I realized I didn’t know it nearly as well as I thought. In an effort to gamify these nuances, I’ve listed them below as a series of puzzles. Answers are provided as well – I’d be impressed if you managed to get them all without peeking.

Single Dispatch

Given the following classes:

class Parent {
  void print(String a) { log.info("Parent - String"); }
  void print(Object a) { log.info("Parent - Object"); }
}

class Child extends Parent {
  void print(String a) { log.info("Child - String"); }
  void print(Object a) { log.info("Child - Object"); }
}

What will get printed below?

String string = "";
Object stringObject = string;

// What gets printed?
Child child = new Child();
child.print(string);
child.print(stringObject);

Parent parent = new Child();
parent.print(string);
parent.print(stringObject);

Answers:

child.print(string);	    // Prints: "Child - String"
child.print(stringObject);  // Prints: "Child - Object"

parent.print(string);	    // Prints: "Child - String"
parent.print(stringObject); // Prints: "Child - Object"

child.print(string) and parent.print(string) are textbook examples of Object-Oriented programming in Java. The method that gets invoked depends on the “actual” instance type, not the “declared” instance type. Ie, regardless of whether you define the variable as being a Child or Parent , because the actual instance type is Child , Child::print will be invoked.

The second set of prints are more tricky. stringObject and string are both the exact same string. The only difference is that string is declared to be a String , whereas stringObject is declared to be an Object . Java does not support double-dispatch , and hence, when dealing with method parameters, what matters is the “declared” type of the parameter, not its “actual” type. print(Object) will be invoked, even though the “actual” parameter type is String

Hidden Override

Given the following:

class Parent {
  void print(Object a) { log.info("Parent - Object"); }
}

class Child extends Parent {
  void print(String a) { log.info("Child - String"); }
}

What gets printed?

String string = "";
Parent parent = new Child();
parent.print(string);

Answer:

parent.print(string);  // Prints: "Parent - Object"

The actual instance type is Child , and the declared argument type is String , and we do indeed have a method defined for Child::print(String) . In fact, that’s exactly what got picked in the previous example when calling parent.print(string) . However, that’s not the method that gets invoked here.

It appears that Java first picks which method to invoke, before checking for sub-class overrides. In this case, the declared instance type is Parent and the only matching method in Parent is Parent::print(Object) . When Java then checks for any potential overrides of Parent::print(Object) , it does not find any, so that’s the method which gets executed.

Exposed Override

Given the following:

class Parent {
  void print(Object a) { log.info("Parent - Object!"); }
  void print(String a) { throw new RuntimeException(); }
}

class Child extends Parent {
  void print(String a) { log.info("Child - String!"); }
}

What gets printed?

String string = "";
Parent parent = new Child();
parent.print(string);

Answer:

parent.print(string);  // Prints: "Child - String!"

The only difference between this and the earlier example, is that we have added a new Parent::print(String) method. This method never actually gets executed – it will throw an exception if it ever gets run! However, its mere presence causes Java to execute a different method.

Presumably what’s happening is that when evaluating parent.print(String) , the runtime now finds a matching Parent::print(String) method, and then sees that this method is overridden by Child::print(String) .

It’s tempting to think that simply adding a new method will never change system behavior if the new method never gets called. The above example shows otherwise.

Ambiguous Parameter

Given the following class:

public class Foo {
  void print(Cloneable a) { log.info("I am cloneable!"); }
  void print(Map a) { log.info("I am Map!"); }
}

What gets printed below?

HashMap cloneableMap = new HashMap();
Cloneable cloneable = cloneableMap;
Map map = cloneableMap;

// What gets printed?
Foo foo = new Foo();
foo.print(map);
foo.print(cloneable);
foo.print(cloneableMap);

Answers:

foo.print(map);           // Prints: "I am Map!"
foo.print(cloneable);	  // Prints: "I am cloneable!"
foo.print(cloneableMap);  // Does not compile

Similar to the single_dispatch example, what matters here is the declared type of the parameter, not the actual type. In addition, if there are multiple methods that are equally valid for a given parameter, Java throws a compile error and forces you to specify which one should be called.

Multiple Inheritance – Interfaces

Given the following:

public interface Father {
  default void print() { log.info("I am Father!"); }
}

public interface Mother {
  default void print() { log.info("I am Mother!"); }
}

public class Child implements Father, Mother {}

What gets printed below?

new Child().print();

Similar to the earlier example, this also does not compile. Specifically, the class definition itself for Child will fail to compile because there are conflicting default methods in Father and Mother . You need to update the Child class to specify the behavior for Child::print . See here for a more detailed explanation.

Multiple Inheritance – Class and Interface

Given the following:

public class ParentClass {
  default void print() { log.info("I am a class!"); }
}

public interface ParentInterface {
  default void print() { log.info("I am an interface!"); }
}

public class Child extends ParentClass implements ParentInterface {}

What gets printed?

new Child().print();

Answer:

new Child().print();  // Prints: "I am a class!"

Explanation: The linked article in the previous section actually covers this as well. If there’s an inheritance conflict between a class and an interface, the class wins.

Transitive Override

Given the following:

public class Parent {
  void print() { foo(); }
  void foo() { log.info("I am Parent!"); }
}

public class Child extends Parent {
  void foo() { log.info("I am Child!"); }
}

What gets printed?

new Child().print();

Answer:

new Child().print();  // Prints: "I am Child!"

Overriding a method will take effect even for transitive calls. Someone reading the Parent class may think that Parent::print will always invoke Parent::foo . But if the method gets overridden, then Parent::print will invoke the overridden version of foo() .

Private Override

Given the following:

public class Parent {
  void print() { foo(); }
  private void foo() { log.info("I am Parent!"); }
}

public class Child extends Parent {
  void foo() { log.info("I am Child!"); }
}

What gets printed?

new Child().print();

Answer:

new Child().print();  // Prints: "I am Parent!"

The setup is identical to the previous one, except for one difference. Parent.foo() is now declared to be private. Because of this, when Parent.print() invokes foo() , this is hard-coded to be Parent.foo() . Regardless of any other implementations of foo() that may exist in the child class, and regardless of the actual type of the instance that is invoking print() .

It is often assumed that changing a method from public to private, is a purely refactoring change, as long as compilation still succeeds. The above example shows that this is false – even if compilation succeeds, system behavior can change in dramatic ways.

Using the @Override annotation on all override methods will help greatly in preventing such regressions, by producing compile errors as soon as any base methods have their visibility changed.

Static Overrides

Given the following:

public class Parent {
  public static void print() { log.info("I am Parent!"); }
}

public class Child extends Parent {
  public static void print() { log.info("I am Child!"); }
}

What gets printed?

Child child = new Child();
Parent parent = child;

parent.print();
child.print();

Answers:

parent.print();	// Prints: "I am Parent!"
child.print();	// Prints: "I am Child!"

Java does not allow for overriding static methods. If you have the same static method defined in both the parent and child classes, the actual type of the instance does not matter at all. Only the declared type is used to determine which of the two methods is invoked.

This is the exact opposite of what happens with non-static methods where the declared type is ignored in favor of the actual type. Hence why you need to be careful when changing a method from non-static to static or vice-versa. Even if there are no compile errors, system behavior could change dramatically.

This is another reason to mark all override methods with the @Override annotation. In the above case, you will get a compile error when adding the annotation to Child::print , telling you that the method cannot be overridden because it is static.

This is also why it is good practice to never invoke static methods using an instance of the class – it can lead to surprising behavior like the above, and fail to alert you when problematic refactoring changes are made. Many IDEs like Intellij will warn you when calling a static-method from a non-static context, and it is best to follow up on such warnings.

Static Linking

Given the following:

public class Parent {
  public void print() { staticMethod(); instanceMethod(); }
  public static void staticMethod() { log.info("Parent::staticMethod"); }
  public void instanceMethod() { log.info("Parent::instanceMethod"); }
}

public class Child extends Parent {
  public static void staticMethod() { log.info("Child::staticMethod"); }
  public void instanceMethod() { log.info("Child::instanceMethod"); }
}

What gets printed?

Child child = new Child();
child.print();

Answer:

Parent::staticMethod
Child::instanceMethod

This is a combination of some different concepts we covered earlier. For instance methods, the override takes effect, even when the caller is in the parent. However, for static methods, even when the variable’s declared type is Child , Parent::staticMethod is what gets invoked, because of the intermediary foo() method.

Wrapping up

If there’s one take-away from all this, it is that inheritance is very very tricky, and easy to get wrong. If you try to be smart, it will bite you in the ass one day. Use very dumb guardrails and best practices to protect yourself:

  1. Always mark all override methods with the @Override annotation
  2. Always call static methods using a class reference , not an instance reference
  3. Set up IDE alerts or lint errors to enforce the above and other code smells
  4. Use composition over inheritance

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK