Module 5: A First Look at Objects


Supplemental material


Static objects: encapsulation

What is a static object?

Let's look at an example:
// 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:

What is encapsulation?

In-Class Exercise 1: Download StaticExample.java and 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.

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

More about encapsulation:

Here's an example of placing a bunch of related methods together:

public class MathStuff {


    // Compute a^b

    public static int power (int a, int b)
    {
        if (b == 0) {
            return 1;
        }
        return (a * power (a, b-1));
    }
    

    // Compute n!

    static int factorial (int n)
    {
        if (n == 1) {
            return 1;
        }
        return ( n * factorial(n-1) );
    }


    // Compute f_n, the n-th Fibonacci number.

    static int fibonacci (int n)
    {
        // Base cases rolled into one:
        if (n <= 2) {
            return (n-1);
        }
        
        return ( fibonacci(n-1) + fibonacci(n-2) );
    }

}
Note:

About class'es and files:


Dynamic objects

Let's look at an example with a dynamic object:


// 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:

In-Class Exercise 2: Download DynamicExample.java and 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:


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

In-Class Exercise 3: 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:


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.

In-Class Exercise 4: Consider the example below and draw the memory picture just after the execution of x2.i=6:

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:

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;
         

In-Class Exercise 5: 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:

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():

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

In-Class Exercise 7: Download DynamicExample8.java and draw the memory picture just after the last statement in main().


Objects that contain objects

Consider this example:


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:

In-Class Exercise 8: Draw the memory picture after the last line executes in main().

Now for an interesting variation:


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

}

In-Class Exercise 9: 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:


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".

In-Class Exercise 10: Download the above program and remove the toString() method from ObjX. Then compile and run. What do you see?


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:


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 = "Mr. X";
        System.out.println (x);
        

        // This compiles fine:

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

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

}

First, let's fix the compilation problem by removing the "old way":


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 ("Mr. X");
        System.out.println (x);

        // Use the second one.
        x = new ObjX ("Mr. 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 ("Mr. X");
        
    the first (one-parameter) constructor executes.

  • Similarly, when the second instance is created above
            x = new ObjX ("Mr. 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 ("Mr. 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 ("Mr. 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:
    
    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:
      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.


More about objects

There's a lot more to objects than we have described here
     ⇒ Our purpose was to introduce objects in enough detail for the data structures we will examine soon.

What we haven't covered:

  • Gory details about constructors.

  • Objects that have both static and dynamic members.

  • Inheritance.

  • Method overriding and callbacks.

  • Abstract classes.

  • Interfaces.

  • Casting in objects.

  • Java's Object class and its implications.

  • Esoteric Java topics like: shadowed variables, the this operator, the use of instanceof, static initializers, and inner classes.
To read more:
  • See Modules 4, 5, 6, 7 and 10 of CS-143

  • See the two short overviews of objects in the CS-143 Appendix entitled "Why use objects?" and "More examples on objects".



© 2006, Rahul Simha (revised 2017)