https://software.rajivprab.com/2019/08/14/nuances-of-overloading-and-overriding-in-java/ Skip to content Software the Hard way Thoughts on Software Development Nuances of Overloading and Overriding in Java RP Uncategorized 2019-08-142023-05-18 7 Minutes [oop] I've been programming in Java for over half a decade, and thought I had mastered all aspects of overloading and overriding. It was only once I started thinking of and writing up the following edge 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. Kudos if you get them all without peeking at the answers. 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); --------------------------------------------------------------------- Answer: 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: 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); --------------------------------------------------------------------- Answer: 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: interface Father { default void print() { log.info("I am Father!"); } } interface Mother { default void print() { log.info("I am Mother!"); } } class Child implements Father, Mother {} What gets printed below? new Child().print(); --------------------------------------------------------------------- Answer: 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: class ParentClass { void print() { log.info("I am a class!"); } } interface ParentInterface { default void print() { log.info("I am an interface!"); } } 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: class Parent { void print() { foo(); } void foo() { log.info("I am Parent!"); } } 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: class Parent { void print() { foo(); } private void foo() { log.info("I am Parent!"); } } 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: class Parent { static void print() { log.info("I am Parent!"); } } class Child extends Parent { static void print() { log.info("I am Child!"); } } What gets printed? Child child = new Child(); Parent parent = child; parent.print(); child.print(); --------------------------------------------------------------------- Answer: 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: class Parent { void print() { staticMethod(); instanceMethod(); } static void staticMethod() { log.info("Parent::staticMethod"); } void instanceMethod() { log.info("Parent::instanceMethod"); } } class Child extends Parent { static void staticMethod() { log.info("Child::staticMethod"); } 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 print() 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 --------------------------------------------------------------------- Discussion thread on /r/java Discussion thread on Hacker News Share this: * Twitter * Facebook * Like this: Like Loading... Related * Tagged * extends * inheritance * Java * object oriented programming * overload * override * subclass Published 2019-08-142023-05-18 Post navigation Previous Post Alert Fast Next Post Abstractions Are In The Eye Of The Beholder 6 thoughts on "Nuances of Overloading and Overriding in Java" 1. [e591f] JoeHx says: 2019-08-14 at 11:36 am Awesome article! I ran into an issue similar to your Single Dispatch example, where I thought the consuming method would take the object type, rather than the variable type. Took me too long to figure out, but I was much wiser after. LikeLiked by 1 person Reply 2. Pingback: Java Weekly, Issue 294 | Baeldung 3. [1cd75] jimtomlinsongmailcom says: 2019-08-27 at 6:56 pm At the end of the Static Linking section, the phrase "because of the intermediary foo() method." doesn't make sense in that context (there's no foo() method). LikeLiked by 1 person Reply 1. [90664] RP says: 2019-08-27 at 7:05 pm Thanks for catching. I have now updated it LikeLike Reply 4. Pingback: Nuances of Overloading and Overriding in Java - CodingNova 5. [b5233] Kale Solis says: 2023-01-26 at 10:41 am Apprecciate your blog post LikeLike Reply Leave a Reply Cancel reply Enter your comment here... [ ] Fill in your details below or click an icon to log in: * * * Gravatar Email (required) (Address never made public) [ ] Name (required) [ ] Website [ ] WordPress.com Logo You are commenting using your WordPress.com account. ( Log Out / Change ) Facebook photo You are commenting using your Facebook account. ( Log Out / Change ) Cancel Connecting to %s [ ] Notify me of new comments via email. [ ] Notify me of new posts via email. [Post Comment] [ ] [ ] [ ] [ ] [ ] [ ] [ ] D[ ] Email Address: [ ] Follow Blog You may also like: * Nuances of Overloading and Overriding in Java * My Path to Financial Independence as a Software Engineer * When Feature Flags Do And Don't Make Sense * Myths Programmers Believe about CPU Caches * Building a WebApp from A-Z: The Caucus Tech Stack * Experiences working with an Outsourced Dev Shop * Rethinking Software Testing: Perspectives from the world of Hardware * Why Is There So Much Crap Software In The World * Cracking the Senior Software Interview * An Alternative to Dependency Injection Frameworks More View all posts About Me Create a website or blog at WordPress.com * Follow Following + [croppe] Software the Hard way Join 116 other followers [ ] Sign me up + Already have a WordPress.com account? Log in now. * + [croppe] Software the Hard way + Customize + Follow Following + Sign up + Log in + Copy shortlink + Report this content + View post in Reader + Manage subscriptions + Collapse this bar Loading Comments... Write a Comment... [ ] Email (Required) [ ] Name (Required) [ ] Website [ ] [Post Comment] %d bloggers like this: [b]