Java Basics: Overriding Methods, Polymorphism, and Static Classes

Using Access Control

The table below illustrates access to a field or method marked with the access modifier in the left column.

Modifier (keyword) Same Class Same Package Subclass in Another Package Universe
private Yes
default Yes Yes
protected Yes Yes Yes
public Yes Yes Yes Yes

The access modifier keywords shown in this table are private, protected, and public. When a keyword is absent, the default access modifier is applied.

private: Provides the greatest control over access to fields and methods. With private, a data field or method can be accessed only within the same Java class.

default: Also called package level access. By default, a data field or method can be accessed within the same class or package. A default class cannot be subclassed outside its package.

protected: Provides access within the package and subclass. Fields and methods that use protected are said to be “subclass-friendly.” Protected access is extended to subclasses that reside in a package different from the class that owns the protected feature. As a result, protected fields or methods are actually more accessible than those marked with default access control.

public: Provides the greatest access to fields and methods, making them accessible anywhere: in the class, package, subclasses, and any other class.

Protected Access Control: Example

Shown below are two classes in two packages. Class Foo is in the package demo, and declares a data field called result with a protected access modifier.

1 package demo;
2 public class Foo {
3         protected int result = 20;
4         int num= 25;
5 }
1 package test;
2 import demo.Foo;
3 public class Bar extends Foo {
4        private int sum = 10;
5        public void reportSum () {
6             sum += result;
7             sum +=num;
8       }
9 } 

In the class Bar, which extends Foo, there is a method, reportSum, that adds the value of result to sum. The method then attempts to add the value of num to sum. The field num is declared using the default modifier, and this generates a compiler error. Why?

Answer: The field result, declared as a protected field, is available to all subclasses―even those in a different package. The field num is declared as using default access and is only available to classes and subclasses declared in the same package.

Access Control: Good Practice

A good practice when working with fields is to make fields as inaccessible as possible, and provide clear intent for the use of fields through methods.

1 package demo;
2 public class Foo3 {
3       private int result = 20;
4       protected int getResult() {
5       return this.result;
6      }
7   }
1 package test;
2 import demo.Foo3;
3 public class Bar3 extends Foo3 {
4       private int sum = 10;
5       public void reportSum() {
6             sum += getResult();
7       }
8 }

A slightly modified version of the example using the protected keyword is shown in the slide. If the idea is to limit access of the field result to classes within the package and the subclasses (package-protected), you should make the access explicit by defining a method purposefully written for package and subclass-level access.

Overriding Methods

Consider a requirement to provide a String that represents some details about the Employee class fields.

3 public class Employee {
4       private int empId;
5       private String name;
14      // Lines omitted
15
 16   public String getDetails() {
17     return "ID: " + empId + " Name: " + name;
18   }

Although the Employee class has getters to return values for a print statement, it might be nice to have a utility method to get specific details about the employee. Consider a method added to the Employee class to print details about the Employee object.

In addition to adding fields or methods to a subclass, you can also modify or change the existing behavior of a method of the parent (superclass). You may want to specialize this method to describe a Manager object.

Overriding Methods

In the Manager class, by creating a method with the same signature as the method in the Employee class, you are overriding the getDetails method:

3 public class Manager extends Employee {
4     private String deptName;
17    // Lines omitted
18
19    @Override
20    public String getDetails() {
21        return super.getDetails () +
22        " Dept: " + deptName;
23 }

A subclass can invoke a parent method by using the super keyword. When a method is overridden, it replaces the method in the superclass (parent) class. This method is called for any Manager instance.

A call of the form super.getDetails() invokes the getDetails method of the parent class.

Note: If, for example, a class declares two public methods with the same name, and a subclass overrides one of them, the subclass still inherits the other method.

Invoking an Overridden Method

During run time, the Java Virtual Machine invokes the getDetails method of the appropriate class. If you comment out the getDetails method in the Manager class shown in the previous section, what happens when m.getDetails()is invoked?

Answer: Recall that methods are inherited from the parent class. So, at run time, the getDetails method of the parent class (Employee) is executed.

Using the previous examples of Employee and Manager:

5   public static void main(String[] args) {
6    Employee e = new Employee(101, "Jim Smith",
7     "011-12-2345", 100_000.00);
8   Manager m = new Manager(102, "Joan Kern",
9   "012-23-4567", 110_450.54, "Marketing");
10
11   System.out.println(e.getDetails());
12   System.out.println(m.getDetails());
13 }

The correct getDetails method of each class is called:

ID: 101 Name: Jim Smith
ID: 102 Name: Joan Kern Dept: Marketing 

Virtual Method Invocation

What happens if you have the following?

5 public static void main(String[] args) {
6   Employee e = new Manager(102, "Joan Kern",
7       "012-23-4567", 110_450.54, "Marketing");
8
9    System.out.println(e.getDetails());
10 }

During execution, the object’s runtime type is determined to be a Manager object:

ID: 102 Name: Joan Kern Dept: Marketing

At run time, the method that is executed is referenced from a Manager object. This is an aspect of polymorphism called virtual method invocation.

Compiler Versus Runtime Behavior

The important thing to remember is the difference between the compiler (which checks that each method and field is accessible based on the strict definition of the class) and the behavior associated with an object determined at run time. This distinction is an important and powerful aspect of polymorphism: The behavior of an object is determined by its runtime reference.

Because the object you created was a Manager object, at runtime, when the getDetails method was invoked, the runtime reference is to the getDetails method of a Manager class, even though the variable e is of the type Employee.

This behavior is referred to as virtual method invocation.

Note: If you are a C++ programmer, you get this behavior in C++ only if you mark the method by using the C++ keyword virtual.

Accessibility of Overriding Methods

The overriding method cannot be less accessible than the method in the parent class.

public class Employee {
       //... other fields and methods
       public String getDetails() { ... }
}
3 public class BadManager extends Employee {
4       private String deptName;
5       // lines omitted
20      @Override
21      private String getDetails() { // Compile error
22      return super.getDetails () +
23     " Dept: " + deptName;
24    }

To override a method, the name and the order of arguments must be identical. By changing the access of the Manager getDetails method to private, the BadManager class will not compile.

Applying Polymorphism

Suppose that you are asked to create a new class that calculates a bonus for employees based on their salary and their role (employee, manager, or engineer):

3 public class BadBonus {
4    public double getBonusPercent(Employee e){
5    return 0.01;
6   }
7
8    public double getBonusPercent(Manager m){
9      return 0.03;
10  }
11
12   public double getBonusPercent(Engineer e){
13     return 0.01;
14  }
// Lines omitted

Design Problem

What is the problem in the example in the slide? Each method performs the calculation based on the type of employee passed in, and returns the bonus amount.

Consider what happens if you add two or three more employee types. You would need to add three additional methods, and possibly replicate the code depending upon the business logic required to compute shares.

Clearly, this is not a good way to treat this problem. Although the code will work, this is not easy to read and is likely to create much duplicate code.

Applying Polymorphism

A good practice is to pass parameters and write methods that use the most generic possible form of your object.

public class GoodBonus {
   public static double getBonusPercent(Employee e){
      // Code here
   }
// In the Employee class
   public double calcBonus(){
     return this.getSalary() * GoodBonus.getBonusPercent(this);
   }

One method will calculate the bonus for every type.

Use the Most Generic Form

A good practice is to design and write methods that take the most generic form of your object possible. In this case, Employee is a good base class to start from. But how do you know what object type is passed in? You learn the answer in the next section.

Using the instanceof Keyword

The Java language provides the instanceof keyword to determine an object’s class type at run time.

3 public class GoodBonus {
4    public static double getBonusPercent(Employee e){
5        if (e instanceof Manager){
6           return 0.03;
7        }else if (e instanceof Director){
8           return 0.05;
9        }else {
10          return 0.01;
11       }
12   }
13 }

In the GoodBonus class, the getBonusPercent method uses the instanceof operator to determine what type of Employee was passed to the method.

Overriding Object methods

The root class of every Java class is java.lang.Object.
– All classes will subclass Object by default.
– You do not have to declare that your class extends Object. The compiler does that for you.

public class Employee { //... }

is equivalent to:

public class Employee extends Object { //... }

The root class contains several nonfinal methods, but there are three that are important to consider overriding: toString, equals, and hashCode.

Object toString Method

The toString method returns a String representation of the object.

Employee e = new Employee (101, "Jim Kern", ...)
System.out.println (e);

You can use toString to provide instance information:

public String toString () {
     return "Employee id: " + empId + "\n"+
            "Employee name:" + name;
}

This is a better approach to getting details about your class than creating your own getDetails method.

The println method is overloaded with a number of parameter types. When you invoke System.out.println(e); the method that takes an Object parameter is matched and invoked. This method in turn invokes the toString() method on the object instance.

Note: Sometimes you may want to be able to print out the name of the class that is executing a method. The getClass() method is an Object method used to return the Class object instance, and the getName() method provides the fully qualified name of the runtime class. getClass().getName(); // returns the name of this class instance. These methods are in the Object class.

Object equals Method

The Object equals method compares only object references.

– If there are two objects x and y in any class, x is equal to y if and only if x and y refer to the same object. For example:

Employee x = new Employee (1,"Sue","111-11-1111",10.0);
Employee y = x;
x.equals (y);    // true
Employee z = new Employee (1,"Sue","111-11-1111",10.0);
x.equals (z);    // false!

Because what we really want is to test the contents of the Employee object, we need to override the equals method:

public boolean equals (Object o) { ... }

The equals method of Object determines (by default) only if the values of two object references point to the same object. Basically, the test in the Object class is simply as follows:

If x == y, return true.

For an object (like the Employee object) that contains values, this comparison is not sufficient, particularly if we want to make sure there is one and only one employee with a particular ID.

Overriding equals in Employee

An example of overriding the equals method in the Employee class compares every field for equality:

1 @Override
2 public boolean equals (Object o) {
3      boolean result = false;
4      if ((o != null) && (o instanceof Employee)) {
5          Employee e = (Employee)o;
6          if ((e.empId == this.empId) &&
7              (e.name.equals(this.name)) &&
8              (e.ssn.equals(this.ssn)) &&
9              (e.salary == this.salary)) {
10                 result = true;
11         }
12     }      return result;
13 }

This simple equals test first tests to make sure that the object passed in is not null, and then tests to make sure that it is an instance of an Employee class (all subclasses are also employees, so this works). Then the Object is cast to Employee, and each field in Employee is checked for equality.

Note: For String types, you should use the equals method to test the strings character by character for equality

@Override annotation

This annotation is used to instruct compiler that method annotated with @Override is an overridden method from super class or interface. When this annotation is used the compiler check is to make sure you actually are overriding a method when you think you are. This way, if you make a common mistake of misspelling a method name or not correctly matching the parameters, you will be warned that you method does not actually override as you think it does. Secondly, it makes your code easier to understand when you are overriding methods.

Overriding Object hashCode

The general contract for Object states that if two objects are considered equal (using the equals method), then integer hashcode returned for the two objects should also be equal.

1 @Override //generated by NetBeans
2 public int hashCode() {
3        int hash = 7;
4        hash = 83 * hash + this.empId;
5        hash = 83 * hash + Objects.hashCode(this.name);
6        hash = 83 * hash + Objects.hashCode(this.ssn);
7        hash = 83 * hash + (int)
(Double.doubleToLongBits(this.salary) ^
(Double.doubleToLongBits(this.salary) >>> 32));
8     return hash;
9 }

The Java documentation for the Object class states: “… It is generally necessary to override the hashCode method whenever this method [equals] is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.”

The hashCode method is used in conjunction with the equals method in hash-based collections, such as HashMap, HashSet, and Hashtable. This method is easy to get wrong, so you need to be careful. The good news is that IDEs such as NetBeans can generate hashCode for you.

To create your own hash function, the following will help approximate a reasonable hash value for equal and unequal instances:

  1. Start with a nonzero integer constant. Prime numbers result in fewer hashcode collisions.
  2. For each field used in the equals method, compute an int hash code for the field. Notice that for the Strings, you can use the hashCode of the String.
  3. Add the computed hash codes together.
  4. Return the result.

Methods Using Variable Arguments

A variation of method overloading is when you need a method that takes any number of arguments of the same type:

public class Statistics {
       public float average (int x1, int x2) {}
       public float average (int x1, int x2, int x3) {}
       public float average (int x1, int x2, int x3, int x4) {}
}

These three overloaded methods share the same functionality. It would be nice to collapse these methods into one method.

Statistics stats = new Statistics ();
float avg1 = stats.average(100, 200);
float avg2 = stats.average(100, 200, 300);
float avg3 = stats.average(100, 200, 300, 400);

Methods with a Variable Number of the Same Type

One case of overloading is when you need to provide a set of overloaded methods that differ in the number of the same type of arguments. For example, suppose you want to have methods to calculate an average. You may want to calculate averages for 2, 3, or 4 (or more) integers.

Each of these methods performs a similar type of computation—the average of the arguments passed in, as in this example:

public class Statistics {
       public float average(int x1, int x2) { return (x1 + x2) / 2; }
       public float average(int x1, int x2, int x3) {
            return (x1 + x2 + x3) / 3;
    }
    public float average(int x1, int x2, int x3, int x4) {
        return (x1 + x2 + x3 + x4) / 4;
   }
}

Java provides a convenient syntax for collapsing these three methods into just one and providing for any number of arguments.

Using Variable Arguments

Java provides a feature called varargs or variable arguments.

1 public class Statistics {
2        public float average(int... nums) {
3        int sum = 0;
4        for (int x : nums) { // iterate int array nums
5           sum += x;
6        }
7        return ((float) sum / nums.length);
8       }
9   }

Note that the nums argument is actually an array object of type int[]. This permits the method to iterate over and allow any number of elements.

The average method shown above takes any number of integer arguments. The notation (int… nums) converts the list of arguments passed to the average method into an array object of type int.

Note: Methods that use varargs can also take no parameters―an invocation of average() is legal. You will see varargs as optional parameters in use in the NIO.2 API in the lesson titled “Java File I/O.” To account for this, you could rewrite the average method shown above as:

public float average(int... nums) {
   int sum = 0; float result = 0;
       if (nums.length > 0) {
         for (int x : nums) // iterate int array nums
           sum += x;
         result = (float) sum / nums.length;
       }
     return (result);
  }
}

Casting Object References

After using the instanceof operator to verify that the object you received as an argument is a subclass, you can access the full functionality of the object by casting the reference:

4 public static void main(String[] args) {
5   Employee e = new Manager(102, "Joan Kern",
6      "012-23-4567", 110_450.54, "Marketing");
7
8   if (e instanceof Manager){
9      Manager m = (Manager) e;
10     m.setDeptName("HR");
11     System.out.println(m.getDetails());
12   }
13 }

Without the cast to Manager, the setDeptName method would not compile. Although a generic superclass reference is useful for passing objects around, you may need to use a method from the subclass.

In the example above, you need the setDeptName method of the Manager class. To satisfy the compiler, you can cast a reference from the generic superclass to the specific class. However, there are rules for casting references. You see these in the next section.

Upward Casting Rules

Upward casts are always permitted and do not require a cast operator.

Director d = new Director();
Manager m = new Manager();

Downward Casting Rules

For downward casts, the compiler must be satisfied that the cast is possible.

5     Employee e = new Manager(102, "Joan Kern",
6     "012-23-4567", 110_450.54, "Marketing");
7
8     Manager m = (Manager)e; // ok
9     Engineer eng = (Manager)e; // Compile error
10    System.out.println(m.getDetails());

Downward Casts

With a downward cast, the compiler simply determines if the cast is possible; if the cast down is to a subclass, then it is possible that the cast will succeed. Note that at run time, the cast results in a java.lang.ClassCastException if the object reference is of a superclass and not of the class type or a subclass.

Finally, any cast that is outside the class hierarchy will fail, such as the cast from a Manager instance to an Engineer. A Manager and an Engineer are both employees, but a Manager is not an Engineer.

static Keyword

The static modifier is used to declare fields and methods as class-level resources

Static class members:

  • Can be used without object instances
  • Are used when a problem is best solved without objects
  • Are used when objects of the same type need to share fields
  • Should not be used to bypass the object-oriented features of Java unless there is a good reason

Static Methods

Static methods are methods that can be called even if the class they are declared in has not been instantiated.

Static methods:

  • Are called class methods
  • Are useful for APIs that are not object oriented – java.lang.Math contains many static methods
  • Are commonly used in place of constructors to perform tasks related to object initialization.
  • Cannot access nonstatic members within the same class.

Using Static Variables and Methods: Example

The java.lang.Math class contains methods and constants for performing basic numeric operations such as the elementary exponential, logarithm, square root, and trigonometric.

The methods and constants in the Math class are all static, so you call them directly from the class as the examples below demonstrates.

3 public class A01MathTest {
4   public static void main(String[] args) {
5          System.out.println("Random: " + Math.random() * 10);
6          System.out.println("Square root: " + Math.sqrt(9.0));
7          System.out.println("Rounded random: " +
8              Math.round(Math.random()*100));
9         System.out.println("Abs: " + Math.abs(-9));
10    }
11 }

Here is some sample output from this example:

Random: 7.064916553599695
Square root: 3.0
Rounded random: 35
Abs: 9

Implementing Static Methods

Use the static keyword before the method. The method has parameters and return types like normal.

3 import java.time.LocalDate;
4
5 public class StaticHelper {
6
7       public static void printMessage(String message) {
8           System.out.println("Messsage for " +
9               LocalDate.now() + ": " + message);
10     }
11
12  }

Static methods or class methods may be without an instantiating an object.

Static Method Limitations

Static methods can be used before any instances of their enclosing class have been created. If a class contains both static and instance components, the instance components cannot be accessed for a static context.

Calling Static Methods

When calling static methods, you should:
– Qualify the location of the method with a class name if the method is located in a different class than the caller – Not required for methods within the same class.
– Avoid using an object reference to call a static method.

double d = Math.random();
StaticHelper.printMessage("Hello");

Static Variables

Static variables are variables that can be accessed even if the class they are declared in has not been instantiated.

Static variables are:

  • Called class variables
  • Limited to a single copy per JVM
  • Useful for containing shared data:
    • Static methods store data in static variables.
    • All object instances share a single copy of any static variables.
  • Initialized when the containing class is first loaded

Class Loading

Application developer-supplied classes are typically loaded on demand (first use). Static variables are initialized when their enclosing class is loaded. An attempt to access a static class member can trigger the loading of a class.

Defining Static Variables

A static variable is defined when the static keyword precedes the type definition for a variable. Static variables are useful for containing shared data, all object instances share a single copy of any static variables.

4 public class StaticCounter {
5      private static int counter = 0;
6
7      public static int getCount() {
8            return counter;
9     }
10
11    public static void increment(){
12    counter++;
13    }
14  }

Using Static Variables

When accessing static variables, you should:
– Qualify the location of the variable with a class name if the variable is located in a different class than the caller – Not required for variables within the same class.
– Avoid using an object reference to access a static variable.

 double p = Math.PI;
5    public static void main(String[] args) {
6        System.out.println("Start: " + StaticCounter.getCount());
7        StaticCounter.increment();
8        StaticCounter.increment();
9        System.out.println("End: " + StaticCounter.getCount());
10}

Object References to Static Members

Just as using object references to static methods should be avoided, you should also avoid using object references to access static variables. Using a private access level prevents direct access to static variables.

Sample output:

Start: 0
End: 2

Static Initializers

Static initializer block is a code block prefixed by the static keyword.

 
3  public class A04StaticInitializerTest {
4    private static final boolean[] switches = new boolean[5];
5
6  static{
7    System.out.println("Initializing...");
8    for (int i=0; i<5; i++){
9        switches[i] = true;
10   }
11  }
12
13   public static void main(String[] args) {
14     switches[1] = false; switches[2] = false;
15     System.out.print("Switch settings: ");
16    for (boolean curSwitch:switches){
17     if (curSwitch){System.out.print("1");}
18     else {System.out.print("0");}
19  }

Here are some key facts about static initialization blocks:

  • They are executed only once when the class is loaded.
  • They are used to initialize static variables.
  • A class can contain one or more static initializer blocks.
  • They can appear anywhere in the class body.
  • The blocks are called in the order that they appear in the source code.

Consider using static initializers when nontrivial code is required to initialize static variables.

Static Imports

A static import statement makes the static members of a class available under their simple name.

Given either of the following lines:

import static java.lang.Math.random;
import static java.lang.Math.*;

Calling the Math.random()method can be written as:

public class StaticImport {
    public static void main(String[] args) {
       double d = random();
   }
}

Overusing static import can negatively affect the readability of your code. Avoid adding multiple static imports to a class.

Design Patterns

Design patterns are:

  • Reusable solutions to common software development problems
  • Documented in pattern catalogs – Design Patterns: Elements of Reusable Object-Oriented Software, written by Erich Gamma et al. (the “Gang of Four”)
  • A vocabulary used to discuss design

Design Pattern Catalogs

Pattern catalogs are available for many programming languages. Most of the traditional design patterns apply to any object-oriented programming language. One of the most popular books, Design Patterns: Elements of Reusable Object-Oriented Software, uses a combinationof C++, Smalltalk, and diagrams to show possible pattern implementations. Many Java developers still reference this book because the concepts translate to any object-oriented language.

Singleton Pattern

The singleton design pattern details a class implementation that can be instantiated only once.

Implementing the Singleton Pattern

A singleton is a class for which only one instance can be created and it provides a global point of access to this instance.

Singletons are useful to provide a unique source of data or functionality to other Java Objects. For example, you may use a singleton to access your data model from within your application or to define logger which the rest of the application can use.

To implement the singleton design pattern:

  1. Use a static reference to point to the single instance. Making the reference final ensures that it will never reference a different instance.
  2. Add a single private constructor to the singleton class. The private modifier allows only “same class” access, which prohibits any attempts to instantiate the singleton class except for the attempt in step 1.
  3. A public factory method returns a copy of the singleton reference. This method is declared static to access the static field declared in step 1. Step 1 could use a public variable, eliminating the need for the factory method. Factory methods provide greater flexibility (for example, implementing a per-thread singleton solution) and are typically used in most singleton implementations.
  4. In singleton pattern, it restricts the creation of instance until requested first time(Lazy initialization). To obtain a singleton reference, call the getInstance method: “SingletonClass ref = SingletonClass.getInstance();”

Singleton: Example

Singletons are great for storing data shared by an entire application. In this example, some database configuration data is stored in a Singleton.

 
3 public final class DbConfigSingleton {
4     private final String hostName;
5     private final String dbName;
6     //Lines omitted
10     private static final DbConfigSingleton instance =
11             new DbConfigSingleton();
12
13    private DbConfigSingleton(){
14      // Values loaded from file in practice
15      hostName = "dbhost.example.com";
16      // Lines omitted
20    }
21
22   public static DbConfigSingleton getInstance() {
23       return instance;
24   }

Immutable Classes

Immutable class:

  • It is a class whose object state cannot be modified once created.
  • Any modification of the object will result in another newn immutable object.
  • Example: Objects of Java.lang.String, any change on existing string object will result in another string; for example, replacing a character or creating substrings will result in new objects.

Rules to create a immutable class in Java:

  • State of immutable object can not be modified after construction. Any modification would result in a new immutable object.
  • Declare the class as final so it cannot be extended.
  • All fields are declared private so that direct access is not allowed.
  • Setter methods are not provided for variables.
  • All fields of immutable class should be final.
  • All the fields are initialized via a constructor.
  • Object should be final in order to restrict subclass from altering immutability of parent class.

Example: Creating Immutable class in Java

The example below demonstrates an immutable class in Java where all fields of class remain immutable and the class is also made final to avoid risk of immutability through inheritance.

  public final class Contacts {
   private final String firstName;
   private final String lastName;

 public Contacts(String fname,String lname) {
     this.firstName= fname;
     this.lastName = lname;

 }
 public String getFirstName() {
   return firstName;
 }
  public String getLastName() {
    return lastName;
 }
  public String toString() {
    return firstName +" - "+ lastName +" - "+ lastName;
}
}

Benefits of immutable classes in Java

  1. Immutable objects are by default thread-safe, and can be shared without synchronization in concurrent environment.
  2. Immutable object boost performance of Java application by reducing synchronization in code.
  3. Reusability, you can cache immutable object and reuse them, much like String literals and Integers.
Note: If Immutable class has many optional and mandatory fields, you can also use Builder Design Pattern to make a class Immutable in Java.
Related Post