Chapter:Inheritance - Overriding methods in Object

From Juneday education
Jump to: navigation, search

Meta information about this chapter

Expand using link to the right to see the full content.

Purpose

The purpose of this lecture, is to answer the question "if the methods we inherit from Object has quite useless implementations, what's the use of inheriting them? The answer being that we can change the definition of the inherited methods to suit our needs without loosing the benefit that we still can pass as an Object, and the benefit that we still are guaranteed to have the same set of methods as Object and every other class - e.g. toString().

Goals

The student shall learn and understand:

  • What overriding means
  • How to override toString()
  • That the println method in PrintStream takes advantage of the fact that all objects have a toString() method, and (indirectly) calls toString on references passed as an argument to println.
  • That inheritance can be flexible when having heterogeneous collections, such as a list of File objects which can be a mix of AudioFile and VideoFile references, if those to classes both extend File
  • That if you override equals() you should also override hashCode()
    • We don't go into the details of why, just that it is a convention which people expect us to follow
  • That parameters to method can be of a reference-to-super-class-type, which now becomes the declared type of the parameter
    • That println uses this principle, it has an overloaded version accepting an Object
  • How to use the keyword super in order to explicitly call a super class version of an inherited method

Instructions to the teacher

Common problems related to this chapter

  • The students don't know so much about collections, but they have used List and ArrayList in some of the exercises in previous chapters
    • You can refer to that when talking about heterogeneous collections using a super class as the generic type
  • If the students don't understand the difference between compile-time type and runtime type, use the parameter of a method as an example - the compiler can't know how this method will be called (or even if it will be called), so the declared type of a reference is what the compiler will "see" when checking if the reference is used correctly.
    • You cannot have a method accepting an Object as a parameter, and then use the parameter to call a method not present in Object

Introduction

Overriding a method we'd like to change

If you extend a class you might be able to change the behaviour, that is write your own version of a method. The mechanism to do this is called overriding.

Now, since all classes eventually extends java.lang.Object in Java, that means that we inherit a few methods from Object, and if we want them to be useful and more specific to our own class, we must override those methods.

We'll not look at every method in Object in this introductory book/course, but settle with a few common and useful methods, which is very common to override:

  • public String toString()
  • public boolean equals(Object other)
  • public int hashCode()

A very useful method from Object is the toString() method. It allows us to get a String representation of an object, which is very handy when debugging and logging, but potentially also useful for client code (code in other classes using objects of your class).

When overriding a method, you can get some help to get it right, using the annotation @Override just above the definition of the method you are overriding. If you misspell the method (or change its signature) you are not overriding it, even if you thought you did. Using the @Override annotation, the compiler will generate an error telling you that you are not overriding anything. We'll use that in the lecture and in code further down this book.

Getting back to the public String toString(), you simply implement that method and return a string summarizing the object. It is considered good style to include all the parts relevant to the object's state in this string summary. Here's a small example:

/* A class representing members of some organization */
public class Member {

  private String name;
  private String email;

  public Member(String name, String email) {
    this.name = name;
    this.email = email;
  }

  public String name() {
    return name;
  }

  public String email() {
    return email;
  }

  @Override
  public String toString(){
    return name + ", " + email;
  }
}

It now becomes very easy to get a String representation of some Member instance. Simply call toString() using a reference to the object.

Another convenient thing about toString(), is that the void println(Object o) method of the class java.io.PrintStream (known from such successes as System.out.println) will actually accept a reference to any kind of object, and eventually use the toString() method to get the String to print. If you really want to know, it uses String.valueOf(object) internally, which in turn actually returns the result of object.toString() for references to objects and the string "null" for null references.

Overriding the method public boolean equals(Object other) is also a great service to users of your classes. This allows the programmer using your classes to compare whether two objects are to be considered "equal". Note that we can have two objects at different places in the memory (with a reference to each one of them) and we might want to know if the two objects represent the same thing in the real world, or at least represent two things that would be considered equal in the real world. The public boolean equals(Object other) method is there for doing exactly this.

If we only care about checking if two references refer to the same object in memory, we can use the operator == for this.

As an example of what we mean with "equals", consider a class for a money bill:

public class Bill {

  private int value;
  private String currency;

  public Bill(int value, String currency) {
    this.value = value;
    this.currency = currency;
  }

  public int value() {
    return value;
  }

  public String currency() {
    return currency;
  }

  // etc etc including an equals() method
}

Now, we might have an application which handles money. If the application should keep track of bills of money, then it is very likely that all that counts is the value of the money and the currency, not what exact bill instance is going where. If we pay 100 SEK we can use any Swedish Kronor 100 bill, right?

This is a case where we'd use equals to see that we got the right amount.

We are probably not interested in what exact Bill object is used for payment, just that it is a Swedish 100 kronor bill.

Another example would be the wrapper classes for numbers, like java.lang.Integer. Consider two different objects both representing some large number like 1234567. While the objects are different (there are two objects at different places in the JVM heap memory), they clearly represent the same integer. So they are two distinct objects but they are equal each other - the equals() method would return true:

Integer first = new Integer(1234567);
Integer second = new Integer(1234567);
boolean sameObject = (first == second); // false
boolean equalObjects = first.equals(second); // true

Now, when you implement public boolean equals(Object other), there is a contract you should honor. You should also override public int hashCode(), because all Java programmers will expect that two objects which "equals" each other, also have the same "hash code". The reason for that is that some classes use the hash code for identifying equality (because comparing two int values is not an expensive operation). While it is convenient to write if (obj1.equals(obj2)), the equals operation might be quite lengthy and costly.

But that's not the only reason. Hash codes are also used in order to distribute objects evenly, in the sense that two objects which are "almost" equal, should have completely different hash codes. The classes java.util.HashSet and java.util.HashMap, for instance, rely heavily on this expectation.

So, for everything to work without surprises for seasoned Java programmers, you must honor the contract to also override hashCode() when overriding equals. There are excellent guidelines for how to write reasonably good hash methods available, and we won't teach it in this book/course.

So remember, two objects who are to be considered equals, must produce the same hashCode() value. The other way around, however, is not a requirement (which is good, for it isn't practically possible) - two objects with the same hashCode() value are not necessarily equals. Two unequal objects with the same hash code are considered a collision and "bad luck" ;-)

The important thing to remember when writing a hash code function (like hashCode()), is to calculate the value using every instance variable which is used by the equals method. For the Bill example, if equals considers value and currency, then you should calculate the hash value using those two variables too. This would ensure that two equal Bills will have the same hashCode (if you don't add entropy to your method in an unpredictable way).

In the exercises and other places in this book, we show you some ways to write hashCode() methods, but we don't expect you to be fluent in this after reading this book. We only expect you to know about the equals() - hashCode() contract as explained above.

Chapters on the topic of Inheritance

Videos

Note, to date (2018-10-23) there is only one video lecture in English and one video lecture in Swedish. More might be added later.

Note also, that the playlist for the English videos contain also the coding videos where we code live and show how to override equals(), hashCode() and toString().

English videos

Swedish videos

Note that the coding videos are not included in this playlist. They are in English and so they are included in the English playlist above.

Bonus videos with live programming

We've recorded a few videos showing you how we can refine a small class by adding a toString() method (overriding the method inherited from Object) and also a simplistic implementation overriding hashCode(), also inherited from Object.

These videos are in English and are included in the playlist for English videos above.

  • Inheritance - Override | Inheritance - Live I | Live II | Live III | Live IV
    • Live coding I: From the previous chapter, you've already seen us creating the Customer class, but you might want to see it again. It's a blockbuster, trending on all hyped social media (not).
    • Live coding II: We add methods to retrieve name and email from a Customer object. We show you how to use a simple annotation.
    • Live coding III: In live video III we show how to override equals().
    • Live coding IV: In live video IV we show how to override hashCode().

Note: The examples in the live videos are mainly a way for us to show you how we can override methods.

Links

Further reading

Where to go next

Next page contains exercises for this chapter: Inheritance_-_Overriding_methods_in_Object_-_Exercises

« PreviousBook TOCNext »