next up previous contents
Next: 5. Inheritance, Polymorphism and Up: Algorithms and Data Structures Previous: 3. Introduction to Object-oriented   Contents

Subsections

4. Classes and Objects

4.1 Introduction

This chapter introduces the basics of classes and objects. We describe how the Java class construct may be used to implement an ADT (abstract-data-type), i.e. class as ADT.

For the moment, we will avoid inheritance, polymorphism and dynamic / run-time binding. Hence, this chapter could be said to be concerned with object-based programming, for, as also mentioned in [Deitel and Deitel, 1999, Chapter 8], this is the term used for software development based on just the plain ADT aspect of classes.

Nevertheless, it is essential that we properly and completely introduce the concepts of class and object; this is an essential foundation. Moreover, it is worth remarking that object-based programming is a colossal advance over programming without ADTs.

In spite of what is left out, by the time you have completed this chapter, you will know an awful lot about objects!

We start with a very simple class, Cell, which reduces the class/object concept its bare essentials. Then we will proceed to a class Time, which is use to store an manipulate clock times.

Recall that a data type, such as the native data types int, float, etc., is characterized by:

  1. A set of values - that can be assumed by objects of the type; for example in the Java type byte, the set of values is $ {-128, -127, \ldots, -1, 0, 1, \ldots 126,
127}$.

  2. A set of operations (functions) that can be legitimately performed on objects of the type; for example, for int, some of the functions are: +, -, *, /.

Users of the type do not concern themselves with the representation of the values, and, certainly, they are not encouraged to fiddle with the representation - they interact with the variables only through the legitimate operations.

4.2 A Cell Class

4.2.1 Informal Specification of the Class

This class does nothing more than represent a single int value. As with types we are interested in values - the set of states an object may take on - and operations -- behaviour.

4.2.1.0.1 State

We would expect a Cell object to be able to store the current state, i.e. its integer value.

4.2.1.0.2 Behaviour

How would we like a Cell object to behave?

Constructors
Since Cell is to be used in a computer program we need to be able to declare, define and create object of the class. We will define a single constructor, the default constructor, which initializes objects to have a state 0. In addition, we provide an initializing constructor - which overload the default constructor name.

Inspector
We should be able to obtain the state - but only via an interface function.

Mutator
We should be able to modify the state - but again only via an interface function.

Input-output
We require - more for the purposes of demonstration than anything else - facilities to convert a Cell object into a humanly readable format; for this we provide a toString() function.

4.2.2 Class Cell

Class Cell class is shown below, and below that a test program. Note: in these examples, we try to keep white space to a minimum - so that it will be possible to get programs on a single OHP slide.

/**
* Cell.java -- Memory cell ADT
* j.g.c. 19/12/99, 15/01/00
* for MSc CSA, QUB.
*/
public class Cell{

public Cell(int val){
  v= val;}

public Cell(){
  this(0);}

public int get(){
  return v;}

public void put(int val){
  v= val;}

public String toString(){
  return new String("Cell: "+ v);
}
private int v;
}

/**
* CellT.java -- Exercises class Cell
* @author j.g.c. 19/12/99, 15/01/00
* for MSc CSA, QUB.
*/
import java.io.*;

public class CellT{

public static void main(String args[]){
  Cell c1, c2= new Cell(123);
  c1= new Cell(); // to demonstrate distinction between 
                  // declaration and creation
  PrintStream o= System.out;

  o.println("c1 = " + c1);
  o.println("c2 = " + c2);

  c1.put(456);
  o.println("c1.put(456), c1 = " + c1);

  c1= c2;
  o.println("c1= c2; c1 = " + c1);
  o.println("c2 = " + c2);

  //c1.v= 22; // compiler error if first // removed

  // reference semantics
  c2.put(222);
  o.println("c2.put(222); c1 = " + c1);
  o.println("c2 = " + c2);
}
}

4.2.2.0.1 Dissection

  1. Public interface. First, we have the interface-functions or methods - which provide the behaviour; these are declared public.

  2. public means that the members can be directly accessed by client programs as: instance.member e.g. c1.put(456); where c is an instance of Cell.

  3. Constructors must have the same name as the class; often, we will have multiple constructors, all with the same name; the name sharing is allowable due to function name overloading - functions may share the same name, as long as they are resolvable by their parameter list (their signature).

  4. By deliberately writing the declaration Cell c1, separate from the creation of a Cell object and assigning it to c1: c1= new Cell(); we are careful to distinguish between declaration and creation. In the corresponding `ordinary' variable case: verb+int i;+, this statement both declares i to be of type int and creates a new variable.

    Note: when we arrive at this point in class, we should discuss whether the concept of reference is clear ...reference semantics versus value semantics ....

  5. Private by default. Had we left out the keyword public, the interface functions would have been inaccessible by user programs.

  6. After the interface functions, we have the representation of the state; this is private; though the private representation is visible in the text, it is still encapsulated and invisible to client programs.

4.2.2.0.2 Encapsulation

As a consequence of encapsulation we can view objects, e.g. of class as capsules, containing the representation data, in this case a single datum int v, but these data may be accessed only through the interface functions (methods). This is shown diagrammatically:

                    +----------------------------------+
                    | private: (hidden data)           |
Public interface    |                                  |
functions (methods) |                                  |
               +---------+    int v;                   |
        ----->-| put()   |                             |
               +---------+                             |
                    |                                  |
               +---------+                             |
        -----<-| get()   |                             |
               +---------+                             |
+ Cell(), toString() etc...                            |
                    +----------------------------------+

After previously specifying public, it is necessary to revoke this directive using private.

private means that the member v cannot be directly accessed by client programs. I.e.

c1.v = 22;// illegal -- compiler error

This is called encapsulation and provides information hiding.

Generally, the syntax for calling a method (member function), i.e. sending a message to an object - is: object.method(argument), e.g.

   c1.put(456);

Message to c: set your state to $ 456$.

Notice that member data of the object itself can be accessed without any . operator.

In the client program, notice how Cell c1 defines an object - same as defining a variable.

Class $ \leftrightarrow$ type, object $ \leftrightarrow$ variable.

4.2.2.0.3 Output from CellT.java

c1 = Cell: 0
c2 = Cell: 123
c1.put(456), c1 = Cell: 456
c1= c2; c1 = Cell: 123 //**1
c2 = Cell: 123
c2.put(222); c1 = Cell: 222
c2 = Cell: 222

4.3 Reference versus Value - Aliases

Carefully inspect the following:

  // reference semantics
  c2.put(222);
  o.println("c2.put(222); c1 = " + c1);
  o.println("c2 = " + c2);

c2.put(222); c1 = Cell: 222
c2 = Cell: 222

Although we have written only to c2 c2.put(222); c1 has changed too!

Contrast the corresponding behaviour of int variables:

  int i1= 456, i2= 123;
  i1 = i2; // both now have value 123

  i2= 222; // i2 has value 222
           // but i1 RETAINS value 123

Why did c1 change its value, even though we did nothing to it since line //**1.?

This is all because object identifiers denote references rather than values. And the assignment c1= c2; has reference semantics rather than copy semantics.

After c1= c2;, the reference c1 refers to the same object that c1 references. c1, c2 are so-called aliases. Thus, before the assignment c1= c2;:

        
  Cell reference            Cell object
  +---------+               +---------+
  |    c1   +-------------->| 456     |
  +---------+               +---------+

  +---------+               +---------+
  |    c2   +-------------->| 123     |
  +---------+               +---------+

c1= c2;

Now c1, c2 refer to (point to) the same object - and the Cell object with $ 456$ in it becomes garbage.

  +---------+               +---------+
  |    c1   +--------+      | 456     |   
  +---------+        |      +---------+
                     |
  +---------+        +----->+---------+
  |    c2   +-------------->| 123     |
  +---------+               +---------+

From now on, anything you do to c2 you (effectively) do to c1.

Usually, reference semantics is fine, and, as in the case here, you have to try pretty hard to get seemingly silly things to happen.

On the other hand, if you want a true copy - copy, value semantics - then you must ensure that your class is equipped with a clone() method. To do justice to that topic we must wait until we get to inheritance in the next chapter. However, later on in this chapter, we do show an example of a clone() method.

4.4 A Time Class

4.4.1 Informal Specification of the Class

We want to design a type capable of representing the Time of day.

4.4.1.0.1 State

We would expect a Time object to be able to store a Time state, i.e. hours, minutes, seconds.

How would we like a Time object to behave? Eventually, we'll build up to a having behaviour like add and subtract, and increment, decrement, that you have grown used to for types like int, double. Of course, because Time is to be used in a computer program, we need constructors etc. Also, maybe it's worth thinking about how one interacts with time in a digital watch.

Constructors
Since Time is to be used in a computer program we need to be able to declare, define and create Time objects.

Inspectors
We should be able to obtain the individual components.

Mutator
We should be able to change a Time object - we specify this as addition.

Input-output
We require - more for the purposes of demonstration than anything else - facilities to display a Time object on the screen, or write it to a file; in Java, the convention is to provide a toString method - that way you are not tied to any device, but can use whatever facilities that are provided for text strings.

We call this class Time1 because we will develop it further in Time2, Time3, and eventually a final version in Time.

4.4.2 Class Time1

/**
 * Time1.java -- Time ADT -- crude to start
 * @author j.g.c. 14/01/00
 * see Deitel & Deitel Chapter 8 (but different).
 * for MSc CSA, QUB.
*/
public class Time1 {
   private int hour; // 0 - 23
   private int min;  // 0 - 59
   private int sec;   // 0 - 59

   // constructor initializes to (0, 0, 0)
   public Time1() {
     System.out.println("Time1 constr.");
     set( 0, 0, 0 );
   }

   public void set(int h, int m, int s ){
     setHour(h);
     setMin(m);
     setSec(s);
   }

   public void setHour(int h){
      hour = ( ( h >= 0 && h < 24 ) ? h : 0 );
   }
   
   public void setMin(int m){
      min = ( ( m >= 0 && m < 60 ) ? m : 0 );
   }

   public void setSec(int s){
      sec = ( ( s >= 0 && s < 60 ) ? s : 0 );
   }

   public int getHour(){
     return hour;
   }

   public int getMin(){
     return min;
   }

   public int getSec(){
     return sec; 
   }
}

4.4.2.0.1 Dissection of Time1

  1. Notice the print statement in the constructor - just in case you are not yet convince that a constructor contains code that must be executed.

  2. Consistent state. Notice how the mutators setHour(), setMin(), setSec() ensure that user programs cannot put a Time1 object into an inconsistent state.

4.4.3 A User Program

The program Time1T.java demonstrates the use of the Time1 class.

/**
 * Time1T.java -- Test for Time1 ADT
 * @author j.g.c. 14/01/00
 * see Deitel & Deitel Chapter 8 (but different).
 * for MSc CSA, QUB.
*/
public class Time1T {
  public static void main( String args[]){
    System.out.println("creating Time1 t ...");
    Time1 t = new Time1();

    System.out.println("current value of t ...");
    System.out.println("t = "+t.getHour()+":"+t.getMin()+":"+t.getSec());
     
    t.set(10, 15, 5 ); 
    System.out.println("current value of t ...");
    System.out.println("t = "+t.getHour()+":"+t.getMin()+":"+t.getSec());  

    t.set(29, 69, 65); 
    System.out.println("current value of t ...");
    System.out.println("t = "+t.getHour()+":"+t.getMin()+":"+t.getSec());  
 
  }
}

4.4.4 Class Time2

Now, as discussed under Cell, we improve with an overloaded initializing constructor, and a toString() method.

/**
 * Time2.java -- Time ADT
 * @author j.g.c. 14/01/00
 * from Time1: + overloaded constr. + toString(); for MSc CSA, QUB.
*/
import java.text.DecimalFormat; //*
public class Time2 {
   private int hour; // 0 - 23
   private int min;  // 0 - 59
   private int sec;   // 0 - 59

   // constructor initializes to (0, 0, 0)
   public Time2() {
     System.out.println("default constr.");
     set( 0, 0, 0 ); }

   public Time2(int h, int m, int s) { //*
     System.out.println("initialising constr.");
     set(h, m, s); }

   public void set(int h, int m, int s ){
     setHour(h);
     setMin(m);
     setSec(s); }

   //etc.... as Time1

   // Convert to String in universal-time format
   public String toUniversalString(){ //*
     DecimalFormat twoDigits = new DecimalFormat( "00" );

     return twoDigits.format(hour) + ":" +
             twoDigits.format(min) + ":" + twoDigits.format(sec);
   }

   // Convert to String in standard-time format
   public String toString() { //*
     DecimalFormat twoDigits = new DecimalFormat( "00" );
      
     return ( (hour == 12 || hour == 0) ? 12 : hour % 12 ) +
             ":" + twoDigits.format(min) +
             ":" + twoDigits.format(sec) + ( hour < 12 ? " AM" : " PM" );
   }
}

4.4.5 A User Program for Time2

/**
 * Time2T.java -- Test for Time2 ADT
 * @author j.g.c. 14/01/00
 * see Deitel & Deitel Chapter 8 (but different).
 * for MSc CSA, QUB.
*/
public class Time2T {
  public static void main( String args[]){
    System.out.println("creating Time2 t1 ... Time2 t1 = new Time2();");
    Time2 t1 = new Time2();
    System.out.println("creating Time2 t ... Time2 t2 = new Time2(9, 15, 55);");
    Time2 t2 = new Time2(9, 15, 55);
    
    System.out.println("current value of t1 ...");
    System.out.println("t1 = "+ t1.toString());
    // note that toString() called implicitly as in ...
    System.out.println("t1 = "+ t1);
    System.out.println("t1 = "+ t1.toUniversalString());

    System.out.println("current value of t2 ...");
    System.out.println("t2 = "+ t2);

    t1.set(14, 15, 5 ); 
    System.out.println("current value of t ...");
    System.out.println("t1 = "+ t1);  
   }
}

4.4.5.0.1 Dissection

  1. Note that toString() called implicitly in System.out.println("t1= " t1);+

  2. Note the use of DecimalFormat - if you want things lined up nicely, you must format using this class. Don't learn off anything to do with DecimalFormat, just know that it's there if you need it.

  3. When you execute it, note the messages indicating the execution of the different constructors.

4.4.6 Class Time3

Now, as discussed under Cell, we improve with an overloaded initialising constructor, and a toString() method.

/**
 * Time3.java -- Time ADT
 * @author j.g.c. 14/01/00
 * from Time2: + addTo + subFrom + inc + dec + this
 * for MSc CSA, QUB.
*/
import java.text.DecimalFormat; 
public class Time3 {

  private static final int daySecs = 24*60*60;

  // constructor initializes to (0, 0, 0)
  public Time3() {
    set( 0, 0, 0 );
  }

  public Time3(int h, int m, int s) {
    set(h, m, s);
  }

  // etc. as Time2

  private int toSecs(){ //*
    return sec + 60*(min + 60*hour);
  }

  private void fromSecs(int t){ //*
    sec= t%60; 
    t= t/60; // now in minutes
    min= t%60;
    hour= t/60; //should be 0..23 if callers behave properly
  }  

  public void addTo(Time3 other){ //*
    int s= toSecs() + other.toSecs();
    // could have written int s= this.toSecs() + other.toSecs();
    s= s%daySecs; //ensure not >= 24 hours 
    fromSecs(s);
  }

  public void subFrom(Time3 other){ //*
    int s= toSecs() - other.toSecs();
    // could have written int s= this.toSecs() + other.toSecs();
    s+= daySecs; //because % doesn't give expected result for -ve
    s= s%daySecs; //ensure not >= 24 hours 
    fromSecs(s);
  }

  public void inc(){
    addTo(new Time3(0,0,1));
  }

  public void dec(){
    subFrom(new Time3(0,0,1));
  }

  //etc. as Time2.

  private int hour; // 0 - 23
  private int min;  // 0 - 59
  private int sec;   // 0 - 59
}

4.4.6.0.1 Dissection

  1. Notice that since it's handy to do the arithmetic in seconds (since midnight), we define private methods toSecs(), fromSecs.

  2. Notice, in addTo, subFrom we have to be careful about negatives, and greater than 23:59:59. The Time number system is like the unsigned and twos-complement systems that we cover in Computer Architecture - it is a modulo system, or `circular'; that is, 23:59:59 + 00:00:01 = 00:00:00.

  3. addTo, subFrom are asymmetric - there is a definite `receiver' of the message, and the `other' object.

  4. In int s= toSecs() + other.toSecs();, note that toSecs() refers to the object itself.

  5. this. If you ever need to refer to the object itself, inside one of its methods, you can use this; hence, we could replace:

    int s= toSecs() + other.toSecs(); with

    int s= this.toSecs() + other.toSecs();

  6. private static final int daySecs = 24*60*60; defines a constant (final) that is a class variable - as opposed to an instance variable (static). We keep it private, however, as in e.g. Math.PI, it could be made public if user programs needed it.

4.4.7 A User Program for Time3

/**
 * Time3T.java -- Test for Time3 ADT
 * @author j.g.c. 14/01/00
 * see Deitel & Deitel Chapter 8 (but different).
 * for MSc CSA, QUB.
*/
public class Time3T {
  public static void main( String args[]){
    Time3 t1 = new Time3(1,2,3);
    Time3 t2 = new Time3(4,5,59);
    Time3 t3 = new Time3(23,59,59);
    Time3 t4 = new Time3(1,2,3);

    System.out.println("t1 = "+ t1.toString());
    System.out.println("t2 = "+ t2);
    System.out.println("t3 = "+ t3);
    System.out.println("t4 = "+ t4);

    // notice that t1.toString() called automatically
    t1.addTo(t2);
    System.out.println("after t1.addTo(t2); t1 = "+ t1);

    t1.addTo(t3);
    System.out.println("after t1.addTo(t3); t1 = "+ t1);

    t2.subFrom(t4);
    System.out.println("after t2.subFrom(t4); t2 = "+ t2);

    t4.subFrom(t3);
    System.out.println("after t4.subFrom(t3); t4 = "+ t4);

    t4.inc();
    System.out.println("after t4.inc(); t4 = "+ t4);

    t4.dec();      
    System.out.println("after t4.dec(); t4 = "+ t4);
  }
}

4.4.7.0.1 Output from Time3T

t1 = 1:02:03 AM
t2 = 4:05:59 AM
t3 = 11:59:59 PM
t4 = 1:02:03 AM
after t1.addTo(t2); t1 = 5:08:02 AM
after t1.addTo(t3); t1 = 5:08:01 AM
after t2.subFrom(t4); t2 = 3:03:56 AM
after t4.subFrom(t3); t4 = 1:02:04 AM
after t4.inc(); t4 = 1:02:05 AM
after t4.dec(); t4 = 1:02:04 AM

4.4.8 Class Time

Class Time is the finished job that corrects some inadequacies in Time3.

/**
 * Time.java -- Time ADT -- almost final
 * @author j.g.c. 14/01/00
 * from Time3: 
 * + static add, sub + make mutators return value + this + clone
 * for MSc CSA, QUB.
*/
import java.text.DecimalFormat; 
public class Time implements Cloneable {

  private static final int daySecs = 24*60*60;

  // constructor initializes to (0, 0, 0)
  public Time() {
    set( 0, 0, 0 );
  }

  public Time(int h, int m, int s) {
    set(h, m, s);
  }

  public Object clone() {
    Time t = new Time();
    t.set(hour, min, sec);
    return t;
  }

  public Time set(int h, int m, int s ){
    setHour(h);
    setMin(m);
    setSec(s);
    return this;
  }

  public Time setHour(int h){
    hour = ( ( h >= 0 && h < 24 ) ? h : 0 );
    return this;
  }
   
  public Time setMin(int m){
    min = ( ( m >= 0 && m < 60 ) ? m : 0 );
    return this;
  }

  public Time setSec(int s){
    sec = ( ( s >= 0 && s < 60 ) ? s : 0 );
    return this;
  }

  public int getHour(){
    return hour;
  }

  public int getMin(){
    return min;
  }

  public int getSec(){
    return sec; 
  }

  private int toSecs(){
    return sec + 60*(min + 60*hour);
  }

  private void fromSecs(int t){ 
    sec= t%60; 
    t= t/60; // now in minutes
    min= t%60;
    hour= t/60; //should be 0..23 if callers behave properly
  }  

  public Time addTo(Time other){
    int s= toSecs() + other.toSecs();
    // could have written int s= this.toSecs() + other.toSecs();
    s= s%daySecs; //ensure not >= 24 hours 
    fromSecs(s);
    return this;
  }

  public Time subFrom(Time other){
    int s= toSecs() - other.toSecs();
    s+= daySecs; //because % doesn't give expected result for -ve
    s= s%daySecs; //ensure not >= 24 hours 
    fromSecs(s);
    return this;
  }

  public Time inc(){
    addTo(new Time(0,0,1));
    return this;
  }

  public Time dec(){
    subFrom(new Time(0,0,1));
    return this;
  }

  public static Time add(Time t1, Time t2){
    Time t= (Time)t1.clone(); //copy of t1
    t.addTo(t2);
    return t;
  }

  public static Time sub(Time t1, Time t2){
    Time t= (Time)t1.clone(); //copy of t1
    t.subFrom(t2);
    return t;
  }

  // Convert to String in universal-time format
  public String toUniversalString(){
    DecimalFormat twoDigits = new DecimalFormat( "00" );

    return twoDigits.format(hour) + ":" +
             twoDigits.format(min) + ":" +
             twoDigits.format(sec);
  }

  // Convert to String in standard-time format
  public String toString() {
    DecimalFormat twoDigits = new DecimalFormat( "00" );
      
    return ( (hour == 12 || hour == 0) ? 12 : hour % 12 ) +
             ":" + twoDigits.format(min) +
             ":" + twoDigits.format(sec) +
             ( hour < 12 ? " AM" : " PM" );
  }

  private int hour; // 0 - 23
  private int min;  // 0 - 59
  private int sec;   // 0 - 59
}

4.4.8.0.1 Dissection

  1. Mutators all return a reference to the mutated object (this). Thus:

      public Time setHour(int h){
        hour = ( ( h >= 0 && h < 24 ) ? h : 0 );
        return this;
      }
    

    This allows chaining of operations, e.g. (t1.setHour(22) ).addTo(t2);

  2. Note static add, sub which correct the asymmetry of addTo, subFrom. To me, the following looks more like normal addition: t1 = Time.add(t3, t4);. They must be static because no object `owns' the method.

  3. Note our provision of a clone() method - and how a cloned (copy) assignment differs from a reference assignment. With clone, a complete copy of the object is made. But the details must wait until the next chapter.

4.4.9 A User Program for Time

/**
 * TimeT.java -- Test for Time ADT
 * @author j.g.c. 15/01/00
 * for MSc CSA, QUB.
 * from Time3T
*/
public class TimeT {
  public static void main( String args[]){
    Time t1 = new Time(1,2,3);
    Time t2 = new Time(4,5,59);
    Time t3 = new Time(23,59,59);
    Time t4 = new Time(1,2,3);

    System.out.println("t1 = "+ t1);
    System.out.println("t2 = "+ t2);
    System.out.println("t3 = "+ t3);
    System.out.println("t4 = "+ t4);

    // notice that t1.toString() called automatically
    Time t5 = new Time();
    t5= t1.addTo(t2);
    System.out.println("after t1.addTo(t2); t1 = "+ t1);
    System.out.println("after t5= t1.addTo(t2); t5 = "+ t5);

    t1.addTo(t3);
    System.out.println("after t1.addTo(t3); t1 = "+ t1);

    t2.subFrom(t4);
    System.out.println("after t2.subFrom(t4); t2 = "+ t2);

    t4.subFrom(t3);
    System.out.println("after t4.subFrom(t3); t4 = "+ t4);

    t4.inc();
    System.out.println("after t4.inc(); t4 = "+ t4);

    t4.dec();      
    System.out.println("after t4.dec(); t4 = "+ t4);

    t1 = Time.add(t3, t4); //static
    System.out.println("after t1 = add(t3, t4); t1 = "+ t1);

    t2 = Time.sub(t1, t3);
    System.out.println("after t2 = sub(t1, t3); t2 = "+ t2);

    (t2.setHour(22) ).addTo(t1);
    System.out.println("after (t2.setHour(22) ).addTo(t1); t2 = "+ t2);

    Time t6 = new Time(4,5,6);
    Time t7 = new Time(5,6,7);
    Time t8 = new Time(6,7,8);
    Time t9 = new Time(7,8,9);

    System.out.println("t6 = "+ t6);
    System.out.println("t7 = "+ t7);
    System.out.println("t8 = "+ t8);
    System.out.println("t9 = "+ t9);

    t6= t7;
    System.out.println("after t6= t7; t6 = "+ t6);
    System.out.println("t7 = "+ t7);

    t6.setHour(11);
    System.out.println("after t6.setHour(11); t6 = "+ t6);
    System.out.println("t7 = "+ t7);

    t8= (Time)t9.clone();
    System.out.println("after t8= t9.clone(); t8 = "+ t8);
    System.out.println("t9 = "+ t9);

    t8.setHour(11);
    System.out.println("after t8.setHour(11); t8 = "+ t8);
    System.out.println("t9 = "+ t9);
  }
}

4.4.9.0.1 Output from TimeT

$java TimeT      
t1 = 1:02:03 AM
t2 = 4:05:59 AM
t3 = 11:59:59 PM
t4 = 1:02:03 AM
after t1.addTo(t2); t1 = 5:08:02 AM
after t5= t1.addTo(t2); t5 = 5:08:02 AM
after t1.addTo(t3); t1 = 5:08:01 AM
after t2.subFrom(t4); t2 = 3:03:56 AM
after t4.subFrom(t3); t4 = 1:02:04 AM
after t4.inc(); t4 = 1:02:05 AM
after t4.dec(); t4 = 1:02:04 AM
after t1 = add(t3, t4); t1 = 1:02:03 AM
after t2 = sub(t1, t3); t2 = 1:02:04 AM
after (t2.setHour(22) ).addTo(t1); t2 = 11:04:07 PM
t6 = 4:05:06 AM
t7 = 5:06:07 AM
t8 = 6:07:08 AM
t9 = 7:08:09 AM
after t6= t7; t6 = 5:06:07 AM
t7 = 5:06:07 AM
after t6.setHour(11); t6 = 11:06:07 AM
t7 = 11:06:07 AM
after t8= t9.clone(); t8 = 7:08:09 AM
t9 = 7:08:09 AM
after t8.setHour(11); t8 = 11:08:09 AM
t9 = 7:08:09 AM

4.5 Time with a complete change of representation

Here is another class Time that completely changes the representation -- now we use seconds-since-midnight, yet users of the class are none the wiser. But just think what would have happened if we had allowed users to directly access hour, min, sec.

/**
 * Time.java -- Time ADT -- now final (int representation)
 * @author j.g.c. 15/01/00
 * from Time (hour, min sec): 
 * for MSc CSA, QUB.
*/
import java.text.DecimalFormat; 
public class Time implements Cloneable {

  private static final int daySecs = 24*60*60;
  private static final int hourSecs= 60*60;

  // constructor initializes to (0, 0, 0)
  public Time() {
    set( 0, 0, 0 );
  }

  public Time(int h, int m, int s) {
    set(h, m, s);
  }

  public Object clone() {
    Time t = new Time();
    t.secs= secs;
    return t;
  }

  public Time set(int h, int m, int s ){
    int hour = ( ( h >= 0 && h < 24 ) ? h : 0 );
    int min = ( ( m >= 0 && m < 60 ) ? m : 0 );
    int sec = ( ( s >= 0 && s < 60 ) ? s : 0 );
    secs = sec + (hour*60 + min)*60;
    return this;
  }

  public Time setHour(int h){
    // notice that set will ensure consistency
    set(h, getMin(), getSec());
    return this;
  }
   
  public Time setMin(int m){
    set(getHour(), m, getSec());
    return this;
  }

  public Time setSec(int s){
    set(getHour(), getMin(), s);
    return this;
  }

  public int getHour(){
    return secs/hourSecs;
  }

  public int getMin(){
    return (secs/60)%60;
  }

  public int getSec(){
    return secs%60; 
  }

  private int toSecs(){
    return secs;
  }

  public Time addTo(Time other){
    secs= secs + other.secs;
    secs= secs%daySecs; //ensure not >= 24 hours 
    return this;
  }

  public Time subFrom(Time other){
    secs= secs - other.secs;
    secs+= daySecs; //because % doesn't give expected result for -ve
    secs= secs%daySecs; //ensure not >= 24 hours
    return this;
  }

  public Time inc(){
    addTo(new Time(0,0,1));
    return this;
  }

  public Time dec(){
    subFrom(new Time(0,0,1));
    return this;
  }

  public static Time add(Time t1, Time t2){
    Time t= (Time)t1.clone(); //copy of t1
    t.addTo(t2);
    return t;
  }

  public static Time sub(Time t1, Time t2){
    Time t= (Time)t1.clone(); //copy of t1
    t.subFrom(t2);
    return t;
  }

  // Convert to String in universal-time format
  public String toUniversalString(){
    DecimalFormat twoDigits = new DecimalFormat( "00" );

    return twoDigits.format(getHour()) + ":" +
             twoDigits.format(getMin()) + ":" +
             twoDigits.format(getSec());
  }

  // Convert to String in standard-time format
  public String toString() {
    DecimalFormat twoDigits = new DecimalFormat( "00" );
    int hour = getHour();
    return ( (hour == 12 || hour == 0) ? 12 : hour % 12 ) +
             ":" + twoDigits.format(getMin()) +
             ":" + twoDigits.format(getSec()) +
             ( hour < 12 ? " AM" : " PM" );
  }

  private int secs;
}

4.5.1 A User Program for the new Time

Just to prove it, compare the user program below with the previous one for the (hour, min, sec) version - no change. Some whilst the internals have changed, the public operations (the behaviour), and the actual (abstract) set of values have not changed.

/**
 * TimeT.java -- Test for Time ADT
 * @author j.g.c. 15/01/00
 * for MSc CSA, QUB.
 * from Time3T
*/
public class TimeT {
  public static void main( String args[]){
    Time t1 = new Time(1,2,3);
    Time t2 = new Time(4,5,59);
    Time t3 = new Time(23,59,59);
    Time t4 = new Time(1,2,3);

    System.out.println("t1 = "+ t1);
    System.out.println("t2 = "+ t2);
    System.out.println("t3 = "+ t3);
    System.out.println("t4 = "+ t4);

    // notice that t1.toString() called automatically
    Time t5 = new Time();
    t5= t1.addTo(t2);
    System.out.println("after t1.addTo(t2); t1 = "+ t1);
    System.out.println("after t5= t1.addTo(t2); t5 = "+ t5);

    t1.addTo(t3);
    System.out.println("after t1.addTo(t3); t1 = "+ t1);

    t2.subFrom(t4);
    System.out.println("after t2.subFrom(t4); t2 = "+ t2);

    t4.subFrom(t3);
    System.out.println("after t4.subFrom(t3); t4 = "+ t4);

    t4.inc();
    System.out.println("after t4.inc(); t4 = "+ t4);

    t4.dec();      
    System.out.println("after t4.dec(); t4 = "+ t4);

    t1 = Time.add(t3, t4); //static
    System.out.println("after t1 = add(t3, t4); t1 = "+ t1);

    t2 = Time.sub(t1, t3);
    System.out.println("after t2 = sub(t1, t3); t2 = "+ t2);
  }
}

4.6 Composition

Classes may be composed of objects from other classes - see [Deitel and Deitel, 1999, section 8.11, p. 355]; see also section 5.9.

4.7 Types, Variables, Values

4.7.1 Introduction

The purpose of this section is to bring some order and formality to the notions of type, variable and value in programming languages.

For the purposes of a course on object-oriented programming, we can make the following close analogies:

Indeed, object-oriented programming usefully can be seen as programming types, and classes simply as an enrichment of the native type system. When we develop a class, we want to be able to use it in client programs with all the freedom that we use a native type.

When you have studied this chapter you should be familiar with the concept of a data type, know why a type system is useful, be able to distinguish between value and variable, be aware of the constituents of a variable, know the significance of static & dynamic typing, and be aware of potential problems associated with aliases, dangling references, and garbage, and know how to guard against these.

In addition, you will be able to connect the concepts of types, variables, values, etc. with classes, objects, object state, etc.

4.7.2 Data Types

A data type, or simply type, is made up of two constituents:

  1. A set of values, e.g. boolean: {false, true}, byte: {-128..127}

  2. The set of operations or functions that can be applied to the members of the set, e.g. for int, +, -, *, /, etc.

Since assembler programmers get away without types (or just one or two types e.g.. byte and word). What are the advantages conferred by including a type system?

  1. Invisibility of the underlying internal representation. That is, abstraction away from (for example) the fact that a float is represented as four contiguous bytes. This promotes:

  2. Correct use of variables can be checked: by the compiler if static typing, by the run-time system if dynamic typing.

  3. Disambiguation of operators and functions: again, if static typing, at compile time, or if dynamic typing, at run-time.

4.7.3 Values

A value is an element of the set associated with a type. E.g. 3 is a value from the set .., -1 ,0, 1, 2, 3, .. associated with the type int in Java.

4.7.3.0.1 Characteristics of values

In order to contrast values with variables we note the following:

4.7.4 Variables

In traditional imperative programming languages, a variable has associated with it:

4.7.4.0.1 Static Typing

Usually, an identifier is statically bound to a type, i.e. the type of the variable is known at compile time, and is fixed thereafter.

4.7.4.0.2 Dynamic Typing

However, in some languages, the binding of type is dynamic, in which case the type may change according to the last assignment, e.g. APL, Smalltalk, Objective-C, and to a limited extent, Java, as we shall see.

4.7.5 L-Values and Values

In imperative languages, when a variable must be referenced, its identifier gets translated into a location (address), i.e. the program must read from or write to the associated memory cell.

There is a subtle difference, however, whether the variable is on the receiving end, or on the delivering end of an assignment.

The difference lies in the semantics of the expressions, for example, a, and b in:

b = a; // Java

or, in general,

<expression1> <assignment operator> <expression2>

in any imperative language.

The difference is that expression1 must evaluate to a location / address, whilst expression2 evaluates to a value. In C/C++ it has become usual to use the term, L-value - from Lefthand-value, for a location/address.

Sometimes, but less frequently, the term R-value - from righthand-value - is used for a true value.

Example. In Java,

                    int a = 25, b;
                    b = a;    /* e.g.. a is at address 1000, and
                              1000 contains 25, b is at 1001 */

translates to: get the value contained in cell address 1000, [1000] in some notations, put that value in 1001 (b); i.e. the expression a on the right-hand side translates to a value (25), whilst the expression b on the left-hand side evaluates to an address (1000).

In the case of pointer variables, the values are themselves addresses.

4.7.6 Aliases

Two variables are aliases if they share the same memory / data object. An example, in Java, from earlier in the chapter:

     Cell c1 = new Cell(456);
     Cell c2 = new Cell(123);
     //both c1 and c2 are references
     t1 = t2;

        
  Cell reference            Cell object
  +---------+               +---------+
  |    c1   +-------------->| 456     |
  +---------+               +---------+

  +---------+               +---------+
  |    c2   +-------------->| 123     |
  +---------+               +---------+

c1= c2;

Now c1, c2 refer to (point to) the same object - and the Cell object with $ 456$ in it becomes garbage.

  +---------+               +---------+
  |    c1   +--------+      | 456     |   
  +---------+        |      +---------+
                     |
  +---------+        +----->+---------+
  |    c2   +-------------->| 123     |
  +---------+               +---------+

From now on, anything you do to c2 you (effectively) do to c1.

4.7.7 Dangling References

In C and C++ it is possible for references to become dangling:

     int *pa,x;
     {
        int a; /*local to this block!*/
        a=22;
        pa=&a; /*pa points at a*/
     }
          /*a is now destroyed, and pa is 'DANGLING'*/
     x = *pa; /* x <- rubbish*/
     *pa= 22; /*22 gets written to where 'a' was - WORSE*/

4.7.8 Uninitialized Pointers

Uninitialized pointers closely resemble dangling pointers. They are a big problem in C/C++; they exist in Java too, but the language watches out for them.

Example in Java:

Cell a;

a is a reference to a Cell object we haven't indicated any object yet. In fact, Cell a; will contain a null reference.

In C/C++, a could have contained a random reference/address, and any attempt to write to it could write to some part of memory not intended - like part of the program!

4.7.9 Garbage

Garbage is the opposite problem to dangling pointers; a dangling pointer points at something that effectively doesn't exist, garbage is memory location(s) that has been allocated, but whose reference has been destroyed, therefore it has no name and is effectively lost.

Example in C++:

     int fred(int a, int b) //not a very useful function {
          int* pi; int i;

          pi = new int;
          *pi = a + b;
          i = *pi;
                    //should have delete pi here!
          return i;
     }

as soon as the function returns, pi is destroyed; but the piece of heap memory that new created remains allocated - and cannot ever again be referenced, even to deallocate/free (delete) it.

On the other hand, in languages like Eiffel and Java, that offer garbage collection, garbage is never a problem - when an object becomes stranded (like the Cell object containing $ 456$ above, the garbage collector can detect this, and the memory that was used can be recycled/deallocated automatically.

4.8 Scope of variables

The scope or visibility of a variable (or function) is those parts of a program where the name can be used to access the variable (or function); simply, scope is the range of instructions over which the name is visible.

Lifetime is a related but distinct concept, see section  4.10.

The scope of automatic / local variables defined in a function or block is from the definition beginning until the end of the function or block: they have local scope, they are private to that function; thus, functions have a form of encapsulation.

Local names that are reused, in the same or different source files, or in different functions or blocks, are unrelated.

Example.

int fred()
{                             |
  int x,y,z,a,b; /* (1)*/     |scope of x, y ..a, b
                              |
  x=1;y=1;a=32;b=33;          |
  z=add(x,y);                 |
  return z;                   |
}
int add(int a, int b)
{                        |
  int value;             | scope of a, b, value
                         |
  value=a+b;             | these a,b NOT related to above a,b
  return value;          |
}

4.8.0.0.1 Blocks are scope units

Rather like functions, blocks are scope units. Thus, within a block, you can declare a variable and its scope will be from the point of declaration to the end brace (}) of the block.

Thus:

int fred(int a)
{
  int b;
  { //c is in scope only in this little block
   int c; c= 22;
  }
  b= a+10;
  return b;
}

4.8.0.0.2 Scope Hiding

Consider:

     void fred(int n, float b)
     {
       int c=33,d=16,i;

       for(i=0;i<=n;i++){
         float d; // note these d, c are different from outer
         d=22.5;
         int c=49;
       }
       // here c==33, d==16.
     }

The outer d - the one declared first - is hidden in the block governed by the for, by the inner declaration.

Later, in the next chapter, 5.4 we give details of scope/visibility modifiers in Java classes, i.e. public, private, protected.

4.9 Heap memory management

4.9.1 Introduction

If you deal only with types like int, char, double (but not String or arrays - which are object, you can become used to memory management being done automatically. Thus:

int fred(int a)
{
  int b; //here b created automatically
  b= a*10+2;
  return b;
  //here the value of b is returned, and b deleted (destroyed)
}

Sometimes, but only in very special cases, we may need to take direct control of the creation and deletion. In the example, b -- and a - is allocated on the stack.

Unlike stack variables, which are created and destroyed automatically, heap the memory management (creation) of heap variables must be programmed. (In C, C++, which have no garbage collection you must also be very careful about deletion of heap objects).

Heap variables are created using new.

4.9.2 Garbage

Garbage refers to the situation of heap memory, whose reference has been deleted or made to refer to elsewhere. Once this reference is lost, the heap memory may never again be accessed by the user program.

Thus, garbage is in some ways the opposite to dangling or uninitialized references - in which the reference exists, but what it references does not.

Again in comparison to dangling references, garbage may be more benign - it can cause program failure only by repeated memory leak leading eventually to the supply free memory becoming exhausted.

Note: do not be confused by the English connotation of the word, garbage does not refer to uninitialized variables. And, the phrase garbage-in garbage-out refers to an entirely different notion.

4.9.2.0.1 Java

In Java, all non-elementary variables (all objects and arrays - well, arrays are considered to be objects) are allocated on the heap. A consequence of this is that objects obey reference semantics rather than value semantics; beware, this is more subtle than may appear at first - for the references are passed-by-value to functions!

In addition, Java has garbage collection. Thus, whilst you need to create objects with new you do not delete them. When the connection between a Java reference and its object is eventually broken - by the reference going out of scope, or by the reference being linked to another object - the object is subjected to garbage collection.

4.10 Lifetime of variables

The lifetime of a variable is the interval of time for which the variable exists; i.e. the time from when it is created to when it is destroyed; duration, span, or extent are equivalent terms for the same thing.

It is common to find confusion between scope and lifetime - though they are in cases related, they are entirely different notions: lifetime is to do with a period of time during the execution of a program, scope is to do with which parts of a program text. In Java, lifetime is dynamic - you must execute the program (or do so in a thought experiment) in order to determine it. Scope is static - determinable at compile time, or by reading the program text.

In the case of local variables (local to blocks or functions), and where there are no scope-holes, lifetime and scope correspond: scope is the remainder of the block / function after the variable definition; lifetime is the whole time that control is in that part of the program from the definition to the end of the block / function.

Example, local variables.

int fred(int a, int b)
  {
  int c;  /*when control passes into 'fred': local
          variables a,b, c are created at this time -- their
          LIFETIME starts then*/

  c= 22;
  ...
  }       /*when control reaches here, locals are destroyed*/
.

Thus, c, and a, b exist only for the duration of the call to function fred. For each call, entirely new variables are created and destroyed.

4.10.1 Lifetime of variables - summary

It is possible to summarize the various lifetime classes by classifying them according to increasing degrees of persistence, from transient - very short lifetime - to persistent - very long lifetime:

Temporary variables
Used in evaluation of an expression, e.g.

y = x * (x + 1.0) + 2.0 * x;

will almost certainly involve temporary variables, e.g. a, b and c, which exist only during the evaluation of the expression:

     a = x + 1.0; b = x * a; c = 2 * x;

     y = a + b + c;

Local variables
Their lifetime is from entry to their definition to exit from their block / function.

Heap variables
Their lifetime is from allocation to until deallocation - which, in Java, is done by the garbage collector only when there is no reference whatsoever pointing to the object.

Variables held in files
Their lifetime is over many program executions; they are persistent. Persistence of objects is of significant interest for database applications - object-oriented databases.

4.11 Exercises

For programming exercises, I advise you to create a directory structure as follows: ads721/ch4/cell - for Cell, /time - for time, etc. A tidy directory will make your work easier. Then, using a web browser, download the files from http://www.cs.qub.ac.uk/~J.Campbell/csc721/progs/ch4/cell/ - simply click on the filenames, and save-as.

  1. For the purposes of examinations, job interviews, etc. make sure that you can define the following terms - illustrated with appropriate examples: class, object, encapsulation, information hiding, private, public, protected, message, method, instance member (method or variable), class member (method or variable), type, variable, value, reference, reference versus value, alias, copy versus reference semantics, clone, state, behaviour, representation versus behaviour, operation, constructor, destructor (not so relevant for Java), inspector, mutator, lifetime / duration / span, garbage, heap memory, scope of variable/object declaration, global variables / objects; and getting towards the fringes: mutable object, immutable object.

  2. See Cell1T.java. When the comment symbols are removed from the following, //c1.v= 22;, there will be a compiler error; (a) what is the nature of the error; (b) suggest, with an evaluation, possible ways to avoid the error.

  3. In the context of object-oriented programming, give a rationale for information hiding; likewise encapsulation.

  4. In the context of the following, explain what will be output (just give the values) for each of the println()s; why might the result be a little surprising for someone used to just int, double?

      c2.put(123);
      c1.put(456);
      c1= c2;
      o.println("c1= c2, Cell c1 value = " + c1.get());
      o.println("c1= c2, Cell c2 value = " + c2.get());
    
      c1.put(789);
      o.println("c1.put(789), Cell c1 value = " + c1.get());
      o.println("c1.put(789), Cell c2 value = " + c2.get());
    

  5. In the context of the previous exercise, explain the role of clone().

  6. Explain the reference this. In Cell.get() show how to make explicit use of this.

  7. Copy Cell.java to CellD.java and convert it to hold a double value, rather than int. Provide four constructors - default (CellD()); CellD(int), CellD(double), CellD(String s); in the case of CellD(String s), use double Double.parseDouble(String s) to convert the String to a double.

    Based on CellT.java, write an appropriate exercising program CellDT.java.

  8. Copy Cell.java to Cell3.java and provide a method void addTo(Cell3 other){v= v other.get()+; if necessary, see Time3. Also, provide subFrom.

    Based on CellT.java, write an appropriate exercising program Cell3T.java which tests subFrom and addTo.

  9. Choose either of the Time classes; use it to create a new class DTime which represents also 100ths of a second; (a) choose an appropriate representation - discuss your rationale; (b) what additional methods will need to be provided? (c) implement the new class; (d) provide a test program DTimeT.java.


next up previous contents
Next: 5. Inheritance, Polymorphism and Up: Algorithms and Data Structures Previous: 3. Introduction to Object-oriented   Contents
jc 2005-11-16