Essential Java Programming - Specializing in a Sub Class
Start course

This Course takes you through many of the essential Java programming features. We’ll review in depth features like Language Statements, Using Strings, Subclasses, Fields and Variables, Using Arrays, Java Packages and Visibility, and much more.

Learning Objectives

  • Understand key Java language statements and keywords
  • Be able to develop confidently with Strings
  • Be able to implement specialization using subclasses
  • Work with fields and variables
  • Implement Arrays to store multiple values in a single variable
  • Learn how to structure your Java code using Packages and Visibility


  • A basic understanding of software development
  • A basic understanding of the software development life cycle

Intended Audience

  • Software Engineers interested in learning Java to develop applications
  • Software Architects interested in learning Java to design applications
  • Anyone interested in basic Java application development and associated tooling
  • Anyone interested in understanding the basics of the Java SDK 



Welcome back. In this lecture, we'll familiarize yourself with subclassing and how to implement specialization through inheritance. We'll cover items such as constructing a class that extends another class. Correctly implementing equals and toString methods. Writing constructors that pass initialization data to the parent constructor when appropriate. Using the instanceof to verify the class type of an object reference. Overriding subclass methods and the use of the super keyword to leverage behaviors in the parent class. And finally, safely casting references to a more refined type. 

A key concept of Object-Oriented Programming is the concept of code reuse through inheritance. We can extend the functionality of an object while still using all of the code developed in the parent class. A class may extend the definition of another class. Methods and instance data of the superclass become available to the subclass based on superclass method/field visibility. New instance data can be added. New methods can be added. Subclassing is expressed using the extends keyword. In this example here, the Employee class extends or inherits from the Person class from an OO perspective, this makes sense. In real life an employee is a person and as such can both work and speak. We can alter the behavior of the parent class by overriding some of the methods defined in the parent class. 

Again in this example, the Employee class overrides the speak method providing a more specialized version of it. Again subclasses can override methods of the superclass. They can have the same name, return type and calling parameters. They often implement different behavior and it is best practice to almost always use the @Override annotation as this will help the compiler to detect errors. Inheritance in Java is the ability of the subclass to use the structure or behavior of its parent class or superclass. Inheritance is a relationship. That is to say, a chair is a type of furniture or a cat is a type of mammal. When you define a subclass, you need only provide the code to implement changes in behavior from the superclass. By default, your subclass inherits the capabilities of its superclass. To change the behavior of an inherited method, the subclass redefines the methods in its class definition. This causes the corresponding superclass method definition to be hidden. 

We can also extend a class with additional functionality by adding new methods and fields. Another key concept is that an extended class can be treated as its base class so long as you do not use any of the new functionality defined in the extended class. Java has removed a problem by making multiple inheritance illegal. Multiple inheritance when used well is very powerful but most of the time simply leads to overly complex, hard to maintain software. Java has implemented the notion of interfaces which solved design problems caused by the no multiple inheritance. These will be covered in a later listen. As with other OO languages, subclasses know everything about their superclasses but superclasses know nothing about their subclasses. Since Java is a strongly typed language, it always checks for type compatibility at compile time. Some checks however are possible at runtime only. 

In cases where an operator would have been compatible operands can does and does occur. In such situations, casting should be used to avoid such issues. Upcasting allows us to tell the compiler to use a different type within the object's class hierarchy. Upcasting to a more generalized type is often referred to as just casting. And is safe since it is able to be checked by the compiler. Downcasting involves casting to a more specialized type and is also referred to as just casting. The compiler can check that a cast is within the class hierarchy. However a compiler cannot verify that the reference object is of that type. If not it would result in a runtime error called a ClassCastException. For example the cast from a Person to an Employer is technically valid since an employer is a subtype of person. The compiler will check for this. However the Person reference could actually refer to an Employee instead of an Employer and so at runtime this cast would fail. Overriding superclass methods. 

A subclass may override methods inherited from the superclass. The signatures of the methods must be the same. This hides the method with the same signature in the superclass. Do not confuse method overloading with method overriding. Overloading is where you define multiple methods with the same name but with different input parameters. Overriding is where you define a method with exactly the same signature as the method in a superclass. Overriding is used to provide specialized behavior. A subclass inherits all of its superclass methods. A subclass can though override and modify the behavior of a method in its superclass. This again is known as overriding. To override a method the subclass must define a method with the exact signature and return type as the one in the superclass. 

In the examples shown here, the Duck and Horse classes provide an override for the speak method since the method signature is identical to that of the parent Animal class. The JungleAnimal class however provides an overloaded speak method as its method signature differs from that of the parent Animal class speak method. This is the key difference between method overriding and method overloading. The ability to override a method in a subclass would only be moderately useful if doing so required us to duplicate the work already done in the superclass. Java allows overriding methods to invoke the overridden method in the superclass using the super keyword. 

Every class that is defined in Java is a subclass of object. Even if it is not stated explicitly in the class definitions. Well designed classes should always override equals, toString and hashcode methods. The instanceof keyword is used to check is an object instance is of a particular type. In the class hierarchy, a Person object is a valid instanceof Person, Mammal and object types. In other words, it provides an implementation of all of these class definitions. The instanceof check is performed on the object instance to which the reference is pointing not the reference type. By using the instanceof keyword, you can add a cheat to your code before you downcast the reference. When the reference is pointing to an instanceof Employee it is valid to use an Employee reference to point to the subject. When instanceof is used against the type that is not within the same class hierarchy, the compiler will throw an error. 

In the example shown here, a Person reference can never be used to point to an instanceof Penguin nor Birds for that matter. And as a result the compiler will immediately mark the statement as an error. When you have two instances of Person, each populated with data, how do you determine if these objects represent the same person? The answer to this question depends on the application that you are developing. In one application they are only considered equal when the ID is unique. For example when the data was read from a database by comparing via primary key. In this situation, two instances with different IDs but the same social security number, are considered to resemble two different Persons. 

Other applications consider two instances equal when the social security number is the same even when they were read from two different rows in a database. Again, other applications may consider two Persons equal if they have the same username. In other words to determine if two instances are equal the developer must provide the logic to determine equality. The object class defines an equals method. The default implementation of this method checks if the two references point to the same object. So they do not implement a business rule since the equals method is inherited by all classes, developers can override the default implementation of the equals method to provide an implementation that resembles the business rule for the application they are developing. The example shown here considers two persons to be equal when the employee ID value of both instances is the same. Another implementation of the equals method might to be to check the name of both Person instances. The first check is to ensure that the object being compared to is of the same type otherwise it is not considered to be equal. 

Once the other reference has been downcasted to ensure it is of the same type, we can then compare the name values. Since the name is of type String, we can use the equals method of the String class to check if the two names are identical. Another method that is inherited from the object class is the hashcode method. When the equals method is overridden you should also override the hashcode method keeping in mind these four rules. One, the hashcode method must generate the same value for all instances that are equal according to the equals method. Two, multiple calls to the hashcode method \ must return the same value as long as the state of the object has not changed. Three, the hashcode must be calculated using the same fields that we use to determine if the two objects are equal. Four, the hashcode method may return the same value for two objects even when these objects are not equal. 

The default constructor. If a class has no constructors it is given a default, no-argument constructor. If a constructor is specified which takes arguments, Java will not provide a default no-argument constructor. Many applications that manage your classes for you will make the assumption that there is a default constructor This should do something reasonable such as setting the variables to meaningful initial values. Implicit constructor chaining. For the same reason that we choose to invoke overridden methods in the superclass, we may also choose to have our constructor pass initialization data to the corresponding superclass constructor. If the superclass constructor is not explicitly invoked the compiler will automatically invoke the no-argument constructor of the superclasses regardless of which constructor was invoked in the lowest level class being instantiated. 

Following the OO paradigm of encapsulating services and reusing them, constructors can be chained together so each more specialized constructor makes use of the more generalized superclasses constructor. Employee is a more specialized form of Person. As objects are built from the inside out, the highest superclass is built first followed by the next and so on down to the most specialized class. The invocation of the superclasses constructor must be the first statement in the constructor. In many cases the decision to pass data up to the superclass constructor is not optional. In this example, an Employees constructor with a name. However, name is a private attribute of the superclass. It would be a mistake for the Employee subclass to duplicate the data storage of the name field. The only way to get this information into the Person superclass is to pass it into the appropriate constructor in initialization time. 

Remember, if the subclass constructor does not explicitly pick which superclass constructor to call the compiler will automatically generate the code to call the no-argument superclass constructor. For example if the Person class did not have a no-argument constructor and there were no explicit superclass constructor calls in the subclass constructors, the compiler would automatically attempt to call the no-argument constructor of the superclass. All of this is apparent. What is often not apparent is that when the subclass constructor that takes a String is invoked, this does not cause the superclass String constructor to be invoked. If the superclass does not have a no-argument constructor, then every constructor in the subclass must explicitly call a valid constructor in the superclass. 

Consider the following questions to test yourself on the content that we have just reviewed. The answers to the above questions are: one, no, multiple inheritance is not possible. Two, no. Three, the equals and hashcode methods. Four, instanceof keyword. Five, yes, and six, yes. The default no-argument constructor is created when no constructors are defined. Okay, there completes this lecture, go ahead and close it and we'll see you shortly in the next one.

About the Author
Learning Paths

Jeremy is a Content Lead Architect and DevOps SME here at Cloud Academy where he specializes in developing DevOps technical training documentation.

He has a strong background in software engineering, and has been coding with various languages, frameworks, and systems for the past 25+ years. In recent times, Jeremy has been focused on DevOps, Cloud (AWS, Azure, GCP), Security, Kubernetes, and Machine Learning.

Jeremy holds professional certifications for AWS, Azure, GCP, Terraform, Kubernetes (CKA, CKAD, CKS).

Covered Topics