More Examples Explaining Objects


Overview

This page contains a bunch of examples that go over key features of objects in Java. None of the examples are accompanied by separate explanations; rather, the examples are explained in comments embedded in the code. Prior to reading this, you might want to read through simpler explanations here:


Static vs. Dynamic


class A {

  // Static data and methods:
  static int data1 = 1;

  static void methodOne () 
  {
    System.out.println ("method one: data1=" + data1);
  }

  // Dynamic data and methods:
  int data2 = 2;
  
  void methodTwo ()
  {
    System.out.println ("method two: data2=" + data2);
  }

}


public class TestClasses {

  public static void main (String[] argv)
  {
    // Call the static method directly using the class name, 
    // the dot-operator and the method name.
    A.methodOne ();
    // Access static data similarly:
    System.out.println (A.data1);       // Prints "1".

    // Note: Java uses the term "class method" to refer to a static method.
    // The term "class method" is just their jargon, and is not a reserved word.
    
    // For a dynamic method, we need to first create an instance and
    // then call the method using the instance variable.
    A a = new A ();
    a.methodTwo ();
    // Access data similarly:
    System.out.println (a.data2);       // Prints "2".

    // Create another instance:
    A x = new A ();
    x.data2 = 3;
    x.methodTwo ();                    // Prints "3".
    a.methodTwo ();                    // Prints "2" (Variable "a" was not affected).
       

    // Note: the variable "a" is really a pointer that points to a blob
    // of memory on the heap. This blob contains space for data like "data2".
    // The blob assigned to variable "x" is different and has its own space
    // allocated for dynamic data and methods.
    // It also helps to think that it has space for methods like "methodTwo()". 
    // In an actual implementation, only a pointer to the method is stored
    // in the blob, so that the real code is elsewhere (pointed to, by the pointer).
    // Java jargon: the term "instance variables" is used for dynamic variables,
    // just as they use "instance methods" instead of "dynamic methods".
  }

}


Inheritance


class A {

  // Static data:
  static int data1 = 1;
  static int data3 = 3;

  // Two static methods:
  static void methodOne () 
  {
    System.out.println ("method one: data1=" + data1);
  }

  static void methodThree ()
  {
    System.out.println ("method three: data3=" + data3);
  }


  // Instance data:
  int data2 = 2;
  int data4 = 4;
  
  // Two instance methods:
  void methodTwo ()
  {
    System.out.println ("method two: data2=" + data2);
  }

  void methodFour ()
  {
    System.out.println ("method four: data4=" + data4);
  }

}

class B extends A {

  // B's own static data, not in A:
  static int data5 = 5;
  
  // B's own static methods, not in A:
  static void methodFive ()
  {
    System.out.println ("method five: data5=" + data5);
  }

  // B overrides A's static variable "data3":
  static int data3 = 30;

  // B overrides A's static method "methodThree":
  static void methodThree ()
  {
    System.out.println ("B's method three: data3=" + data3);
  }
  

  // B's own instance data:
  int data6 = 6;

  // B's own instance method:
  void methodSix ()
  {
    System.out.println ("method six: data6=" + data6);
  }


  // B overrides (overshadows) A's instance variable "data4":
  int data4 = 40;

  // B overrides A's instance method "methodFour":
  void methodFour ()
  {
    System.out.println ("B's method four: data4=" + data4);
  }
    
}



public class TestClasses2 {

  public static void main (String[] argv)
  {
    // We don't need an instance for static methods and data.
    // First, we can always access B's newly created methods (not in A):
    B.methodFive ();
    // Then, B inherits everything from A. For example, it inherits
    // "methodOne", that we can call in B:
    B.methodOne ();

    // B overrode A's "methodThree()" with it's own (different) version:
    B.methodThree ();                 // Prints 30.
    System.out.println (B.data1);      // Prints 1
    System.out.println (B.data3);      // Prints 30
    
    // NOTE: It is RARE to encounter examples of overriding static
    // methods, even though it can be done. Most often, it is 
    // instance methods that are overridden. 

    // We need an instance to access the instance methods.
    B b = new B ();

    // First, we'll access B's own newly created methods (not in A):
    b.methodSix ();
    // Then, B's "methodTwo()" that got inherited from A:
    b.methodTwo ();

    // Now, we'll access B's "methodTwo()" that overrode A's "methodTwo()":
    b.methodFour ();                  // Prints 40

    // Now for polymorphism. Here, we are declaring a variable "a" of type "A".
    // To this variable, we are assigning a "B" instance so that "a" points
    // to the B instance that "b" is also pointing to. What this really means
    // is that the blob contains the method "methodFour()" in class B.
    A a = b;
    // This calls "methodFour()" in the class B, because the blob contains 
    // that method.
    a.methodFour ();                  // Prints 40

    // We've discussed overriding methods. Observe what happens with data.
    System.out.println (b.data4);      // Prints 40, as we expect.

    // Here's a surprise: data is treated differently. 
    System.out.println (a.data4);      // Prints 4.
    // This prints the variable in class A. Why? Because data is "shadowed"
    // rather than overridden. By casting, we remove the shadow.

    // Why is it called polymorphism? Because, in the above example, the
    // variable "a" is really pointing to something that's at least of
    // of type A. It happens to be a "B" object, but every "B" object is
    // an "A" object because it inherits everything from "A" (and maybe 
    // overrides some things in it). Thus, the B instance is a "morph"
    // of the A instance. 

    // Why this is useful? The next example shows you why.
  }

}


Inheritance

class A {

  // Instance data and method:
  int data2 = 2;

  void methodTwo ()
  {
    System.out.println ("method two: data2=" + data2);
  }

}

class FancyDataStructure {
  // This data structure has been written for objects of type "A".
  // The code could be long and tedious, but it's written just once,
  // compiled and handed out to others for use.
  static void insertStuff (A a)
  {
    // This method expects an "A" type object coming in.

    // ... The actual code is not relevant to the example ...
  }
}


class B extends A {
  
  // B's own stuff:
  void methodThree () 
  {
    // ... not relevant ...
  }

}



public class TestClasses3 {

  public static void main (String[] argv)
  {
    A a = new A ();
    FancyDataStructure.insertStuff (a);
    
    // Here, the method has been written, and let's say, compiled 
    // for "A" type objects. But, later we want to add "B" type 
    // objects as well. We can do that directly, as the method call above shows,
    // without having to re-write the data structure for "B" type objects.
    // We can do this because every B object is an A object. Indeed,
    // the data structure can be compiled and written just ONCE for
    // A type objects.

    // OK, so let's add a "B" object to the data structure:
    B b = new B ();
    FancyDataStructure.insertStuff (b);
    // Note that as far as FancyDataStructure is concerned, we inserted
    // an "A" object into it.

    // Now, there's another important use of overridding, as the
    // next example shows.
  }


}


Method callbacks


class A {

  int methodNeededInDataStructure ()
  {
    // ... whatever ...
    return 1;
  }

}

class FancyDataStructure {
  // This data structure has been written for objects of type "A".
  static void insertStuff (A a)
  {
    // This method expects an "A" type object coming in
    // and is going to call the "methodNeededInDataStucture()", as in:
    
    int x = a.methodNeededInDataStructure();

    // ... The actual code is not relevant to the example ...
  }
}


class B extends A {
  
  // B's own stuff:
  void methodThree () 
  {
    // ... not relevant ...
  }

  // B overriddes the method needed for the data structure.
  int methodNeededInDataStructure ()
  {
    // ... whatever ...
    return 2;
  }
}



public class TestClasses4 {

  public static void main (String[] argv)
  {
    A a = new A ();
    FancyDataStructure.insertStuff (a);
    
    B b = new B ();
    FancyDataStructure.insertStuff (b);
    // Note that as far as FancyDataStructure is concerned, we inserted
    // an "A" object into it. However, it is really a "B" object that
    // has its own implementation of the "methodNeeded ..." method that
    // the data structure called.
  }


}


Method callbacks: another example


class A {

  int compare (A a)
  {
    // ... An "A" object knows how to make a comparison with
    // another "A" object.
  }

}

class FancyDataStructure {
  // This data structure has been written for objects of type "A".
  static void insertStuff (A a)
  {
    // This method expects an "A" type object coming in.

    // Some where in here, there will be a need to make comparisons like
    int compValue = a.compare (x);
    if (compValue < 0) {
      // ... yada, yada ...
    }
    
    // Here, x is some existing "A" object in the data structure.
  }
}


class B extends A {
  
  // B overriddes the method needed for the data structure.
  // NOTE: it must have the same signature to override, and so must
  // declare an "A" object as parameter.
  int compare (A a)
  {
    // It knows that the input parameter will be a "B" object
    // because it expects the user of "B" objects to store 
    // a bunch of "B" objects in the data structure.
    B b = (B) a;
    
    // Now make comparison that is relevant to "B" objects....

    // Of course, a careless user of "B" objects can store both "A" and
    // "B" objects, in which case this will be called with an 
    // honest-to-goodness "A" object. This will cause a casting exception.
    // A slick programmer will anticipate this and have code that
    // uses the "instanceof" operator like this ...
    if (! (a instanceof B)) {
      // ... take care of the problem ...
    }
    
  }
}



public class TestClasses5 {

  public static void main (String[] argv)
  {
    // Insert one object ...
    B b = new B ();
    FancyDataStructure.insertStuff (b);

    // Then, another ...
    b = new B ();
    FancyDataStructure.insertStuff (b);
  }


}


Abstract classes


abstract class Stuff {
  abstract int compare (Stuff x);
}


class A extends Stuff {

  int compare (Stuff x)
  {
    A a = (A) x;
    // ... compare etc ...
  }

}

class FancyDataStructure {
  // This data structure has been written for objects of type "A".
  static void insertStuff (Stuff x)
  {
    // This method expects an "Stuff" type object coming in.

    // Some where in here, there will be a need to make comparisons like
    int compValue = x.compare (y);
    if (compValue < 0) {
      // ... yada, yada ...
    }
    
    // Here, y is some existing "Stuff" object in the data structure.
  }
}



public class TestClasses6 {

  public static void main (String[] argv)
  {
    // Insert one object ...
    A a = new A ();
    FancyDataStructure.insertStuff (a);

    a = new A ();
    FancyDataStructure.insertStuff (a);
  }


}


Abstract classes: another example


abstract class A {

  // An abstract class can have non-abstract methods.
  void methodOne () 
  {
    // ...
  }
  
  // If even one method is abstract, the class is abstract.
  abstract void methodTwo ();
}

class B extends A {
  
  // Inherits the non-abstract methods in A.

  // Must implement the abstract method if you want to
  // create instances of B.

  void methodTwo () 
  {
    // ...
  }
  
}

public class TestClasses7 {

  public static void main (String[] argv)
  {
    B b = new B ();

    // Call an inherited method.
    b.methodOne ();

    // Call an implemented method that was abstract.
    b.methodTwo ();
  }

}


Abstract classes: a limitation

abstract class A {
  abstract void methodOne ();
}

abstract class B {
  abstract void methodTwo ();
}


// Class C can only extend one other class. C cannot
// extend both A and B.
class C extends A {
  
  void methodOne () 
  {
    // ...
  }
  
}

public class TestClasses8 {

  public static void main (String[] argv)
  {
    // ...
  }

}


Interfaces


interface A {
  abstract void methodOne ();
}

interface B {
  abstract void methodTwo ();
}


// Class C can only extend one other class, but
// can implement many interfaces.
class C implements A, B {
  
  public void methodOne () 
  {
    // ...
  }

  public void methodTwo () 
  {
    // ...
  }
  
}

public class TestClasses9 {

  public static void main (String[] argv)
  {
    C c = new C ();
    c.methodOne ();
    c.methodTwo ();
  }

}


Interfaces and abstract classes


interface A {
  abstract void methodOne ();
}

interface B {
  abstract void methodTwo ();
}

class D {
  void methodThree ()
  {
    // ...
  }
}


// Class C can extend one class and implement as many interfaces
// as desired.
class C extends D implements A, B {
  
  public void methodOne () 
  {
    // ...
  }

  public void methodTwo () 
  {
    // ...
  }
  
}

public class TestClasses10 {

  public static void main (String[] argv)
  {
    C c = new C ();
    c.methodOne ();
    c.methodTwo ();
    c.methodThree ();
  }

}