Inheritance

Sample Application

The examples in this document are all part of the Java Application Inheritance. Download the source archive
Inheritance.zip
Extract and install it as a Java Application with Existing Sources. See the Using NetBeans document for details.

Inheritance

Inheritance in Java begins with the relationship between two classes defined like this:
class SubClass extends SuperClass
See the tutorial. Inheritance expresses the is a relationship in that SubClass is a (specialization of) SuperClass. The extends relation has many of the same characteristics of the implements relationship used for interfaces
class MyImplementationClass implements MyInterface
As with inheritance, we say that MyImplementationClass is a MyInterface.

Diagrammatically, these relationships are expressed in UML with the extends as a solid line and implements as a dashed line:
The is a relationship is transitive in that, if we have this hierarchy:
in which
ClassB is a ClassA
ClassC is a ClassB
then, by transitivity:
ClassC is a ClassA
The term base class is also used for superclass, and derived class as subclass. Being a subclass is also transitive in that we can say that:
ClassC  is a subclass of  ClassA
The term inheritance expresses the fact that the objects of the subclass inherit all the features of the superclass including data members and functions, although the private data members and functions of the superclass are not directly accessible.

Polymorphism

The word polymorphic means "having, assuming, or passing through many or various forms, stages, or the like." In Java object-oriented programming, it means that objects which appear to be the same by virtue of type behave differently by virtue of what they really are. We will rely on this as a running example whose UML diagram looks like this:
Expressed in Java notation:
class SubClass1 extends SuperClass { ... }
class SubClass2 extends SuperClass { ... }
class SubSubClass1 extends SubClass1 { ... }
The SuperClass can be used to express the type of any object in this hierarchy, thus we can write:
SuperClass objs[] = {
  new SubClass1(),
  new SubClass2(),
  new SubSubClass1(),
}
Suppose each class in the hierarchy, including the SuperClass, defines the member function
public void foo() { ... }
Then it at least makes sense to write:
for (SuperClass obj: objs) {
  obj.foo();
}
The question is: which "foo" is actually called in each case? It turns out that the foo() called is based the object's class, not the declared type, which is consistently SuperClass. Calling by class as opposed to type is referred to as dynamic binding in that the determination is made at runtime, not compile time.

Dynamic binding is the essence of polymorphsim in that the foo member function call means something different to each object in the array.

Textbook Introductory Example

GradedActivity is the base class, FinalExam extends it. GradeDemo is a simple test of GradedActivity and FinalExamDemo tests FinalExam, using its own operations and those inherited from the base class.
GradedActivity  
GradeDemo  
FinalExam  
FinalExamDemo  

A Basic Example

Using the class structure from the previous section, let's illustrate some key points with this pedagogical example found in the basic package:

basic.SuperClass
package basic;
 
public class SuperClass {
  public void top() {
    System.out.println("top in SuperClass");
  }
  public void pervasive() {
    System.out.println("pervasive in SuperClass");
  }
}

basic.SubClass1
package basic;
 
public class SubClass1 extends SuperClass {
  @Override
  public void pervasive() {
    System.out.println("pervasive in SubClass1");
  }
}

basic.SubClass2
package basic;
 
public class SubClass2 extends SuperClass {
  @Override
  public void pervasive() {
    System.out.println("pervasive in SubClass2");
  }
}

basic.SubSubClass1
package basic;
 
public class SubSubClass1 extends SubClass1 {
  @Override
  public void pervasive() {
    System.out.println("pervasive in SubSubClass1");
  }
  public void bottom() {
    System.out.println("bottom in SubSubClass1");
  }
}
The driver program is this:

basic.Driver
package basic;
 
public class Driver {
 
  public static void main(String[] args) {
    SuperClass[] objs = {
      new SuperClass(),
      new SubClass1(),
      new SubClass2(),
      new SubSubClass1(),
    };
 
    System.out.println("\n====> calls to top");
    for(SuperClass obj: objs) {
      System.out.print("---- obj in " + obj.getClass().getSimpleName());
      System.out.print(" calls: ");
      obj.top();
    }
 
    System.out.println("\n====> calls to pervasive");
    for(SuperClass obj: objs) {
      System.out.print("---- obj in " + obj.getClass().getSimpleName());
      System.out.print(" calls: ");
      obj.pervasive();
    }
 
    System.out.println("\n====> calls to bottom");
    for(SuperClass obj: objs) {
      System.out.print("---- obj in " + obj.getClass().getSimpleName());
      if (obj instanceof SubSubClass1) {
        System.out.print(" calls ");
        ((SubSubClass1) obj).bottom();
      }
      else {
        System.out.println();
      }
    }
 
    System.out.println("\n====> casting changes nothing about pervasive call");
    SuperClass obj = new SubSubClass1();
    System.out.print("---- obj in " + obj.getClass().getSimpleName());
    System.out.println(" casted to SubClass1 type, calls pervasive");
    ((SubClass1) obj).pervasive();
  }
}
The output of the driver program is:
====> calls to top
---- obj in SuperClass calls: top in SuperClass
---- obj in SubClass1 calls: top in SuperClass
---- obj in SubClass2 calls: top in SuperClass
---- obj in SubSubClass1 calls: top in SuperClass

====> calls to pervasive
---- obj in SuperClass calls: pervasive in SuperClass
---- obj in SubClass1 calls: pervasive in SubClass1
---- obj in SubClass2 calls: pervasive in SubClass2
---- obj in SubSubClass1 calls: pervasive in SubSubClass1

====> calls to bottom
---- obj in SuperClass
---- obj in SubClass1
---- obj in SubClass2
---- obj in SubSubClass1 calls bottom in SubSubClass1

====> casting changes nothing about pervasive call
---- obj in SubSubClass1 casted to SubClass1 type, calls pervasive
pervasive in SubSubClass1
The first section illustrates that subclasses have access to the public top member function defined only in the base class.

The second section verifies the dynamic binding concept. The member function pervasive calls the version defined within the object's class event though all objects are of type SuperClass

Casting

The third section of the output indicates how to access a member function not defined at the top level. Doing so is achieved by casting the object. In our example, SubSubClass1 contains the method bottom which no other class defines.

The cast operation means simply to alter the type like this:
(SubSubClass1) obj
It is usually intended to go down an inheritance path from superclass towards the class of the object. Casting cannot be used to "make a cat bark," so to speak. For example, if we were to use this code
SuperClass obj = new SubClass1();
 
(SubClass2) obj
// or
(SubSubClass1) obj
the runtime outcome would be a ClassCastException.

Casting an object used for a member function call up toward the SuperClass is legal, but useless, since it can never change the outcome of a member function, i.e.
SuperClass obj = new SubSubClass1();
 
((SubClass1) obj).pervasive();
will not be any different than if the cast were not used:
obj.pervasive();

The instanceof operator

In the polymorphic setting where objects of subclasses are typed by the superclass, we use the instanceof operator to determine the type prior to casting. Thus, casting is often combined with instanceof in the manner used in the example:
if (obj instanceof SubSubClass1) {
  ((SubSubClass1) obj).bottom();
}
The instanceof operator recognizes the "is a" relation of classes in that these two print statements print true values:
SuperClass obj1 = new SubSubClass1();
 
System.out.println("" + (obj1 instanceof SubClass1));
System.out.println("" + (obj1 instanceof SuperClass));

The getClass() and .class operators

In our basic.Driver demo program we used the Object-level member function, roughly typed like this:
Class obj.getClass()
to identify the actual class to which an object belongs. This return values is a "Class" object. Another way to obtain this Class object is the class attribute:
SuperClass.class
We now have 3 ways to measure whether an object, obj "belongs to" the class SuperClass:
obj instanceof SuperClass
obj.getClass() == SuperClass.class
obj.getClass() == new SuperClass().getClass()
The notion of "belongs to" is different for instanceof than the others because subclass objects also satisfy the instanceof requirement.

The following simple demo program illustrates these usages:

basic.GetClassTest
package basic;
 
public class GetClassTest {
 
  public static void main(String[] args) {
    System.out.println(SuperClass.class);
    System.out.println();
 
    SuperClass superclassobj;
 
    SuperClass[] objs = {
      superclassobj = new SuperClass(),
      new SubClass1(),
      new SubClass2(),
      new SubSubClass1(),
    };
    for (Object obj : objs) {
      System.out.println(obj.getClass());
 
      if (obj instanceof SuperClass) {
        System.out.println("object \"is a\" SuperClass object");
      }
 
      if (obj.getClass() == SuperClass.class) {
        System.out.println("v1: object is of type SuperClass");
      }
      if (obj.getClass() == superclassobj.getClass()) {
        System.out.println("v2: object is of type SuperClass");
      }
 
      System.out.println();
    }
  }
}
The output hopefully speaks for itself:
class basic.SuperClass

class basic.SuperClass
object "is a" SuperClass object
v1: object is of type SuperClass
v2: object is of type SuperClass

class basic.SubClass1
object "is a" SuperClass object

class basic.SubClass2
object "is a" SuperClass object

class basic.SubSubClass1
object "is a" SuperClass object

Method Override

The pervasive method in each subclass overrides the version in the superclass. NetBeans strongly suggests using the annotation:
@Override
and it is a very good idea because doing so will help you avoid errors.

For example, try this experiment:
  1. Edit SubClass1. Comment out the @Override line and simulate a "typing mistake" by changing the pervasive method name to pervesive (misspelled):
      //@Override
      public void pervesive() {
        System.out.println("SubClass1.pervasive");
      }
    
  2. Re-run Driver and observe the output change in the second group:
    ====> calls to pervasive
    ---- obj in SuperClass calls: pervasive in SuperClass
    ---- obj in SubClass1 calls: pervasive in SuperClass
    ---- obj in SubClass2 calls: pervasive in SubClass2
    ---- obj in SubSubClass1 calls: pervasive in SubSubClass1
    
    Of course! There no longer is a pervasive method in SuperClass1.
  3. NetBeans detects the problem at compile time if you un-comment the @Override:
      @Override
      public void pervesive() {
        System.out.println("SubClass1.pervasive");
      }
    
    The @Override line is now flagged with the error message:
    method does not override or implement a method from the supertype
  4. Fix the "typing mistake" by changing the method back to its original name pervasive. Re-run Driver to confirm the fix.

Overload vs. Override

Overriding means reusing the same method name in a subclass. In addition to being the same name, it must also have the same argument type signature. If the type signature is different, then the method is overloaded, and it doesn't override the inherited method.

Here is an example, from the overload package:

overload.SuperClass
package overload;
 
public class SuperClass {
  public void callMe(SuperClass s) {
    System.out.println("SuperClass: callMe");
  }
}

overload.SubClass
package overload;
 
public class SubClass extends SuperClass {
 
  @Override
  public void callMe(SuperClass s) {
    System.out.println("SubClass: callMe overridden");
  }
 
  // overloaded, not overridden: attempting @Override is an error
  //@Override
  public void callMe(SubClass s) {
    System.out.println("SubClass: callMe overloaded");    
  }
}

overload.Driver
package overload;
 
public class Driver {
 
  public static void main(String[] args) {
    SuperClass top = new SuperClass();
    SubClass below = new SubClass();
 
    // both SuperClass calls
    top.callMe(top);
    top.callMe(below);
 
    System.out.println();
 
    // both in SubClass calls, but,
    below.callMe(top);   // overridden
    below.callMe(below); // overloaded
 
    // a type change by casting will call the overridden method
    below.callMe((SuperClass) below);
  }
}
The SubClass supports both the overridden and an overloaded callMe methods. What differentiates their usage is the type of the object argument.

Textbook Examples

GradedActivity  
CurvedActivity  
CurvedActivityDemo  
SuperClass3  
SubClass3  
ShowValueDemo  

Abstract and final classes

A class can be declared with the abstract qualifier, e.g.:
public abstract class SuperClass 
When this is added, it means that no object can be instantiated from this class and so must have subclasses for it to be useful.

Experiment with abstract

Using the classes from the basic package above:
  1. Edit SuperClass, changing the declaration line to the following and save the changes:
    public abstract class SuperClass {
    
    When you save the changes, you'll see that the Driver class now has an error.
  2. Edit Driver and observe NetBeans' response. Fix it by commenting out the flagged line:
    //new SuperClass(),
    
    Re-run the driver program observe the only change is reflected by the missing object.
  3. Reset SuperClass and Driver back to their original states.
Note that the top member function in SuperClass is unaffected by making the class abstract. An abstract class is like an interface in that it must be extended to be used, but unlike an interface in that it usually does have functionality whereas an interface has no functionality, only prototypes.

Final classes

Declaring a class final means that it cannot be extended; it is effectively the exact opposite of abstract. Final classes are the "true leaves" in an inheritance tree diagram. As a simple experiment with our basic example, add final onto the declaration line of SubClass1:
public final class SubClass1 {
Save the changes and observe that the class declaration in the file SubSubClass1 is now flagged with the obvious error message. Again, undo the change in SubClass1 to fix it.

Java Examples

Numbers in java.lang

All the numeric classes in java.lang extend the Number class as well as the ones in java.math: java.math.BigDecimal, java.math.BigInteger. Here is an excerpt:
The Number class is abstract. The numeric wrapper classes Integer, Double, etc. are final, but the others, such as BigDecimal are not.

Here is a program which illustrates a polymorphic collection of Numbers to which the function doubleValue is applied

numbers.ShowNumbers
package numbers;
 
public class ShowNumbers {
 
  public static void main(String[] args) {
    Number[] nums = {
      100,              // int => Integer
      333.44,           // double => Double
      (float) 333.44,   // float => Float
      new java.math.BigDecimal(111.555555555555555555555555555),
    };
    for(Number num: nums) {
      System.out.println(num.doubleValue());
    }
  }
}
You can always interchange, say, 100 with new Integer(100) and 333.44 with new Double(333.44). The transformation of putting a number in its wrapper is called "boxing," and the opposite of taking it out of its wrapper is "unboxing."

Swing GUI components

Here is an excerpt of the Java Swing GUI classes, presenting the ones we have used so far in the course as well as others:
The package paths in this portion of the GUI hierarchy are easy to characterize: any class whose name begins with "J" is in the javax.swing package, except for javax.swing.text.JTextComponent. AbstractButton is also in javax.swing. Otherwise the class is in the java.awt package.

These classes are all abstract:
Component
JComponent
AbstractButton
JTextComponent

Base Class Constructors

Assume the class relationship:
class SubClass extends SuperClass
Whenever a SubClass object is instantiated, the SuperClass portion of it must be first instantiated through a SuperClass constructor.

Unless otherwise controlled, the no-argument SuperClass constructor will be called if present. If no constructors are defined, the default constructor is to "do nothing."

Start with this example:

samples.ConstructorCalls
package samples;
 
class SuperClass {
  private int n;
  public SuperClass() { 
    n = 11;
    System.out.println("SuperClass: " + n); 
  }
  public int getN() { return n; }
}
 
class SubClass extends SuperClass {
  private int m;
  public SubClass() { 
    m = 5 + getN();
    System.out.println("SubClass: " + m); 
  }  
}
 
public class ConstructorCalls {
  public static void main(String[] args) {
    SuperClass obj = new SubClass();
  }
}
The run of the Driver program would be:
SuperClass: 11
SubClass: 16
If there is some constructor defined, but not the no-argument constructor, then the SubClass must explicitly call a base class constructor using the the Java super keyword. For example, the following code variation would not compile without the added "super(11)" statement:
class SuperClass {
  private int n;
  public SuperClass(int n) {                   // modified
    this.n = n;                                // modified
    System.out.println("SuperClass: " + n); 
  }
  public int getN() { return n; }
}
 
class SubClass extends SuperClass {
  private int m;
  public SubClass() {
    super(11);                                 // added
    m = 5 + getN();
    System.out.println("SubClass: " + m); 
  }  
}
Whenever the super constructor is used, it must be the very first statement within the subclass constructor.

People Example

We'll look at the demo classes in the people package. The UML diagram is:
This is the first time that we are using data members within the classes. The data members of the base class are automatically part of the derived class. In this case the data presentation of the objects in this hierarchy are this:
Person Employee Staff Admin Student
name name name name name
id id id id id
salary salary salary gpa
office office
parkingSpace
Even though the derived objects have access to the base class data, they cannot use it directly because they are declared private. Here is the code:

people.Person
package people;
 
public class Person {
  private String name;
  private int id;
  public Person(String name, int id) {
    this.name = name;
    this.id = id;
    System.out.printf("==> Person(%s,%s)\n",name,id);
  }
}

people.Employee
package people;
 
public class Employee extends Person {
  private double salary;
  public Employee(String name, int id, double salary) {
    super(name,id);
    this.salary = salary;
    System.out.printf("==> Employee(%s,%s,%s)\n",name,id,salary);
  }
}

people.Student
package people;
 
public class Student extends Person {
  private double gpa;
  public Student(String name, int id, double gpa) {
    super(name, id);
    this.gpa = gpa;
    System.out.printf("==> Student(%s,%s,%s)\n",name,id,gpa);
  }
}

people.Staff
package people;
 
public class Staff extends Employee {
  private String office;
  public Staff(String name, int id, double salary, String office) {
    super(name, id, salary);
    this.office = office;
    System.out.printf("==> Staff(%s,%s,%s,%s)\n",name,id,salary,office);
  }
}

people.Admin
package people;
 
public class Admin extends Staff {
  private int parkingSpace;
  public Admin(String name, int id, double salary, String office, 
               int parkingSpace) {
    super(name, id, salary, office);
    this.parkingSpace = parkingSpace;
    System.out.printf("==> Admin(%s,%s,%s,%s,%s)\n",
                           name,id,salary,office,parkingSpace);
  }
}

people.Driver
package people;
 
public class Driver {
  public static void main(String[] args) {
    new Person("Joe Walsh", 111);
    System.out.println();
    new Employee("Don Henley", 222, 5010.33);
    System.out.println();
    new Staff("Glenn Frey", 333, 6312.33, "UNA3333");
    System.out.println();
    new Admin("Randy Meisner", 444, 7302.95, "AND112", 123);
    System.out.println();
    new Student("Bernie Leadon", 555, 3.02);
  }
}
In this example, the no-argument constructor is not available in any class throughout this inheritance hierarchy, and so we are consistently forced to use the super constructor:
class SubClass extends ... {
  SubClass(...) { 
    super( ... );
  }
}
Do the following experiment to verify the need for this by doing the following test:
  1. Edit Staff first and comment out the super statement and save:

    people.Staff
    public class Staff extends Employee {
      private String office;
      public Staff(String name, int id, double salary, String office) {
        //super(name, id, salary);
        this.office = office;
      }
    }
    The error message given is saying that the no-argument constructor cannot be applied to Employee. The Admin subclass does not register any errors.
  2. Now edit Employee, comment out the constructor and save:

    people.Employee
    package people;
     
    public class Employee extends Person {
      private double salary;
    //  public Employee(String name, int id, double salary) {
    //    super(name,id);
    //    this.salary = salary;
    //  }
    }
    You'll see an error of the same nature appear in Employee in that it cannot use the no-argument Person constructor. However, now the previous error in Staff disappears because there it can use the default no-argument constructor in Employee.
  3. You can "fix" this last problem by introducing a no-arg constructor into the Person class:

    people.Person
    package people;
     
    public class Person {
      private String name;
      private int id;
      public Person() {
      }
      public Person(String name, int id) {
        this.name = name;
        this.id = id;
        System.out.printf("==> Person(%s,%s)\n",name,id);
      }
    }
Finally undo the changes back to the original state.

Textbook Examples

This introduces the Cube class as an extension of the Rectangle class by adding a third dimension. The Cube constructor uses the 2-arg constructor from the Rectangle class.
Rectangle  
Cube  
CubeDemo  

The Object class

This is discussed in section 10.6 of the textbook. Inheritance is at the core of Java in that every class is a subclass of java.lang.Object. That is to say, this holds for any pre-existing Java class:
class AnyJavaClass extends SomeOtherClass extends ... extends Object
or
class AnyJavaClass is an Object
For example, in the java.lang package:
class Number extends Object ...
class Integer extends Number ...
Any class that you define which does not explicitly extend some other class automatically extends Object, i.e. writing, for example
public class User { 
  private String name;
  public User(String name) { this.name = name; }
}
implicitly becomes
public class User extends Object { 
  private String name;
  public User(String name) { this.name = name; }
}
The Object class supports a few crucial public methods. The most important methods for this course are
public String toString()
public boolean equals(Object)
public Class getClass()
The discussions in the next two sections use examples from the equality package.

The toString method

Refer to the class:
equality.User  
Every object has a string representation. Suppose I took the User class defined above and apply these statements:
User joe = new User("Joe Smith");
System.out.println(joe);
The println operation implicitly uses
joe.toString()
and the result would be something like this:
packagename.User@11ace672
The hexadecimal code following the "@" is the object's memory address. The default toString() output is not terribly useful and so we usually want to override it; for example, the most obvious would be:
public class User { 
  ..
  @Override
  public String toString() { return name; }
}
Try this test program with and without the toString member commented in the User class.

equality.PrintUser
package equality;
 
public class PrintUser {
  public static void main(String[] args) {
    User joe = new User("Joe Smith");
    System.out.println( joe );
  }
}
Keep in mind the importance of the @Override annotation to avoid a misspelling like
public String tostring() { ... }

The equals method

The equals method is the second most important Object method. It's usage within a class is as the member function:
@Override
public boolean equals(Object obj) {
  ...
}
By default, the Object equals testing for user-defined classes is based on identity, but classes which use equality testing usually override the Object-level equals so that the test is somehow based on the object's content. This following program illustrates ".equals" comparisons for common wrapper classes:

equality.Wrappers
package equality;
 
public class Wrappers {
 
  public static void main(String[] args) {
    Integer m = new Integer(33);
    Integer n = m;
    Integer p = new Integer(33);
    Integer q = 55;
 
    String s = new String("33");
    Double d = new Double(33);
 
    System.out.println("m == n: " + (m==n));
    System.out.println("m == p: " + (m==p));
    System.out.println();
 
    System.out.println("m.equals(n): " + m.equals(n));
    System.out.println("m.equals(p): " + m.equals(p));
    System.out.println("m.equals(q): " + m.equals(q));
    System.out.println("m.equals(s): " + m.equals(s));
    System.out.println("m.equals(d): " + m.equals(d));
    System.out.println("m.equals(null): " + m.equals(null));
    System.out.println();
 
    String str1 = "22";
    String str2 = "22";
    String str3 = "2" + "2";
    String str4 = "2233".substring(0, 2);
 
    System.out.println("str1 == str2: " + (str1 == str2));
    System.out.println("str1 == str3: " + (str1 == str3));
    System.out.println("str1 == str4: " + (str1 == str4));
    System.out.println("str1.equals(str4): " + (str1.equals(str4))); 
  }
}
The String class is different from the number wrappers class in that Java "caches" strings to avoid duplication under certain circumstances, making "equals" and "==" behave in the same way. Of course, you should not rely on this behavior, and (almost) always use "equals".

equals for user-defined classes

Suppose we consider the User classes defined above. Here are the relevant classes:
equality.User  
equality.SpecialUser  
We want to override equals so that it is based on name content. Here is a test program:

equality.UserEquality
package equality;
 
public class UserEquality {
  public static void main(String[] args) {
    User joe1 = new User("Joe Smith");
    User joe2 = new User("Joe Jones");
    User joe3 = new User("Joe Smith");
 
    User joeSpecial = new SpecialUser("Joe Smith");
 
    System.out.println("joe1 = " + joe1);
    System.out.println("joe2 = " + joe2);
    System.out.println("joe3 = " + joe3);
    System.out.println();
 
    System.out.println("joe1.equals(joe2) = ");
    System.out.println( joe1.equals(joe2) );
    System.out.println();
 
    System.out.println("joe1.equals(joe3) = ");
    System.out.println( joe1.equals(joe3) );
    System.out.println();
 
    System.out.println("joe1.equals(null) = ");
    System.out.println( joe1.equals(null) );
    System.out.println();
 
    System.out.println( "joe1.equals((Object) joe3) = " );
    System.out.println( joe1.equals( (Object)joe3 ) );
    System.out.println();
 
    System.out.println("joe1.equals(joeSpecial) = ");
    System.out.println( joe1.equals(joeSpecial) );
    System.out.println();
 
    System.out.println("joe1.equals(\"Joe Smith\") = ");
    System.out.println( joe1.equals("Joe Smith") );
    System.out.println();
   }
}
Running this gives the expected false value for every equals comparison because all objects are distince. However what you what you want is
joe1.equals(joe3) = true
because joe1 and joe3 have identical name fields. Here is a sequence of steps toward the desired solution:
  1. Initially we want to overload the equals method and deal with the null argument.
    public class User { 
      ...  
      public boolean equals(User user) {
        System.out.println("==> overloaded, user=" + user);
        if (user == null) {
          return false;
        }
        return name.equals(user.name);
      }
    }
    The overloaded method correctly gets:
    joe1.equals(joe3) = true
    
    However it fails in something else we want to be true:
    joe1.equals((Object) joe3) = false
    
    Equality of two objects should not be dependent on the type cast.
  2. The overloaded equals does not work in general, so go to the overridden equals. the parameter. Our first attempty is this:
    public class User { 
      ...  
      @Override
      public boolean equals(Object obj) {    
        if (obj == null) {
          return false;
        }
     
        User user = (User) obj;
        return name.equals(user.name);
      }
    }
    The thing that would go wrong is an error from this:
    joe1.equals("Joe Smith")
    
    In particular, the obj must be a non-null User for the cast statement to work.
  3. A next step is:
    public class User { 
      ...  
      @Override
      public boolean equals(Object obj) {   
        if (obj == null) {
          return false;
        }
     
        if (! (obj instanceof User) ) {
          return false;
        }
     
        User user = (User) obj;
        return name.equals(user.name);
      }
    }
    With this in place we get the desired outcome:
    joe.equals((Object) joe3) = true
    
    Because the null value never belongs to any class, so we can actually drop the initial null test, leaving:
    public class User { 
      ...  
      @Override
      public boolean equals(Object obj) {   
        if (! (obj instanceof User) ) {
          return false;
        }
     
        User user = (User) obj;
        return name.equals(user.name);
      }
    }
  4. The instanceof also gives something which we may not want:
    User joeSpecial = new SpecialUser("Joe Smith");
    joe1.equals(joeSpecial) = true
    
    These two objects should be different since they come from different classes.

    A more restrictive version is one which ensures that the equals argument is exactly of the right class, not of a subclass. It can be written like this:
    public class User { 
      ...  
      @Override
      public boolean equals(Object obj) {   
        if (obj == null) {
          return false;
        }
     
        if (obj.getClass() != User.class) {
          return false;
        }
     
        User user = (User) obj;
        return name.equals(user.name);
      }
    }
    Alternatively, it can be written like this:
    public class User { 
      ...  
      @Override
      public boolean equals(Object obj) {   
        if (obj == null) {
          return false;
        }
     
        if (obj.getClass() != this.getClass()) {
          return false;
        }
     
        User user = (User) obj;
        return name.equals(user.name);
      }
    }
The fact that "==" is OK to use to compare Class object is a subtle point. The Class class does not override equals, meaning that equals for Class objects is "==". Underneath the surface, it means that Java keeps track of classes in use by caching them in some form so as to make all classes which are the same be identical.

Observe the correct equals in an ArrayList of Users

Try running this program using an ArrayList of Users:

equality.ListUserEquality
package equality;
 
import java.util.ArrayList;
import java.util.List;
 
public class ListUserEquality {
 
  public static void main(String[] args) {
    User joe1 = new User("Joe Smith");
    User joe2 = new User("Joe Jones");
    User joe3 = new User("Joe Smith");
 
    List<User> users = new ArrayList<>();
    users.add(joe1);
    users.add(joe2);
 
    System.out.println("users.contains(joe3) = " + users.contains(joe3));
  }
}
According to the intended name comparison, we want the outcome to be true. Try manipulating the User class in 3 ways and observe the outcome:
  1. Comment out both the overloaded and overridden equals. Observe the expected false result.
  2. Use the overloaded equals. Observe the (perhaps unexpected) false result.
  3. Use the overridden equals method. Observe the desired true result.
What is happening here? The issue is that behind the scenes in Java's ArrayList, the underlying array is regarded as an array of Objects. Results given to the user are cast as the generic type at the very end.

Exceptions

Exception usage can only be fully explained with inheritance concepts in place. Here is an except from the Java Exception hierarchy:
The full package paths (with documentation links) are these:
java.lang.Throwable
java.lang.Exception
java.io.IOException
java.io.EOFException
java.nio.file.FileSystemException
java.nio.file.NoSuchFileException
java.nio.file.AccessDeniedException
java.io.ObjectStreamException
java.io.NotSerializableException
java.lang.RuntimeException
java.lang.IndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.IllegalArgumentException
java.lang.NumberFormatException
java.util.NoSuchElementException
java.util.InputMismatchException
java.lang.ReflectiveOperationException
java.lang.ClassNotFoundException
java.lang.ClassCastException

Runtime Exceptions

The RuntimeException hierarchy consists of exceptions which do not have to be caught or declared in a throws clause for statements which generate them. They are a portion of the slightly larger group of Throwable objects called Unchecked Exceptions which includes Error objects as well. For example in the above hierarchy, these two closely related exceptions are different:

Sequence in a multi-catch block

The inheritance hierarchy gives meaning to the way a try/catch blocks work. Consider the outline:
try {
  // exception-generating code
} 
catch(ExceptionType1 ex) {
  // ...
} 
catch(ExceptionType2 ex) {
  // ...
} 
catch(ExceptionType3 ex) {
  // ...
} 
...
catch(Exception ex) {
  // this block will catch any uncaught exception
}
We said an exception generated falls into the first catch block whose type matches the thrown exception type. By matches we now specifically mean the is a relation. In particular, since every exception type is an Exception, the last catch block will catch everything.

The ordering of Exception types in catch blocks must be this, for M < N:
ExceptionTypeM   is an   ExceptionTypeN
or
ExceptionTypeM   and   ExceptionTypeN   are unrelated (neither is a subclass of the other)

They never go down the chain from superclass to subclass, e.g., this is wrong (NetBeans would report an error):
try {
  // exception-generating code
} 
catch(IOException ex) {
  // ...
} 
catch(NoSuchFileException ex) {
  // ...
}
Another consequence is that if Exception is used as one of catch blocks it must be the last one.

User-defined Exception classes

In the Throwing an Exception section of the Exceptions document, we described how to create a user-controlled exit of a try block using the statement
try {
  ...
  throw new Exception("my message");
  ...
}
catch(Exception ex) {
  // handle user-generated throw
}
Under these circumstances, the single catch(Exception ex) block is meant to handle all user-generated exceptions in the same way. What if we want to have more than one way to handle user-generated exceptions? Based on what we know now, we can create our own exception types, throw and handle those.

If we want a one-time usage of our user-generated exceptions, we can make them be local classes like this:
class MyException extends Exception {
  MyException(String msg) { 
    super(msg); 
  }
}
 
try {
  ...
  throw new MyException("my message");
  ...
}
...
catch(MyException ex) {
  // handle user-generated throw
}
Observe how our newly defined class must use the base class constructor
super(msg);
in order to carry a message with the thrown exception.

Exception listing in throws clause

Exceptions which are not subclasses of RuntimeException, if not caught, must be declared in a throws clause from a function where they are generated
public void myFunction() throws LIST_OF_EXCEPTION_TYPES {
  // exception-generating code
  // for non-runtime exceptions
}
This LIST_OF_EXCEPTION_TYPES must match all the types generated, but not caught. RuntimeExceptions do not have to be listed. The broadest, least informative way to achieve the effect is this:
public void myFunction() throws Exception {
  // exception-generating code
  // for non-runtime exceptions
}

Examples

The following demo programs illustrate the creation and usage of user-defined Exception classes. Look in the package
exceptions
In addition to illustrating user-defined exception types, we're making a pedantic point of our knowledge that the Double.parseDouble function generates an exception type which is a subclass of RuntimeException; in general you should make the exception a closer match to the exception generated by the scanner.

exceptions.UserDefined
package exceptions;
 
import java.util.Scanner;
 
public class UserDefined {
 
  public static void main(String[] args) {
 
    class TooBigException extends Exception {
      TooBigException(String msg) { 
        super(msg); 
      }
    }
 
    class TooSmallException extends Exception {
      TooSmallException(String msg) { 
        super(msg); 
      }
    }
 
    Scanner in = new Scanner(System.in);
    while (true) {
      try {
        System.out.print("==> enter number (empty to stop): ");
        String line = in.nextLine().trim();
        if (line.isEmpty()) {
          break;
        }
        double x = Double.parseDouble(line);
        if (x > 1000) {
          throw new TooBigException("Too big");
        }
        else if ( x < -1000) {
          throw new TooSmallException("Too small");          
        }
        System.out.println("Just Right");
      }
      catch (RuntimeException ex) {
        System.out.println("Invalid Double");
      }
      catch(TooBigException ex) {
        System.out.println(ex.getMessage());
      }
      catch(TooSmallException ex) {
        System.out.println(ex.getMessage());
      }
    }
  }
}
The classes are local because we don't need them other places. If you think you're going to reuse these classes within other functions, or declare them at the main function, then pull them out to either the file level as they are, or to the class level like this:
public class UserDefined {
 
  private static class TooBigException extends Exception {
    TooBigException(String msg) { 
      super(msg); 
    }
  }
 
  private static class TooSmallException extends Exception {
    TooSmallException(String msg) { 
      super(msg); 
    }
  }
 
  ...
}
Here is a demo program which uses user-defined exceptions and illustrates possibilities for the throws clause. The UML diagram of the classes is

exceptions.ThrowsClause
package exceptions;
 
class ExceptionA extends Exception {
  public ExceptionA(String message) {
    super(message);
  }
}
 
class ExceptionB extends Exception {
  public ExceptionB(String message) {
    super(message);
  }
}
 
class ExceptionC extends ExceptionB {
  public ExceptionC(String message) {
    super(message);
  }
}
 
public class ThrowsClause {
 
  public static void testFunction()
                             throws ExceptionA, ExceptionB, ExceptionC 
  {
    java.util.Random rand = new java.util.Random();
    int x = rand.nextInt(40);
    if (x < 10) {
      throw new ExceptionA("test1: " + x);
    }
    else if (x < 20) {
      throw new ExceptionB("test2: " + x);
    }
    else if (x < 30) {
      throw new ExceptionC("test3: " + x);
    }
    System.out.println("no exception: " + x);
  }
 
  public static void main(String[] args) 
                             throws ExceptionA, ExceptionB, ExceptionC 
  {
    testFunction();
  }
}
select
According to our logic, the possible alternatives for the throws clause used in main are these:
throws ExceptionA, ExceptionB, ExceptionC   (list all)
throws ExceptionA, ExceptionB               (ExceptionC is an ExceptionB)
throws Exception                            (all are an Exception)
The ordering of the exception classes in the throws statement is irrelevant, i.e., the second choice could equally be:
throws ExceptionA, ExceptionB 
throws ExceptionB, ExceptionA
Suppose we want to avoid using the throws clause used in main. What must be added in main? Here, of course, we must introduce a try/catch block, and the ordering of the catch clauses matters.

General Suggestions for Exception Handling

Before we complete our discussion of Exceptions, it's important to state a few general rules.
  1. For the most part you should allow exceptions to be thrown out of helper functions, catching them at the program's "top level," like the main function or within the controller constructor of a GUI application.
  2. You must catch all exceptions generated in event handler code itself, because there is no "level above" which can catch them.
  3. Throws clauses should be made as specific as possible, stating all specific exception types instead of just "throws Exception".
  4. The try/catch clauses used should attempt to present a catch clause for more than just the general Exception type. Java supports multi-catch clauses for those with the same catch body (we haven't presented this point).
The last two rules are basically saying "make the program as informative as possible." In practice, you can fudge on this level of rigor, for example, using IOException to handle any type of file-related exception.

Exceptions in Reading/Writing Objects

Referring back to the section about Serializability in the File I/O document, we can now give a full explanation of the exception handling. Examples are in the rwobjects package; these are variants of the programs in the serial package of FileIO. The classes are:
Person
WriteObject
ReadObject

Writing an Object


rwobjects.WriteObject
package rwobjects;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
 
public class WriteObject {
 
  private static void writeObject(Object obj, File f) throws IOException {
    FileOutputStream ostr = new FileOutputStream(f);
    ObjectOutputStream obj_ostr = new ObjectOutputStream(ostr);
    obj_ostr.writeObject(obj);
    obj_ostr.close();
  }
 
  public static void main(String[] args) {
    try {
      Path pathBad = Paths.get("subdir", "person.dat");
      Path pathGood = Paths.get("person.dat");
 
      Path path = pathGood;
      //Path path = pathBad;
 
      Person joe = new Person("Joe Cool", 23);
 
      System.out.println("Writing Person joe to file: " + path);
 
      writeObject( joe, path.toFile() );
    }
    catch (NotSerializableException ex) {
      System.err.println("-- not serializable --");
    }
    catch (IOException ex) {
      System.err.println("-- bad path --");
    }
  }
}
Two exceptions are caught, all generated from the writeObject helper function. They can be created by:
  1. commenting out the line in Person:
    implements Serializable
    
  2. changing to the "bad" path (a non-existent directory) in main:
    Path path = pathBad;
    
Let's analyze the exceptions in writeObject according to the Java documentation: Putting it together:

Reading an Object

Here's the demo program:

rwobjects.ReadObject
package rwobjects;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
 
public class ReadObject {
 
  private static Object readObject(File f)
      throws IOException, ClassNotFoundException 
  {
    FileInputStream istr = new FileInputStream(f);
    ObjectInputStream obj_istr = new ObjectInputStream(istr);
    Object obj = obj_istr.readObject();
    obj_istr.close();
    return obj;
  }
 
  public static void main(String[] args) {
    try {
      File fileGood = new File("person.dat");
      File fileBad = new File("nobody.dat");
 
      File file = fileGood;
      //File file = fileBad;
 
      Object obj = readObject(file);
      System.out.println("object class = " + obj.getClass());
 
      //Integer thePerson = (Integer) obj;
    }
    catch (IOException ex) {
      System.err.println("-- missing file --");
    }
    catch (ClassNotFoundException ex) {
      System.err.println("-- no such class --");
    }
    catch (ClassCastException ex) {
      System.err.println("-- wrong cast --");
    }
  }
}
Three exceptions are caught, two generated from the readObject helper function and one in main. They can be created by:
  1. changing to the "bad" file in main:
    File file = fileBad;
    
  2. moving the Person class out of the rwobjects package into the rwobjects.hold package
  3. uncommenting the line in main:
    Integer thePerson = (Integer) obj;
    
Again analyzing the exceptions in readObject according to the Java documentation: Putting it together:

Calling overridden methods

The super keyword, besides activating the base class constructor, can be used to call a method defined in the base class which is overridden by the derived class. Consider this very simple example, contained in one file in the dedicated supercalls package:

supercalls.Driver
package supercalls;
 
class SuperClass {
  public void foo() {
    System.out.println("SuperClass.foo");
  }
}
 
class SubClass extends SuperClass {
  @Override
  public void foo() {
    System.out.println("SubClass.foo");
    super.foo();
  }
}
 
class SubSubClass extends SubClass {
  @Override
  public void foo() {
    System.out.println("SubSubClass.foo");
    super.foo();
  }
}
 
public class Driver {
  public static void main(String[] args) {
    SuperClass obj = new SubSubClass();
    obj.foo();
  }
}
The output of the run is:
SubSubClass.foo
SubClass.foo
SuperClass.foo
Unlike the usage of super to construct the base class, the member function calls using "super." do not have to be the first statement. Also keep in mind that, without the "super." prefix, the foo() calls in derived objects would be infinitely recursive, e.g.:
class SubSubClass extends SubClass {
  @Override
  public void foo() {
    System.out.println("SubSubClass.foo");
    foo();
  }
}
The foo() call is the same as this.foo(), but because of the dynamic binding, casting this to a superclass type will not make any difference, i.e.,
((SubClass)this).foo()     is no different than     foo()

The people hierarchy with toString method

Here is a rewrite of the people package example used above to illustrate how the toString function can be defined at all levels below Person by referencing the member function
super.toString
Included here is a simple main class Driver to test the effectiveness.
superpeople.Person: Show Hide
package superpeople;
 
public class Person {
  private String name;
  private int id;
  public Person(String name, int id) {
    this.name = name;
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public int getId() {
    return id;
  }
  @Override
  public String toString() {
    return String.format("%s, id:%s", name, id);
  }
}
superpeople.Employee: Show Hide
package superpeople;
 
public class Employee extends Person {
  private double salary;
  public Employee(String name, int id, double salary) {
    super(name,id);
    this.salary = salary;
  }
  @Override
  public String toString() {
    return super.toString() + String.format(", sal:%s", salary);
  }
}
superpeople.Staff: Show Hide
package superpeople;
 
public class Staff extends Employee {
  private String office;
  public Staff(String name, int id, double salary, String office) {
    super(name, id, salary);
    this.office = office;
  }
  @Override
  public String toString() {
    return super.toString() + String.format(", ofc:%s", office);
  }
}
superpeople.Admin: Show Hide
package superpeople;
 
public class Admin extends Staff {
  private int parkingSpace;
  public Admin(String name, int id, double salary, String office, int parkingSpace) {
    super(name, id, salary, office);
    this.parkingSpace = parkingSpace;
  }
  @Override
  public String toString() {
    return super.toString() + String.format(", prk:%s", parkingSpace);
  }
}
superpeople.Student: Show Hide
package superpeople;
 
public class Student extends Person {
  private double gpa;
  public Student(String name, int id, double gpa) {
    super(name, id);
    this.gpa = gpa;
  }
  @Override
  public String toString() {
    return super.toString() + String.format(", gpa:%s", gpa);
  }
}
superpeople.Driver: Show Hide
package superpeople;
 
public class Driver {
 
  public static void main(String[] args) {
    Person people[] = {
      new Person("Joe Walsh", 111),
      new Employee("Don Henley", 222, 5010.33),
      new Staff("Glenn Frey", 333, 6312.33, "UNA3333"),
      new Admin("Randy Meisner", 444, 7302.95, "AND112", 123),
      new Student("Bernie Leadon", 555, 3.02),
    };
 
    for(Person person : people) {
      System.out.println(person);
    }
  }
 
}

The protected access qualifier

This is discussed in section 10.4 of the textbook. Our current list of access qualifiers is this: We're going to add the fourth and last qualifier
protected
A protected member of a class is one which, like "none", is accessible to classes within the package. However, it is accessible to derived classes outside the package as well. The complete access heirarchy is thus: Let's return to our people example by changing private fields to protected. This time, use the people1 package.

people1.Person
package people1;
 
public class Person {
  protected String name;
  protected int id;
  protected Person() { }
 
  // the rest is the same as before
  public Person(String name, int id) {
    this.name = name;
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public int getId() {
    return id;
  }
  @Override
  public String toString() {
    return String.format("Person(%s,%s)", name, id);
  }
}

people1.Employee
package people1;
 
public class Employee extends Person {
  protected double salary;
  protected Employee() { }
  public Employee(String name, int id, double salary) {
    this.name = name;
    this.id = id;
    this.salary = salary;
  }
}

people1.Student
package people1;
 
public class Student extends Person {
  protected double gpa;
  public Student(String name, int id, double gpa) {
    this.name = name;
    this.id = id;
    this.gpa = gpa;
  }
}

people1.Staff
package people1;
 
public class Staff extends Employee {
  protected String office;
  protected Staff() { }
  public Staff(String name, int id, double salary, String office) {
    this.name = name;
    this.id = id;
    this.salary = salary;
    this.office = office;
  }
}

people1.Admin
package people1;
 
public class Admin extends Staff {
  protected int parkingSpace;
  public Admin(String name, int id, double salary, String office, int parkingSpace) {
    this.name = name;
    this.id = id;
    this.salary = salary;
    this.office = office;
    this.parkingSpace = parkingSpace;
  }
}
Making the fields protected allows them to be set in constructor of derived classes. We still, however, have to deal with the need of derived constructors activating some base class constructor. In this example, we simply provide the no-argument, empty constructor in the superclass as:
protected SuperClass() { }
Making it protected means it cannot be used outside the hierarchy to create objects with un-initialized fields.

Textbook Examples

GradedActivity2  
FinalExam2  
ProtectedDemo  

Abstract and final members

This is discussed in section 10.8 of the textbook. Recall that an abstract class is one which cannot be instantiated directly implying that it must be extended to be used. Also, a final class is one which cannot be extended. These qualifiers also apply to member functions. Here is an example of their usage; it is in the package
absfinal
The commented sections are to be used for illustrating the behaviors of these two qualifiers.

absfinal.SuperClass
package absfinal;
 
public abstract class SuperClass {
  public void foo() { System.out.println("Super:foo"); }
//  public abstract void foo();
 
  public void bar() { System.out.println("Super:bar"); }
//  public final void bar() { System.out.println("Super:bar"); }
}
select

absfinal.SubClass
package absfinal;
 
public class SubClass extends SuperClass {
  @Override
  public void foo() { System.out.println("foo"); }
 
//  @Override
//  public final void bar() { System.out.println("bar"); }
}
select

absfinal.Driver
package absfinal;
 
public class Driver {
  public static void main(String[] args) {
    SuperClass obj = new SubClass();
    obj.foo();
    obj.bar();
  }
}

Abstract

A member function of an abstract class may be usable without being overridden. If we want to enforce that a member function is never used from the base class (i.e., superclass), we can do so by adding the abstract qualifier to it.

Abstract member functions can only appear within an abstract class.

If we add the abstract qualifier to the member function, Java recognizes that this member function is acting precisely like an interface prototype and requires that it have no body.

So, if we want to make foo abstract, it would be rewritten like this:
public abstract void foo();
The implication is that a subclass must override foo. Verify this effect by switching the commented/uncommented foo definitions in SuperClass and then commenting out the foo definition in SubClass, i.e.:
package absfinal;
 
public abstract class SuperClass {
//  public void foo() { System.out.println("Super:foo"); }
  public abstract void foo();
  ...
}
 
public class SubClass extends SuperClass {
//  @Override
//  public void foo() { System.out.println("foo"); }
  ...
}
Undo the change in the SubClass to fix the error.

As as second usage check, remove the abstract qualifier for SuperClass, getting
package absfinal;
 
public class SuperClass {
//  public void foo() { System.out.println("Super:foo"); }
  public abstract void foo();
  ...
}
You'll see that NetBeans now complains about SuperClass.

Final

The final qualifier applied to a class means that it cannot be extended. When applied to a member function it means that the member function cannot be overridden within a subclass. Verify this by switching the commented/uncommented bar definitions in SuperClass and then un-commenting out the bar definition in SubClass, i.e.:
package absfinal;
 
public abstract class SuperClass {
  ...
//  public void bar() { System.out.println("Super:bar"); }
  public final void bar() { System.out.println("Super:bar"); }
}
 
public class SubClass extends SuperClass {
  ...
  @Override
  public final void bar() { System.out.println("bar"); }
}
You'll see an error appear. Undo the change in the SubClass to fix the error.

Textbook Examples

Student  
CompSciStudent  
CompSciStudentDemo  

Anonymous Extensions

This is discussed in section 10.10 of the textbook. Our GUI applications make big use of anonymous classes which are implementations of interfaces, for example:
new ActionListener() { 
  public void actionPerformed(ActionEvent evt) { ... } 
}
The same can be done with extensions, i.e., we can create, say,
class MyClass { 
  public MyClass() { ... }
  ...
}
 
class Container {
  void someFunction() {
    // instantiate an object from an anonymous class
    MyClass obj = new MyClass() { 
      /* additional code */ 
    }
  }
}
It's the additional "{ ... }" after the MyClass() constructor call which makes the outcome different from simply creating an instance of MyClass. Under the surface, Java is executing code similar to what is done for interfaces:
class Container$1 extends MyClass {
  /* additional code */
}
MyClass obj = new Container$1();
The internal class, Container$1 (assuming this is the first anonymous class created), is created as an extension of MyClass. Unlike an anonymous class for an interface, the anonymous extension class may rely on a base class constructor, which would be activated though a derived class constructructor via a super(..) call.

Adapter Classes

In Java Swing, an adapter class is meant to give vacuous implementations of the prototyes of a related interface. The adapter class usage provides a more succinct way of creating an event handler object.

Our new knowledge of anonymous extensions gives us the ability to explain the following code from the MoreGUIs document. Specifically, these statements were used:
// in RegexMatcher
frame.getSampleStringField().addKeyListener(new KeyAdapter() {
  @Override
  public void keyReleased(KeyEvent ke) {
 
  }
});
 
// in FileOpenSave
frame.addWindowListener(new WindowAdapter(){
  @Override
  public void windowClosing(WindowEvent evt) {
    ...
  }
});
The KeyAdapter class is a vacuous implementation of the KeyListener interface:

java.awt.event.KeyAdapter
public class KeyAdapter implements KeyListener {
  @Override
  public void keyTyped(KeyEvent e) { }
 
  @Override
  public void keyPressed(KeyEvent e) { }
 
  @Override
  public void keyReleased(KeyEvent e) { }
}
The code above uses this object for the KeyListener:
new KeyAdapter() {
  @Override
  public void keyReleased(KeyEvent evt) {
    ...
  }
}
We could achieve the same end using the KeyListener directly:
frame.addContentKeyListener(new KeyListener(){
  @Override
  public void keyReleased(KeyEvent evt) {
    ...
  }
 
  @Override
  public void keyTyped(KeyEvent e) { }
 
  @Override
  public void keyPressed(KeyEvent e) { }
});
The KeyAdapter usage is more succinct since it avoids rewriting the unnecessary vacuous member functions.

The same logic holds true of the WindowAdapter usage over WindowListener, even more so because WindowListener has 7 method protypes.

Again, keep in mind that the
@Override
usage is essential in for anonymous extensions to avoid failure from a misspelling or other mistake.

Anonymous User class Extension

An anonymous extension can be used to override specific features of user-defined classes. A simple example is our User class above. Consider this example located in the anonymous package:

anonymous.User
package anonymous;
 
public class User {
  private String name;
  private int id;
 
  public User(String name, int id) {
    this.name = name;
    this.id = id;
  }
 
  @Override
  public String toString() {
    return String.format("(%s,%s)", name, id);
  }
}

anonymous.Driver
package anonymous;
 
public class Driver {
 
  public static void main(String[] args) {
    User joe = new User("Joe Jones", 111111);
 
    User joe1 = new User("Joe Jones", 111111) {
      @Override
      public String toString() {
        return "My Buddy";
      }
    };
 
    System.out.println("anonymous class: " + joe1.getClass());
 
    System.out.println(joe);
 
    System.out.println(joe1);
  }
}
The User object joe1 is created as an anonymous extension of the User class which overrides of the Object-level toString function, affecting the printed output. After running this program, observe the anonymous class generated (in the Files window) as
build/classes/anonymous/Driver$1.class
What's going on underneath the surface is something like this:
class Driver$1 extends User {
  Driver$1(String name, int id) {
    super(name, id);
  }
  @Override
  public String toString() {
    return "My Buddy";
  }
}
User joe1 = new Driver$1("Joe Jones", 111111); 
How can we prevent overriding? Two ways:
  1. Make the User class final. Try changing:
    public final class User { 
      ... 
    }
    
    Doing so makes it so that there can be no subclasses of User, anonymous or otherwise.
  2. Less drastically, make the toString member function be final:
    public class User {
      ...
      @Override
      public final String toString() {
        return String.format("(%s,%s)", name, id);
      }
    }
    
In either case, by making the change you will observe that the definition of joe1 is now flagged.

Inheritance for Interfaces

Interfaces can extend other interfaces, e.g.:
interface SubInterface extends SuperInterface
As complex as it may sound, it is not at all complicated. The complexities with class inheritance have to do with combining behaviors of member functions and constructors in an inheritance hierarchy. With interfaces, you're simply aggregating the prototypes. For example:
package interfaces;
 
interface SuperInterface {
  void foo();
}
 
interface SubInterface extends SuperInterface {
  int bar();
}
 
class MySampleClass implements SubInterface {
  @Override
  public int bar() { return 0; }
 
  @Override
  public void foo() { }
}
There is little to add to what you see here. Interfaces can also specify constants and a constant from a subinterface can "hide" one from superinterface.


© Robert M. Kline