Module 5: Java objects, Part II: dynamism


Brief review

 

Let's review what we know about objects so far:

 
About methods: in a class:
 
Consider another example: (source file)
// A simple static object. Note: all members (data and methods) are declared
// with the "static" keyword.

class ObjX {

    static int i;

    static void print ()
    {
	System.out.println ("i=" + i);
    }

} //end-ObjX


// The class that has "main"

public class StaticExample {

    public static void main (String[] argv)
    {
        // Refer to a member of ObjX using the class name.
	ObjX.i = 5;

        // Call a method in the class using the class name and dot-operator.
	ObjX.print();
    }

}
Note:
 

Let's now examine the "memory picture" for a static object:

 

More about encapsulation:

 


Dynamic objects

 

Let's look at an example with a dynamic object: (source file)


// Dynamic object definition

class ObjX {

    // No "static" keyword for either member.

    int i;

    void print ()
    {
	System.out.println ("i=" + i);
    }

}


public class DynamicExample {

    public static void main (String[] argv)
    {
        // First create an instance, which allocates space from the heap.
	ObjX x = new ObjX (); 

        // Now access members via the variable and the dot-operator.
	x.i = 5;
	x.print();
    }

}
Note:
 

Exercise 5.1: In DynamicExample.java add a second data member (say, another int) to the class ObjX. Then, in main, assign a value to the new variable and print it.
 

Next, we'll make a small modification: (source file)

class ObjX {

    // No "static" keyword for either member.

    int i;

    void print ()
    {
	System.out.println ("i=" + i);
    }

}


public class DynamicExample2 {

    // A simple variable declaration.
    static ObjX x;

    public static void main (String[] argv)
    {
        // First create an instance, which allocates space from the heap.
	x = new ObjX (); 

        // Now access members via the variable and the dot-operator.
	x.i = 5;
	x.print();
    }

}
Note:
 

We'll now examine what memory looks like

 

Exercise 5.2: Go back to the first example, DynamicExample.java, and draw the memory pictures for this example at various points in the execution of main().
 

A few words about nomenclature:

 


Dynamic objects: more examples

 

Consider this example: (source file)

class ObjX {

    int i;

    void print ()
    {
	System.out.println ("i=" + i);
    }

} //end-ObjX


public class DynamicExample3 {

    public static void main (String[] argv)
    {
        // Create an instance and do stuff with it.
	ObjX x = new ObjX (); 
	x.i = 5;
	x.print();

        // Create another instance assigned to the same variable.
        x = new ObjX ();
        x.i = 6;
        x.print();
    }

}
Note:
  • After the first three lines of main(), memory looks like this:

  • The memory picture after the execution of the lines
            // Create another instance assigned to the same variable.
            x = new ObjX ();
            x.i = 6;
         

  • What happens to the "floating" instance containing "5"?
         ⇒ It gets garbage-collected.

  • Finally, after main() completes, all the heap and stack memory is freed -up.

  • About garbage collection:
    • This is a process that runs invisibly in the background.
    • The garbage collector looks for "unattached" chunks of heap memory (nothing points to these chunks) and scoops them up.
           ⇒ Puts the memory back into the "available pool" of heap memory.
    • The garbage collector always runs in the background, every few milliseconds.
 

Exercise 5.3: Consider the example below and draw the memory picture just after the execution of x2.i=6: (source file)

class ObjX {

    // ... same as before

} 

public class DynamicExample4 {

    public static void main (String[] argv)
    {
        // Create an instance and do stuff with it.
	ObjX x = new ObjX (); 
	x.i = 5;
	x.print();

        // Create another instance assigned to the same variable.
        ObjX x2 = new ObjX ();
        x2.i = 6;
        x2.print();
    }

}
  
 

An example with an array of objects: (source file)

class ObjX {

    int i;

    void print ()
    {
	System.out.println ("i=" + i);
    }

} //end-ObjX


public class DynamicExample5 {

    public static void main (String[] argv)
    {
        // Make space for 4 ObjX pointers.
        ObjX[] xArray = new ObjX [4];
        
        // Make each of the 4 pointers point to ObjX instances.
        for (int k=0; k < 4; k++) {
            xArray[k] = new ObjX ();
        }

        // Now assign data to some of them.
        xArray[0].i = 5;
        xArray[1].i = 6;

        // Print all.
        for (int k=0; k < 4; k++) {
            xArray[k].print();
        }
        
    }

}
Note:
  • Observe how an array variable (for ObjX instances) is declared:
            ObjX[] xArray = new ObjX [4];
         

  • We could have, alternatively, split the declaration and space allocation:
            ObjX[] xArray;
            xArray = new ObjX [4];
         

  • This creation of space only makes space for 4 ObjX pointers:

  • We have to create four instances of ObjX ourselves with the code:
            // Make each of the 4 pointers point to ObjX instances.
            for (int k=0; k < 4; k++) {
                xArray[k] = new ObjX ();
            }
         
    The memory picture after this is:

  • The memory picture after the two assignments:
            // Now assign data to some of them.
            xArray[0].i = 5;
            xArray[1].i = 6;
         

 

Exercise 5.4: Consider the example below and draw the memory picture just after the execution of x2.i=6:

class ObjX {

    // ... same ...

}


public class DynamicExample6 {

    public static void main (String[] argv)
    {
	ObjX x = new ObjX (); 
	x.i = 5;

        // Assignment between object variables.
        ObjX x2 = x;
        x2.i = 6;

	x.print();
        x2.print();
    }

}
  
What gets printed out?
 

Object instances can be passed as parameters to methods and can be returned from methods: (source file)

class ObjX {

    // ... same ...

} //end-ObjX


public class DynamicExample7 {

    public static void main (String[] argv)
    {
        // Call a method that returns an instance of ObjX.
        ObjX x = makeAnObject();

        // Pass the instance as parameter to a method.
        printTheObject (x);
    }


    // Note: return type is ObjX

    static ObjX makeAnObject ()
    {
	ObjX obj = new ObjX (); 
	obj.i = 5;
        return obj;
    }
    

    // Note: parameter type is ObjX

    static void printTheObject (ObjX obj)
    {
        obj.print();
    }
    
}
Note:
  • Consider the state of memory just before the return statement is executed in makeAnObject():

  • After makeAnObject() returns to main():

 

Exercise 5.5: What is the memory picture just after executing the only line in printTheObject(), but before the method returns?
 

Exercise 5.6: Download DynamicExample8.java and draw the memory picture just after the last statement in main().
 


Objects that contain objects

 

Consider this example: (source file)

class ObjX {

    int i;

    ObjY y;

} 


class ObjY {

    String name;

}



public class DynamicExample9 {

    public static void main (String[] argv)
    {
        // Make an ObjX instance.
        ObjX x = new ObjX ();
        x.i = 5;

        // The y instance variable in x is assigned an instance of ObjY
        x.y = new ObjY ();

        // Note the repeated use of the dot-operator.
        x.y.name = "Mr. Y";
    }

}
Note:
  • The memory picture after the first two lines:

  • After the third line executes, the memory picture is:

 

Exercise 5.7: Draw the memory picture after the last line executes in main().
 

Now for an interesting variation: (source file)

class ObjX {

    int i;

    // A variable of the same type:
    ObjX anotherX;

} 


public class DynamicExample10 {

    public static void main (String[] argv)
    {
        // Make an ObjX instance.
        ObjX x = new ObjX ();
        x.i = 5;

        // Make the anotherX variable point to a new ObjX instance.
        x.anotherX = new ObjX ();
        x.anotherX.i = 6;
    }

}
 

Exercise 5.8: Draw the memory picture after the last line executes in main().
 


The special method toString()

 

The method toString(), if written for any object as shown below, has a special use in Java: (source file)

class ObjX {

    int i;

    public String toString ()
    {
        String s = "i=" + i;
        return s;
    }

} 


public class DynamicExample11 {

    public static void main (String[] argv)
    {
        // Make an ObjX instance and assign something to its variable i.
        ObjX x = new ObjX ();
        x.i = 5;

        // Note how "x" is directly given to println()
        System.out.println (x);

        // Another use:
        String outputStr = "Object x: " + x;
        System.out.println (outputStr);
    }

}
Note:
  • The method toString() must be defined exactly as shown above, with the signature
        public String toString ()
        {
        }
        

  • As the return type indicates, it must return a String.

  • Whenever the Java compiler sees an object variable in the context of a String, as in
            String outputStr = "Object x: " + x;
        
    • The compiler places a call to the object's toString() method.
    • The call returns a String, as we would expect.
    • This String is then used in place of the object.
    • Thus, in the above example, the string "i=5" gets concatenated with the string "Object x:" to give "Object x: i=5".
 

Exercise 5.9: Download the above program, compile and run. Now remove the toString() method from ObjX, compile and run. What do you see?
 


Accessors and mutators

 

As an object designer, you may want allow users (other programmers) to access and modify (mutate) data in your object.

The wrong way to do this is to make all data public:


class Customer {

  public String custName;
  public int bankBalance;

}

// Later somewhere ....

    Customer c = new Customer ();
    c.bankBalance = -1000;       // Don't want to allow this.
    c.custName = "331-77-9910"   // Oops.

Instead, it's best to allow access only via methods:


class Customer {

    // Internal data.
    private String custName;
    private int bankBalance;

    // Accessors.
    public String getCustName ()
    {
        return custName;
    }  

    public int getBankBalance ()
    {
        return bankBalance;
    }

    // Mutators.
    public void setBankBalance (int newBalance)
    {
        if (newBalance >= 0) {
            bankBalance = newBalance;
        }
        else {
            // ... handle error.
        }
    }

    public void setCustName (String newName)
    {
        //... perform extensive checking.
    }

}// End of class "Customer"

// Later somewhere ....

    Customer c = new Customer ();
    c.bankBalance = -1000;       // Won't compile.    
    c.custName = "331-77-9910"   // Won't compile.

Note:

  • By convention, accessors are "read-only" and mutators are "write-only".

  • The above example shows how a dynamic method can take parameters, like any static function:
    
    class Customer {
    
        // ... etc
    
        public void setBankBalance (int newBalance)
        {
            if (newBalance >= 0) {
                bankBalance = newBalance;
            }
            else {
                // ... handle error.
            }
        }
    
    }
    

  • An even smarter way to handle errors is to throw exceptions. (We will learn how to do this later).

  • It is fashionable to create an accessor and mutator for every internal variable.
    • The fact is, many variables don't need accessors or mutators.
    • Some variables should really remain private (or protected).

  • Be careful about returning object references, e.g.,
    
    class MasterList {
        Customer[] custList;
      
        // Accessor.
        public Customer getCustomer (int i)
        {
            return custList[i];
        }
    }
    
        //... later somewhere...
        MasterList ml;
        //... more stuff...
        Customer c = ml.getCustomer (9);
    
        // This is OK, to get the name.
        System.out.println (c.getCustName()); 
    
        // This is not what we had in mind.
        c.setBankBalance (0);
    

    In this case, it's probably better to return just a name:

    
    class MasterList {
        Customer[] custList;
      
        // Accessor.
        public String getCustName (int i)
        {
            return custList[i].getCustName();
        }
    }
    
 
Exercise 5.10:
Part I: Download and add code to TestComplex.java to make the methods in main() work correctly. All your code will be written inside the class Complex. Note:
  • To invoke the sqrt function, use Math.sqrt, as in
    
       double sqrtOf2 = Math.sqrt (2);
    

  • Here's a micro-tutorial on complex numbers:
    • A complex number is a pair of real numbers, such as (0.3, 1.996)

    • Any pair of real numbers can be considered a complex number.

    • However, the purpose becomes clear when we consider the special rules for adding, multiplying... etc complex numbers.

    • In the following, a,b,c,d are real numbers:
      • If a and b are real, the ordered pair (a,b) is a complex number.
      • a is the "real" part of (a,b). Example: 0.3 is the real part of (0.3, 1.996).
      • b is the "imaginary" part of (a,b). Example: 1.996 is the imaginary part of (0.3, 1.996).
      • The magnitude of complex number (a,b) is sqrt (a*a + b*b).
      • Addition: (a,b) + (c,d) is defined to be the complex number (a+c, b+d)
      • The magnitude of complex number (a,b) is sqrt (a*a + b*b).
      • Multiplication: (a,b) * (c,d) is defined to be the complex number (ac-bd, ad+bc).

    • The complex number (a,0) is considered equivalent to the real number a.

    • Consider: (0,1) * (0,1) = (-1, 0). Here, the square of complex number (0,1) is the real number -1.
      (Thus, -1 has a square root, a complex square root).

Part-II. Declare the two variables inside your class Complex as static. Does it work? Explain.
 


Constructors

 

Recall how we create object instances with the new operator:

        // Make an ObjX instance.
        ObjX x = new ObjX ();
You might wonder why there are parentheses at the end.

Before we answer that question, we will enhance our ObjX example and add a few so-called constructors: (source file)

class ObjX {

    int i;
    String name;


    // This is the first constructor: only takes in one parameter for the name.

    public ObjX (String initialName)
    {
        name = initialName;
        i = 0;
    }
    

    // This is the second constructor: both name and ID are initialized.

    public ObjX (String initialName, int ID)
    {
        name = initialName;
        i = ID;
    }


    public String toString ()
    {
        return "My name is " + name + " and my ID number is " + i;
    }

} 


public class DynamicExample12 {

    public static void main (String[] argv)
    {
        // The old way: without using constructors. Will not compile
        ObjX x = new ObjX ();
        x.i = 5;
        x.name = "Ms. X";
        System.out.println (x);
        

        // This compiles fine:

        // Use the first constructor.
        x = new ObjX ("Ms. X");
        System.out.println (x);

        // Use the second one.
        x = new ObjX ("Ms. X", 5);
        System.out.println (x);
    }

}
 

First, let's fix the compilation problem by removing the "old way": (source file)


class ObjX {

    int i;
    String name;


    // This is the first constructor: only takes in one parameter for the name.

    public ObjX (String initialName)
    {
        name = initialName;
        i = 0;
    }
    

    // This is the second constructor: both name and ID are initialized.

    public ObjX (String initialName, int ID)
    {
        name = initialName;
        i = ID;
    }


    public String toString ()
    {
        return "My name is " + name + " and my ID number is " + i;
    }

} 


public class DynamicExample13 {

    public static void main (String[] argv)
    {
        // Use the first constructor.
        ObjX x = new ObjX ("Ms. X");
        System.out.println (x);

        // Use the second one.
        x = new ObjX ("Ms. X", 5);
        System.out.println (x);
    }

}
Note:
  • A constructor executes just once when an object instance is created.

  • Thus, when the first instance is created above:
            ObjX x = new ObjX ("Ms. X");
        
    the first (one-parameter) constructor executes.

  • Similarly, when the second instance is created above
            x = new ObjX ("Ms. X", 5);
        
    the second constructor (with two parameters) executes.

  • Constructors are similar to methods in that they take parameters and execute like code.

  • But constructors are unlike methods in that:
    • They are declared differently: they must have the same name as the class and they cannot declare a return type.
    • They cannot be called as a regular method. This fails to compile:
                ObjX x = new ObjX ("Ms. X");
                x.ObjX ("Mr. Y");                 // Will not compile.
             
    • They execute only once for each instance, and execute at the time of creation. Thus, the constructor finishes execution by the time we get to the second line below:
              // Use the first constructor.
              ObjX x = new ObjX ("Ms. X");
              System.out.println (x);
             

  • Other facts about constructors:
    • Any number of them can be defined, as long as they have different signatures.
 

So, now let's get back to some nagging questions:

  • Why were there parentheses earlier when we didn't define any constructor?

  • Why, after we defined constructors, didn't the "old way" work?

  • And, finally, why bother with constructors at all?

Answers:

  • When you don't define your own constructors, Java puts one in for you (but you don't see the code, of course).
         ⇒ This is the "default no-parameter" constructor.
         ⇒ This is why the parentheses are part of the syntax.

  • When a programmer defines constructors, the Java compiler takes away the default no-parameter constructor
         ⇒ It now becomes a compiler error to try and use one.

  • You can of course, define your own no-parameter constructor: (source file)
    
    class ObjX {
    
        int i;
        String name;
    
    
        // This is the first constructor: only takes in one parameter for the name.
    
        public ObjX (String initialName)
        {
            name = initialName;
            i = 0;
        }
        
    
        // This is the second constructor: both name and ID are initialized.
    
        public ObjX (String initialName, int ID)
        {
            name = initialName;
            i = ID;
        }
    
    
        // No-parameter constructor that we added
    
        public ObjX ()
        {
            name = "X";   // Default name
            i = 0;        // Default value of i
        }
        
    
    
        public String toString ()
        {
            return "My name is " + name + " and my ID number is " + i;
        }
    
    } 
    
    
    public class DynamicExample14 {
    
        public static void main (String[] argv)
        {
            // Use the no-parameter constructor.
            ObjX x = new ObjX ();
            x.i = 5;
            System.out.println (x);
        }
    
    }
        

  • Why use constructors at all, especially if you can initialize values directly?
            ObjX x = new ObjX ();
            x.i = 5;
         
    • First, it's bad practice to place initialization code like this outside the class.
    • The right place for initialization code is the class itself, in the constructor.
    • Most often a class is written by one programmer, and used by another.
           ⇒ The creator of the class shouldn't allow others to modify data
           ⇒ Data should be declared private
    • Thus, the right way to declare data in ObjX is: (source file)
      class ObjX {
      
          private int i;
          private String name;
      
      
          // ... constructors, methods ... etc
      
      }
            
    • Then, something like this in main()
              ObjX x = new ObjX ();
              x.i = 5;                    // Compiler error!
            
      won't compile.
 


An object method that takes an instance as parameter

 

Consider the following example: we want to be able to compare customers using their balances: (source file)


class Customer {
    String custName;
    int bankBalance;

    public Customer (String cname, int balance)
    {
        // ... 
    }

    public int getBalance ()
    {
        return bankBalance;
    }

    // ... other constructors/methods (not shown)

} //end-Customer

public class DynamicExample16 {

    static int compare (Customer c1, Customer c2)
    {
        int b1 = c1.getBalance();
        int b2 = c2.getBalance();

        // Some calculation with the balances.
        double ratio = (double) b1 / (double) b2;

        if ( (0.1 < ratio) && (ratio < 10) )
            return 0; 
        else if (b1 < b2)
            return -1;
        else
            return 1;
    }

    public static void main (String[] argv)
    {
        Customer c1 = new Customer ("Bill G", 1000000);
        Customer c2 = new Customer ("Warren B", 100000);

        int comp = compare (c1, c2);
        if (comp < 0)
            System.out.println ("c2 is richer");
        else if (comp > 0)
            System.out.println ("c1 is richer");
        else 
            System.out.println ("same balance");
  }

}

Note:

  • A compare function was defined to compare two customers.

  • The compare function was outside the Customer object.
 

For encapsulation it is better to place comparison code inside the relevant object (inside Customer).

For example: (source file)


class Customer {

    //... stuff left out...

    // The comparion function -- now it's inside the class Customer.
    int compare (Customer c1, Customer c2)
    {
        int b1 = c1.getBalance();
        int b2 = c2.getBalance();

        //... as before ...
    }

} // End of class "Customer"


public class DynamicExample17 {

    public static void main (String[] argv)
    {
        Customer c1 = new Customer ("Bill G", 1000000);
        Customer c2 = new Customer ("Warren B", 100000);

        // Call the function compare in c1.
        int comp = c1.compare (c1, c2);

        if (comp < 0) {
            System.out.println ("c2 is richer");
        }
        else if (comp > 0) {
            System.out.println ("c1 is richer");
        }
        else {
            System.out.println ("same balance");
        }

  }

}
 

There is something strange about using c1's accessor functions from within c1 itself - it's inefficient!

Instead, consider the following improvement: (source file)


class Customer {

    String custName;
    int bankBalance;

    // ... stuff left out ...

    // New and improved comparison:
    public int compare (Customer c)
    {
        // bankBalance is part of THIS object.
        double ratio = (double) bankBalance / (double) c.bankBalance;

        // Rest of the calculation ...
    }

} // End of class "Customer"


public class DynamicExample18 {

    public static void main (String[] argv)
    {
        Customer c1 = new Customer ("Clark K", 10000);
        Customer c2 = new Customer ("Bruce W", 100000);

        // Only c2 is passed as parameter:
        int comp = c1.compare (c2);

        if (comp < 0) {
            System.out.println ("c2 is richer");
        }
        else if (comp > 0) {
            System.out.println ("c1 is richer");
        }
        else {
            System.out.println ("same balance");
        }
    }

}
 

This would be the end of the story, except that Java provides an interesting feature:

When an object instance is passed as a parameter to another instance of the same object, the first instance's variables (even if private) are accessible from the second.

Thus, the following is possible (and recommended): ( source file)


class Customer {

    // To emphasize the point, let's make these private.
    private String custName;
    private int bankBalance;

    // ... stuff left out...

    // Same comparison method as above:
    public int compare (Customer c)
    { 
        // We can access c's private data!
        double ratio = (double) bankBalance / (double) c.bankBalance;

        // ... same as before 
    }

} // End of class "Customer"


public class DynamicExample19 {

    public static void main (String[] argv)
    {
        Customer c1 = new Customer ("Clark K", 10000);
        Customer c2 = new Customer ("Bruce W", 100000);

        // Only c2 is passed as parameter:
        int comp = c1.compare (c2);

        // ... stuff omitted ...
    }

}

Note:

  • The current instance is able to access the private members of another instance:
    
    public int compare (Customer c)
    {
        double ratio = (double) bankBalance / (double) c.bankBalance;
    
        //...etc
    }
    

  • This works only between instances of the same class.

  • Why was this feature introduced?
    • Because it is very useful for comparisons, synchronization, etc.
    • There is no security problem because the actual code for both objects is really the same code.
    • It is written by one person, and so, presumably that one person knows what s/he is doing.
 

In-class Exercise 5.11: Embellish the Complex object in Exercise 5.10 with the following features:

  • Create three constructors, so that they can be used as follows:
    
    Complex c1 = new Complex ();            // (0,0)
    Complex c2 = new Complex (4.35, 9.002); // (4.35, 9.002)
    Complex c3 = new Complex (8.93);        // (8.93, 0)
    
  • Include functions for addition and multiplication of complex numbers, so that they can be used as follows:
    
    Complex c4 = c1.add (c2);
    Complex c5 = c2.multiply (c3);
    
  • Implement a toString() method so that a complex number may be printed out, indicating real and imaginary parts.
    
      Complex c6 = new Complex (4.35, 9.002);
      System.out.println ("c6 = " + c6);
    
You will also need to copy the above code into main().
 


Objects that are both static and dynamic

 

In our examples so far, we've used a separate class for our object of interest, and one class for testing that contains main().

For example: (source file)


class Customer {

    // Data:
    String custName;
    int bankBalance;

    // A constructor:
    public Customer (String cname, int balance)
    {
        custName = cname;
        bankBalance = balance;
    }

    // An accessor:
    public String getCustName ()
    {
        return custName;
    }

    // ... 

} // End of class "Customer"

public class DynamicExample19 {

    public static void main (String[] argv)
    {
        Customer c = new Customer ("Joe", 1000);
        System.out.println ("Name: " + c.getCustName() );
    }

}

Note:

  • Here, class Customer is purely dynamic and class DynamicExample19 is purely static

  • The purpose of DynamicExample19 is simply to "test" our code in Customer.

  • The main function in DynamicExample19 is similar to a globally-accessible function in C.
 

In Java, you can include a static "test" function directly within a dynamic object, as follows: (source file)


public class Customer {

    // Dynamic data:
    String custName;
    int bankBalance;

    // Dynamic constructor:
    public Customer (String cname, int balance)
    {
        custName = cname;
        bankBalance = balance;
    }

    // Dynamic function:
    public String getCustName ()
    {
        return custName;
    }

    // A static function!
    public static void main (String[] argv)
    {
        Customer c = new Customer ("Joe", 1000);
        System.out.println ("Name: " + c.getCustName() );
    }

} // End of class "Customer"

Note:

  • It certainly looks strange: a Customer object has been created from within the object.

  • Note that the file must be renamed to Customer.java.

  • The main method is now inside the object.

  • main still acts like a "test" function.
 

It is possible to define both variables and methods within an object that are static.

For example: (source file)


public class Customer2 {

    // Dynamic:
    String custName;        // Instance variable.
    int bankBalance;        // Instance variable.

    // Static:
    static int numCust = 0; // Class variable

    // Constructor (dynamic):
    public Customer2 (String cname, int balance)
    {
        custName = cname;
        bankBalance = balance;
        numCust++;
    }

    // Instance method (dynamic):
    public int getName ()
    {
        return custName;
    }

    // Class method (static):
    public static int getNumCust ()
    {
       return numCust;
    }

    // Class method (static):
    public static void main (String[] argv)
    {
        Customer2 c1 = new Customer2 ("George", 10);
        Customer2 c2 = new Customer2 ("Elaine", 20);
        Customer2 c3 = new Customer2 ("Kramer", 30);
        Customer2 c4 = new Customer2 ("Jerry", 100);

        System.out.println ("There are totally " + getNumCust() + " customers");
    }

} // End of class "Customer2"

Note:

  • This class has both static and dynamic members.

  • In Java, dynamic members are called instance members.

  • Static members are called class members.

  • Example: custName is an instance variable below
    
    public class Customer2 {
    
        String custName;        // Instance variable.
        int bankBalance;        // Instance variable.
        static int numCust = 0; // Class variable
        // ...etc..
    
    }
    
    and numCust is a class variable

  • Each dynamically created object has its own instance variables and methods.

  • Each such object shares static data and methods.

  • Thus, in our example
    
    public class Customer2 {
    
        String custName;        // Instance variable.
        int bankBalance;        // Instance variable.
    
        static int numCust = 0; // Class variable
    
        // ...etc..
    
    }
    
    there is only one numCust variable, no matter how many Customer2 objects are created.

  • A static method cannot access instance variables.
    The following will not compile:
    
    public class Customer2 {
    
        String custName;        // Instance variable.
        int bankBalance;        // Instance variable.
    
        // Static method.
        public static void test ()
        {
            custName = "Blah";    // Cannot access custName.
        }
    
    } 
    

  • A dynamic method can access static variables, as we saw:
    
    public class Customer2 {
    
        String custName;        // Instance variable.
        int bankBalance;        // Instance variable.  
        static int numCust = 0; // Class variable  
    
        // Instance method.  
        public Customer2 (String cname, int balance)
        {
            custName = cname;
            bankBalance = balance;
    
            // Accessing a static variable.
            numCust++;
        }
    
        // ...
    
    }
    

  • You do not have to create an instance of a class to use a static method, e.g.,
    
    class ObjA {
    
        double x;
    
        public static String getName ()
        {
            return "ObjA";
        }
    
    }
    
    
    public class Test {
    
        public static void main (String[] argv)
        {
            String s = ObjA.getname();
            // No instance of ObjA was created.
        }
    
    }
    

  • In general, static variables are used for:
    • Sharing constants across all instances.
    • Sharing an occasionally-modified variable across all instances.
    • Providing constants to the outside world (e.g., Math.PI).

  • Generally, static methods are used for:
    • Creating "test" functions.
    • Providing instance-independent functionality to the outside world (e.g., Math.sqrt()).
    • Returning an instance to the outside world, as in:
      
      public class Customer2 {
          // ... static/dynamic data...
      
          public static Customer2 getInstance ()
          {
              // Return an object of this type.
              return new Customer2 ("Mr.X", 0);
          }
      }   
      

  • Finally, note that static methods can access instance variables when given references to the instances (via parameters, say). Thus, a "comparison" method can be written as follows: source file)
    
    class Customer {
    
        String custName;
        int bankBalance;
    
        public Customer (String cname, int balance)
        {
            custName = cname;
            bankBalance = balance;      
        }
    
        public static int compare (Customer c1, Customer c2)
        {
            // c1 and c2's data is accessible.
            int b1 = c1.bankBalance;
            int b2 = c2.bankBalance;
    
            double ratio = (double) b1 / (double) b2;
    
            if ( (0.1 < ratio) && (ratio < 10) ) {
                return 0; 
            }
            else if (b1 < b2) {
                return -1;
            }
            else {
                return 1;
            }
        }
    
    }
    
    public class DynamicExample20 {
    
        public static void main (String[] argv)
        {
            Customer c1 = new Customer ("Dilbert D", 1000000);
            Customer c2 = new Customer ("Beetle B", 100000);
    
            int comp = Customer.compare (c1, c2);
    
            if (comp < 0) {
                System.out.println ("c2 is richer");
            }
            else if (comp > 0) {
                System.out.println ("c1 is richer");
            }
            else {
                System.out.println ("same balance");
            }
        }
    
    }
     
 


Some mysteries explained

 

Now, we can go back to some earlier code and begin to decipher what we found strange.

First, let's look at some string code:


    String s = "hello";
    int len = s.length();
    String s2 = s.substring(0,4);  // Extract first 4 chars  

Now that we know String's are objects, let's examine the library definition of String:

  • First, the object declaration:
    
    public final class String 
        extends Object implements Serializable {
            //... stuff ...
    }
    

    Here,

    • The keyword final prevents inheritance from this object.
    • The keyword extends says String inherits from a class called Object.
    • The keyword implements says String handles the Serializable interface
      (More about interfaces later).

  • Next, let's list some (only some) of String's members:
    
    public final class String 
        extends Object implements Serializable {
    
        // Some, but not all, constructors:
        public String();
        public String (String s);
        public String (char[] char_array);
        //... many other constructors.
    
        // Some Class (static) methods:
        public static String valueOf (char[] char_array);
        public static String valueOf (boolean b);
        public static String valueOf (int i);
        public static String valueOf (double d);
        //... many others not shown ... 
    
        // Some instance (dynamic) methods:
        public char charAt (int index);
        public int compareTo (String s);
        public boolean equals (String s);
        public boolean equalsIgnoreCase (String s);
        public String toLowerCase ();
        public String toUpperCase ();
        public String trim ():
        //... many others ... 
    
    } 
    

    Note:

    • String has several constructors (11 in all).

    • String has many Class (static) methods.

    • String has many instance methods.

    • The library documentation omits any description of internal variables or private methods.
 
Next, let's look at the Math object:
 
public final class Math extends Object {
    // No constructors at all.

    // Constants: (actual value not specified)
    public static final double E;
    public static final double PI;

    // Some Class methods:
    public static int abs (int i);
    public static double abs (double d);
    public static double exp (double d);
    public static double log (double d);
    public static double floor (double d); // Returns double!
    public static double ceil (double d);  // Returns double
    public static long round (double d);

        // No instance methods

}

Note:

  • Math is in the package java.lang.

  • Math has no constructors. In fact it is completely static (no instance methods or data).

  • Math's class methods are the usual mathematical functions we expect.

  • Thus, this is an example of pure encapsulation.

  • Minor point: floor and ceil methods return double.
 
Finally, recall the code for reading a string from the screen (Screen I/O):

public class ScreenIO1 {

    public static void main (String argv[])
    {
        // Put out a prompt.
        System.out.print ("Enter string: ");

         // We have to have a try clause because
        // the function readLine throws an exception.
        try {
            // These are the key steps in setting up the read operation.
            InputStreamReader isr = new InputStreamReader (System.in);
            LineNumberReader lr = new LineNumberReader (isr);

            // Now read the input line.
            String inputLine = lr.readLine ();

            // Echo it.
            System.out.println ("Echo: " + inputLine);
        }
        catch (IOException e) {
            // If there was a problem...
            System.out.println (e);
        }
    }

}

Given our new understanding of objects, we see that:

  • isr is an object of type InputStreamReader.

  • The constructor of InputStreamReader takes in something called System.in.

  • Let's look at the System object in package java.lang:
    
    public final class System extends Object {
        // Constants.
        public static final PrintStream out;
        public static final InputStream in;
        public static final PrintStream err;
    
        // Class methods...
    }
    
    • It is a static object that encapsulates a bunch of useful functions.

    • It has three public data variables called err, in and out.

    • err, in and out correspond to stderr, stdin and stdout in C.

    • System.in (one of these variables) is an object of type InputStream.

    • Thus, InputStreamReader attaches to System.in.

    • Next, a LineNumberReader object was created that took in the InputStreamReader in the constructor.

    • The LineNumberReader object (in the package java.io) has a method called readLine().

    • It is this method that gives us a string from the screen (keyboard).
 

Exercise 5.12: Consider these two familiar lines of Java code:


  System.out.println ("Number of primes below 100: ");
  System.out.println (25);
  
  • What is out? Is it a class? A method?
  • Where do you find the println() method? What is its signature?
 


Inheritance in dynamic objects

 

Just as static objects can be extended, so too can dynamic objects.

As we will see, inheritance in dynamic objects has some surprising and powerful features.

Consider this example: (source file)


public class Person {

    // Data
    String name;
    String ssn;

    // Constructors.
    public Person (String nameIn, String ssnIn)
    {
        name = nameIn;  ssn = ssnIn;
    }

    public Person ()
    {
        name = ssn= "Not initialized";
    }

    // Accessors.
    public String getName ()
    {
      return name;
    }

    public String getSSN ()
    {
        return ssn;
    }

    public String toString ()
    {
        return "Person: Name=" + name + ", ssn=" + ssn;
    }

    // Testing:
    public static void main (String[] argv)
    {
        Person p = new Person ("George", "111-11-1234");
        System.out.println (p);
    }

} 
 

We will use inheritance to extend this class.

Inheritance can be used for several reasons:

  1. Inheritance can be used to create a "new and improved" Person object, say, by adding new data and methods: (source file)
    
    class PersonV2 extends Person {
    
        // New data.
        int age;
    
        // New constructor.
        public PersonV2 (String nameIn, String ssnIn, int ageIn)
        {
            name = nameIn;
            ssn = ssnIn;
            age = ageIn;
        }
    
        // New accessor function
        public int getAge ()
        {
            return age;
        }
    
        // Testing.
        public static void main (String[] argv)
        {
            PersonV2 p = new PersonV2 ("Elaine", "333-33-4567", 35);
            System.out.println (p + ", age=" + p.getAge());
        }
    
    }
    

    Note:

    • In this example, Person is the parent class (super class) and PersonV2 is the child class (subclass).

    • Alternatively, PersonV2 derives or inherits from Person.

    • Superclass members are accessible from the subclass, e.g.,
      
      public PersonV2 (String nameIn, String ssnIn, int ageIn)
      {
          name = nameIn;   // "name" is defined in Person
          ssn = ssnIn;     // "ssn" is defined in Person
          age = ageIn;
      }
      

      and

      
      System.out.println (p + ", age=" + p.getAge());
      // Uses the toString() function in the superclass.
      

  2. Inheritance can be used to re-write an existing class by overriding superclass methods, for example: (source file)
    
    class PersonV3 extends Person {
    
        // New data.
        int age;
    
        // New constructor.
        public PersonV3 (String nameIn, String ssnIn, int ageIn)
        {
            name = nameIn;
            ssn = ssnIn;
            age = ageIn;
        }
    
        // New accessor function
        public int getAge ()
        {
            return age;
        }
    
        // Overriding the toString() method:
        public String toString ()
        {
            return "PersonV3: name=" + name + ", ssn=" + ssn +
                   ", age=" + age;
        }
    
        // Testing.
        public static void main (String[] argv)
        {
            PersonV3 p = new PersonV3 ("Kramer", "666-66-1234", 38);
    
            // The newer toString() overrides the superclass' toString()
            System.out.println (p);
        }
    
    }
    

  3. The most important use of inheritance is polymorphism.

    Suppose we want to distinguish between two types of Person's: customers and employees: (source file)

    • First, we extend Person to create a Customer.
      
      class Customer extends Person {
        
          // New data.
          int loyalty;   // Number of years as customer.
      
          // New constructor.
          public Customer (String nameIn, String ssnIn, int loyaltyIn)
          {
              name = nameIn;  ssn = ssnIn;  loyalty = loyaltyIn;
          }
      
          // Override toString()
          public String toString ()
          {
              return "Customer: name=" + name + ", " + loyalty +
                     " years with the company";
          }
      
          // A new method.
          public double discountRate ()
          {
              if ( (loyalty >= 1) && (loyalty < 5) ) {
                  return 0.95;  // 5% discount.
              }
              else if ( (loyalty >= 5) && (loyalty < 10) ) {
                  return 0.75;  // 25% discount.
              }
              else if (loyalty >= 10) {
                  return 0.50;  // 50% discount.
              }
              else {
                  return 1.0;   // no discount.
              }
        }
        
      } 
      

    • Next, we derive Employee from Person:
    • 
      class Employee extends Person {
      
          int salary;
      
          // New constructor
          public Employee (String nameIn, String ssnIn, int salaryIn)
          {
              name = nameIn;  ssn = ssnIn;  salary = salaryIn;
          }
      
          // Override the toString() method in Person.
          public String toString ()
          {
              return "Employee: name=" + name + ", salary=" + salary;
          }
      
      } 
      

    • Finally, let's write some code to test these objects:
      
      public class TestPoly {
      
          public static void main (String[] argv)
          {
              Customer c = new Customer ("John", "123-12-1234", 2);
              System.out.println (c);      // What gets printed?
      
              Employee e = new Employee ("Paul", "234-23-2345", 50000);
              System.out.println (e);      // What gets printed?
      
              // Declare some Person variables.
              Person p1, p2, p3, p4;
      
              // Person variables can hold Person instances
              p1 = new Person ("George", "345-45-3456");
              p2 = new Person ("Ringo", "456-45-4567");
      
              // ... as well as subclass instances
              p3 = c;        // c is a Customer.
              p4 = e;        // e is an Employee.
      
              System.out.println ("Person data: ");
      
              // Which toString() is called?
              System.out.println (p1);
              System.out.println (p2);
      
              // Which toString() is called?
              System.out.println (p3);
      
              // Which toString() is called?
              System.out.println (p4);
      
          }
      
      } 
      

    Note:

    • Customer and Employee both derived from Person.

    • You can derive as many subclasses as desired from a single class.

    • Customer defined its own constructor:
      
      public Customer (String nameIn, String ssnIn, int loyaltyIn)
      {
          name = nameIn;  ssn = ssnIn;  loyalty = loyaltyIn;
      }
      

      This was used in

      
          Customer c = new Customer ("John", "123-12-1234", 2);
      

    • Similarly, Employee had its own constructor:
      
      public Employee (String nameIn, String ssnIn, int salaryIn)
      {
          name = nameIn;  ssn = ssnIn;  salary = salaryIn;
      }
      

      which was used as:

      
          Employee e = new Employee ("Paul", "234-23-2345", 50000);
      

    • Customer overrode Person's toString() method:
      
      public String toString ()
      {
          return "Customer: name=" + name + ", " + loyalty +
                 " years with the company";
      }
      

      Note: the string " Customer" is part of the output.

    • Similarly, Employee overrode toString():
      
      public String toString ()
      {
          return "Employee: name=" + name + ", salary=" + salary;
      }
      

      Note: the string "Employee" is part of the output.

    • Obviously, Person instances can be created and assigned to Person variables:
      
          p1 = new Person ("George", "345-45-3456");
          p2 = new Person ("Ringo", "456-45-4567");
      

    • What is interesting is that subclass instances can also be assigned:
       
          p3 = c;        // c is a Customer.
          p4 = e;        // e is an Employee.
      

      This is similar in spirit to assigning an int to a long:

      
          int i = 5;
          long j = i;    // i is automatically cast into a long.
          Person p = c;  // Customer c is cast into a Person.
      

    • What is really interesting is that even after casting, the object instance seems to remember what it was:
      
          // Call to toString() in Customer.
          System.out.println (p3);
      
          // Call to toString() in Employee.
          System.out.println (p4);
      

      The output for these two calls is:

      
      Customer: name=John, 2 years with the company
      Employee: name=Paul, salary=50000
      

      Note:

      • p3 is really a Customer.

      • p4 is really an Employee.

      • The correct toString() function is called in each case!

    • This feature is known as polymorphism.
      • A superclass can be subclassed into many "morphs".
      • Each morph has access to its own functions and data even when treated as a superclass type.
 

Exercise 5.13: Demonstrate polymorphism starting with your Complex code. To do this, define two new classes (and name them anything you like) that each extend the class Complex . Thus, Complex will have two sub-classes. Next, create a method called print() in the class Complex in which you print something to the screen. After this, override the print() method in each sub-class and make sure you print something unique in each sub-class. Then, create four Complex variables and and assign instances of Complex and your new classes. Finally, call the print() method of each of the variables. Essentially, you will mimic the polymorphism example above with your print() method instead of using toString()
 


Inheritance - constructor chaining

 

Constructor chaining is a way of using a superclass's constructor:

  • First, recall the constructors in Person
    
    public class Person {
    
          // ... stuff ...
    
        // Constructors.
        public Person (String nameIn, String ssnIn)
        {
            name = nameIn;  ssn = ssnIn;
        }
    
        public Person ()
        {
            name = ssn= "Not initialized";
        }
    
      // ... stuff ...
    
    }
    

  • Next, recall how Customer was derived from Person:
    
    class Customer extends Person {
      
        // ... stuff ...
    
        // New constructor.
        public Customer (String nameIn, String ssnIn, int loyaltyIn)
        {
            name = nameIn;  ssn = ssnIn;  loyalty = loyaltyIn;
        }
    
        // ... stuff ...
    }
    

  • Here, similar assignments are repeated. Is there a way of using the superclass's constructor?
    Yes:
    
    class Customer extends Person {
      
         // New data.
        int loyalty;  
    
        // New constructor.
        public Customer (String nameIn, String ssnIn, int loyaltyIn)
        {
            // This is like calling Person (nameIn, ssnIn).
            super (nameIn, ssnIn);
    
            // Subclass data.
            loyalty = loyaltyIn;
        }
    
        // Override toString()
        public String toString ()
        {
            return "Customer: name=" + name + ", " + loyalty +
                   " years with the company";
        }
      
    }
    

  • Here, the reserved word super is used for a function call to the superclass' constructor.

  • The call to super must match the signature of at least one constructor in the superclass.

  • The super call must be the first line in any constructor of the subclass, even before any declarations of local variables.

    This method of using a superclass' constructor is called constructor chaining.
    (It is recommended).

 

The default in constructor chaining is worth noting because it can cause a compiler error if mishandled:

  • The following is true in Java:

    Whenever a class does not have a constructor, Java automatically creates a no-parameter constructor.

  • The following is also true:

    If a subclass does not explicitly call its superclass constructor, an implicit call to super() is added to every constructor in the subclass

     

    For example, consider the older version of Customer (which didn't have the super call).

    
    class Customer extends Person {
      
        // ... stuff ...
    
        // New constructor.
        public Customer (String nameIn, String ssnIn, int loyaltyIn)
        {
            // There is an implicit call to super() here.
            name = nameIn;  ssn = ssnIn;  loyalty = loyaltyIn;
        }
    
        // ... stuff ...
    }
    

  • But what constructor of Person does the no-parameter super() call invoke?
    Answer: the compiler looks in Person for a constructor with the signature:
    
      public Person ()
    

  • This is why we had to have the constructor
    
    public Person ()
    {
        name = ssn= "Not initialized";
    }
    
    in Person.
 
Beware of the following problem:
  • Let's say you do NOT define a no-parameter constructor for a superclass.
    
    class A {
        int i;
    
        public A (int k) // Takes a parameter.
        { 
            i = k; 
        } 
    
        public someMethod (int j) 
        { 
            i = j; 
        } 
    
    }
    

  • Suppose no constructor is defined for a subclass.
      
    class B extends A {
    
        double x;
        // No constructor at all.
    
    }
    

  • Then, the compiler will create a default constructor for the subclass B.
  • This default constructor will contain the call super().
  • But there isn't a no-parameter constructor in superclass A.
    A compiler error!
 


Inheritance - further inheritance from a subclass

 

Subclasses can be subclassed, which can be subclassed ... and so on.

For example, consider subclassing the class Employee above:

  • Here's Employee:
    
    class Employee extends Person {
    
        int salary;
    
         // Constructor.
        public Employee (String nameIn, String ssnIn, int salaryIn)
        {
            super (nameIn, ssnIn);
            salary = salaryIn;
        }
    
        // Override the toString() method in Person.
        public String toString ()
        {
            return "Employee: name=" + name + ", salary=" + salary;
        }
    
    } 
    

  • Now subclass Consultant from Employee:
    
    class Consultant extends Employee {
    
        // New constructor for this subclass:
        public Consultant (String nameIn, String ssnIn, int salaryIn)
        {
            // Call to superclass' constructor.
            super (nameIn, ssnIn, salaryIn);
    
            // New stuff.
            if (salaryIn < 500000) {
                System.out.println ("Salary too low - catastrophic error");
                System.exit (0);
            }
        }
    
        // Override toString()
        public String toString ()
        {
            return "Consultant: name=" + name + ", salary = " + salary;
        }
    
    }
    

  • Notice the use of constructor chaining:
    • Consultant's constructor calls Employee's constructor.
    • And Employee's constructor calls Person's constructor.

  • Thus, in the creation of a Consultant instance:
    
       Consultant c = new Consultant ("Pricey Pete", "222-33-4456", 40000);
    

    the following functions are really called:

    • First, the constructor method
      
      public Consultant (String nameIn, String ssnIn, int salaryIn)
      
      is called.

    • This calls super(nameIn,ssnIn, salaryIn):
      
      public Consultant (String nameIn, String ssnIn, int salaryIn)
      {
          // Call to superclass' constructor.
          super (nameIn, ssnIn, salaryIn);
      
          // ...
        }
      
      as the first step.

    • Thus, the constructor
    • 
      public Employee (String nameIn, String ssnIn, int salaryIn)
      {
          super (nameIn, ssnIn);
          salary = salaryIn;
      }
      
      is called.

    • And this calls the constructor
      
        public Person (String nameIn, String ssnIn)
        {
            name = nameIn;  ssn = ssnIn;
        }
      
      in Person.

  • Note: The toString() method is overridden in Consultant once more.
 


Inheritance and casting

 

Just as casting was applied to basic types, casting can be applied to objects:

  • Recall what casting meant for basic types:
    
        int i = 5;
        long j = 6;
    
        j = i;         // Automatic cast.
    
        i = (int) j;   // Explicit cast required (because of
                       // potential information loss)
    

  • Now consider this example with objects: (source file)
    
    class Customer extends Person {
      
        // ... code not shown ...
    
    } 
    
    
    public class TestPoly2 {
    
        public static void main (String[] argv)
        {
            Customer c = new Customer ("Gullible Gus", "555-55-5678", 5);
    
            // Automatic cast.
            Person p = c;   
    
            // Explicit cast required.
            Customer c2 = (Customer) p;
        }
    } 
    

  • Similarly, an object can be automatically cast to any ancestor in a hierarchy of inheritance.

  • An explicit cast is required to cast to a descendant.

  • Warning: you must know what you are doing when you cast "down": (source file)
    
    public class TestPoly3 {
    
        public static void main (String[] argv)
        {
            Customer c = new Customer ("Gullible Gus", "555-55-5678", 5);
            Person p = new Person ("Zany Zoe", "112-12-1212");
    
            // This is OK because p2 is really a Customer:
            Person p2 = c;
            Customer c2 = (Customer) p2;
    
            // This, on the other hand, will generate a 
            // run-time exception because p is not a Customer:
            Customer c3 = (Customer) p;
        }
    } 
    
 

Exercise 5.14:

  1. Write code (in a single file) to derive class B from class A and then to derive class C from class B. For each class, create a no-parameter constructor that simply prints out to screen the name of the class. Then, create an instance of class C. What gets printed out?
  2. Can static variables be inherited? Find out by defining a class A with a static integer variable i and deriving class B. Initialize and print i in the body of the constructors of both A and B (Use no-parameter constructors). Create instances of A and B and test. (Put all your code for this second question in a single file).
 


Review

 

Let us review some of the key concepts in Java objects using an example:

  • Suppose we create an object called ObjA with one piece of data:
    
    class ObjA {
       int x;
    }
    

    Just by itself, this object is rather useless.

  • So, let's add a print method:
    
    class ObjA {
    
        int x;
        public void printx ()
        {
            System.out.println ("This is x: " + x);
        }
    
    }
    

  • To test, we will add a public class in the file:
    
    class ObjA {
    
        int x;
    
        public void printx ()
        {
            System.out.println ("This is x: " + x);
        }
    
    }
    
    
    public class test {
        public static void main (String[] argv)
        {
            // Object creation.
            ObjA a = new ObjA ();
    
            // What does this print?
            a.printx();
        }
    }
    

    The new operator will create an instance of the object:

    • Behind the scenes at run-time, memory is allocated from the heap for an instance's data.
    • Data values are initialized.
    • Pointers to an instance's methods are created in the block of memory.
    • The appropriate constructor is called.
    • When the constructor returns, execution continues.

  • Let us add two constructors:
  • 
    class ObjA {
    
        int x;
    
        // No-parameter constructor:
        public ObjA ()
        {
            x = 5;
        }
    
        // Another constructor:
        public ObjA (int y)
        {
            x = y;
        }
    
        public void printx ()
        {
            System.out.println ("This is x: " + x);
        }
    
    }
    
    
    public class Test {
        public static void main (String[] argv)
        {
            ObjA a = new ObjA ();
            a.printx();
    
            ObjA a2 = new ObjA (6);
            a2.printx();
        }
    }
    

    Note:

    • It is always wise to define the no-parameter constructor, even if it does nothing.
    • Constructors are only called once at object creation time.
    • As many instance variables as possible should be initialized in the constructor.
    • Remember:
      • Each constructor should have a unique signature.
      • The modifier public is required for each constructor.
      • A constructor cannot declare a return value (not even void).

  • About initialization:
    • Instance variables are initialized (numbers to 0, booleans to false and pointers to null.) Nonetheless, it's good programming practice to initialize variables yourself.
    • The compiler will not necessarily warn you about uninitialized instance variables.
    • Beware of a NullPointerException when actually accessing something initialized to null.

  • Let us now derive an object from ObjA
    
    class ObjA { 
        int x; 
    
        public ObjA () 
        { 
            x = 5; 
        } 
    
        public ObjA (int y) 
        {
            x = y; 
        } 
    
        public void printx () 
        { 
            System.out.println ("This is x: "+ x); 
        } 
    
    } 
    
    
    class ObjB extends ObjA { 
    
        String desc; 
        // No-parameter constructor.
    
        public ObjB () 
        { 
            // Call to super() here. 
            x = 10; 
        } 
    
        // A two-parameter constructor. 
        public ObjB (int y, String s) 
        { 
            // Call to super() here.
            x = y; 
            desc = s; 
        } 
    
        // Override printx. 
        public void printx () 
        { 
            System.out.println (desc + ", value = " + x); 
        } 
    
    } 
    
    public class Test { 
    
        public static void main (String[] argv) 
        { 
            ObjB b = new ObjB (8, "lightyears");
            b.printx(); 
    
            ObjA a = b; 
            a.printx ();   // What does this print? 
        } 
    } 
    

    Note:

    • All non-private data members of the superclass are accessible in the subclass.
    • A subclass has its own constructors. However, the superclass no-parameter constructor is called otherwise.
    • The appropriate superclass constructor can be called specifically:
      
      public ObjB (int y, String s)
      {
          super (y);
          desc = s;
      }
      
    • A subclass can override superclass functions and data.
    • A subclass instance can be assigned (automatic casting) to a superclass variable.
    • The overriding subclass method will be called nonetheless.

  • We will cover visibility in greater detail later.
 


The class Object

 

Java defines a special class called Object:

  • Object is sort-of the "parent" of all classes, from which all classes derive, whether you specify it or not.

  • Thus, even if you don't explicitly say
    
    class ObjA extends Object {
    }
    
    Java actually derives every class from Object.

  • The definition of Object (in package java.lang) includes:
    
    public class Object {
        // No-parameter constructor.
        public Object ();
        public String toString();
        public boolean equals (Object obj);
        //... and other methods...
    }
    

  • Every class instance can be automatically cast into an Object instance, e.g.,
    
        ObjA a = new ObjA();
     
        // Automatic cast
        Object ob = a;
    
        //...later...
        a = (ObjA) ob;
    

  • This language feature turns out to be very useful in writing generic code, e.g., a linked list that is defined to contain Object's but actually contains anything.



© 1998, Rahul Simha (revised 2017)