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:
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.
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.
Cell object to be able to store
the current state, i.e. its integer value.
Cell object to behave?
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.
Cell object
into a humanly readable format; for this we provide a toString()
function.
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);
}
}
public.
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.
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).
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 ....
public,
the interface functions would have been inaccessible by user
programs.
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
.
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
type, object
variable.
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
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
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.
We want to design a type capable of representing the Time of day.
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.
Time is to be used in a computer
program we need to be able to declare, define and create Time
objects.
Time object - we specify this as addition.
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.
/**
* 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;
}
}
setHour(),
setMin(), setSec() ensure that user programs cannot put a
Time1 object into an inconsistent state.
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());
}
}
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" );
}
}
/**
* 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);
}
}
System.out.println("t1= " t1);+
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.
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
}
private methods toSecs(), fromSecs.
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.
addTo, subFrom are asymmetric - there is a definite
`receiver' of the message, and the `other' object.
int s= toSecs() + other.toSecs();, note that toSecs()
refers to the object itself.
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();
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.
/**
* 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);
}
}
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
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
}
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);
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.
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.
/**
* 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);
}
}
$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
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;
}
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);
}
}
Classes may be composed of objects from other classes - see [Deitel and Deitel, 1999, section 8.11, p. 355]; see also section 5.9.
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.
A data type, or simply type, is made up of two constituents:
boolean: {false, true}, byte: {-128..127}
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?
float is
represented as four contiguous bytes. This promotes:
float numbers rather than having to think about numbers one moment
and having to switch to bits and bytes the next.
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.
In order to contrast values with variables we note the following:
2 describes the
general concept of twoness - of all pairs of anything.
i = 3; is O.K., but
not 3 = 4;!
5 will change depending on when, or how it is
referenced in an expression; int i, j; i = 5; will always assign
5 to i; however, i = j; depends on the history of
j.
In traditional imperative programming languages, a variable has associated with it:
a in int a;.
int.
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.
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.
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.
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
in it becomes garbage.
+---------+ +---------+
| c1 +--------+ | 456 |
+---------+ | +---------+
|
+---------+ +----->+---------+
| c2 +-------------->| 123 |
+---------+ +---------+
From now on, anything you do to c2 you (effectively) do to c1.
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*/
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!
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
above, the garbage collector can detect this, and the
memory that was used can be recycled/deallocated automatically.
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; |
}
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;
}
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.
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.
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.
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.
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.
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:
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;
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.
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.
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());
clone().
this. In Cell.get() show how to
make explicit use of this.
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.
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.
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.