Why Use Objects?


Overview

Object-oriented programming has gone from an obscure research topic (in the 1970's) to taking over the mainstream (1990's).

Why? What good are objects?

First, let's clarify what we mean by Object-Oriented Programming:

Next, let's look at Java/OOP features one by one.


Encapsulation

What is encapsulation? Answer: Packaging related data and code into a single name-able unit.

Example:

class A {
  static int i;
  static void print () 
  {
    System.out.println ("my age is" + i);
  }
}

class B {
  static int i;
  static void print () 
  {
    System.out.println ("my weight is = " + i);
  }
}

public class Test {
  public static void main (String[] argv) 
  {
    // Use the print method in A: 
    A.print ();
 
    // Use the print method in B: 
    B.print ();

    // No name clash for the print methods (or data). 
    // Typically, A and B will be written by different programmers. 
    // => they won't need to worry about name clashes. 
  }
}
  


Static vs. Dynamic

It's important to understand the distinction between static and dynamic classes:

Another example:



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

What is inheritance?

  • Extend an object to add stuff to it.
  • Implement an interface (specification).

Why use it?

  • Building better, more customized objects with less effort:
    • The original can be modified independently.
    • Well-known interface, method signatures.
  • Instance encapsulation: all the data for an instance is kept inside the instance.
  • Polymorphism (example below).
  • Function callbacks (next section).

Example:


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. This is called polymorphism.
    // A "B" object is a "morph" an "A" object.

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


}


Method callbacks

Let's look at an example. The comments explain ...



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 classes help force an implementation:

  • A data structure designer can design a data structure for objects of a "certain type".
  • This "certain type" is specified as an abstract object.
  • Users of the data structure (other programmers) who want to store their objects, need to make their objects be of the "certain type".
  • To do that, they extend the "certain type".


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 ();
  }

}


Summary

To summarize, objects are useful because:

  • Static encapsulation reduces name clashes, allows for clean divison of labor.
  • Dynamic classes allow for instance encapsulation, in addition.
  • Inheritance is used for:
    • Method callbacks (essential in event-driven programming).
    • Extending someone else's work.
    • Defining a "type" for others.
  • Object-oriented design:
    • Think in terms of objects (data, and methods that operate on them).
    • Objects with properties, and behavior specification.
    • Interfaces.


Links: