07. Creating abstraction

advertisement
07 ADTs
Chapter 7
Abstract Data Types and
Classes
This chapter describes a fundamental concept of modern programming: that
of abstract data type. Program development with abstract data types enables
us to express problem solutions using concepts – that is, abstractions – that
are appropriate to the problem. Constructing data types appropriate to a
problem isolates the principal task of expressing the algorithms of an
application from the details of data representation and manipulation. Abstract
data types provide the foundation for object-oriented programming. We will
be revisiting some Java concepts we have seen already: classes, objects,
and methods, but this time with emphasis on abstraction.
1 Introduction
The principal data types of a language such as Java commonly include integer and real
numbers, booleans, characters and strings, and arrays of fixed size. Such a listing of types
appears rich until one reflects on what is not included: fractions, imaginary numbers,
complex numbers, polynomials, sequences, queues, stacks, trees, and graphs, to name just a
few. Thus, while Java's collection of primitive types is substantial, it is by no means
complete. Other languages offer a few more types including subrange (restricting the values
of an integer or character to some smaller range), enumerated types (where the type is
defined by simply listing all the possible values), and sets. But we can’t fix the problem by
adding more types – any collection will be incomplete.
The lack of an appropriate data type can cause substantial problems to arise even in
conceptually simple situations. Consider the problem of calculating the due date for a book
that is borrowed from a library, and of calculating the fine for a book that was returned after
the due date. If the lending period is two weeks, then each time a book is loaned, the due
date is the date of the loan plus fourteen days. If a data type called “date” were available
and the value of the variable currentDate was today’s date, then the due date might
reasonably be expressed as the value of the expression (currentDate + 14). Similarly,
when an overdue book was returned, we could calculate the number of days overdue as the
value of the expression (currentDate - dueDate). While adding 14 to a date and finding
the difference between two dates are conceptually simple tasks, there are ample
opportunities for error: Does adding 14 cause the month or year to change? Is it a leap year?
If the programmer is provided with a (correctly implemented) “date” data type; these
 2001Donald F. Stanat & Stephen F. Weiss
Chapter 7
Abstract Data Types and Classes
Page 2
questions do not arise because they are handled by the implementation. Clearly, using a
data type appropriate to a problem can make the expression of algorithms both more
transparent and less error-prone.
Faced with the problem of designing and implementing algorithms, a naive programmer is
tempted to fall into the trap of not keeping separate the problems of how data are
represented and manipulated from the problems of expressing the algorithms of interest. In
contrast, the sophisticated programmer recognizes that the choice of data representation,
and therefore the implementation of the operations, can be made independently, and can be
solved separately. Divorcing the problems of implementing a data type from those of
expressing the desired algorithms is one way that programmers enforce a separation of
concerns. This separation not only supports unencumbered thinking (e.g., subtraction of
one date from another can be done without concern for whether a leap year is involved), but
also simplifies changes in either the essential algorithms of the application or in the way
that data are represented and manipulated.
This leads to an approach to problem solving based on abstract data types: the solution to a
problem is expressed using data types appropriate to the problem. For example, the
algorithms of a chess-playing program will be expressed in terms of the current board
configuration and movements of the pieces. A particular board configuration will be a value
of the data type. The operations on the data type will include legitimate moves of a piece
from one square to another and capture of the opponent’s pieces as well as less obvious
operations such as tests to determine the condition of the current board configuration (e.g.,
Is the king is under attack?). The availability of such a data type makes it possible to
implement algorithms related to chess in a way familiar to and comfortable for chess
players. There is no reference to the way a chess board is represented or how the
representation changes when a piece is moved.
Entirely separately, a programmer is faced with the problem of implementing the data types
with which the strategies are expressed. For chess, this requires first choosing a
representation for legitimate board configurations and then writing algorithms to interpret
moves of chess pieces as changes in the representation and tests of board configurations
(such as “Is the king in check?”) as tests of properties of the representation.
The problems in both domains – that of playing chess, and that of representing the play –
can be difficult, but divorcing the difficulties of the domains from one another provides
tremendous advantages both for writing and maintaining software.
2 What is an Abstract Data Type?
The concept of abstract data type is not one that can be rigorously defined in a way that is
satisfactory for every need, so our definition is necessarily informal.
Definition: An abstract data type (ADT) is a set of values, some of which may be
distinguished as constants, together with a collection of operations involving members of
the set.
Printed February 12, 2016
at 4:14 AM
2
Chapter 7
Abstract Data Types and Classes
Page 3
This suffices as a definition of ADT. But if the ADT is to be useful, we must be able to
create instances of the ADT and apply the operations to those instances.
For example, the set of values could be the set of all arrangements of chess pieces on a
chessboard, together with a flag or switch that indicates whether it is white or black's
move1. The set of operations on those values must include the set of all legal moves, and
will almost certainly include others as well, such as tests for whether the game is over.
There is also a distinguished value, or constant, which is the initial configuration of a game.
While a chessboard provides a convenient illustration of an ADT, there is really no need to
step outside Java to illustrate the concept2, since it is simply a generalization of our view of
built-in data types. Thus, we'll next describe some familiar systems as ADTs.
2.1 The ADTs Integer and Java int
We can define an ADT Integer as an abstract data type with operations of + and ==. We
define the set of values as the infinite set N == {0,1,-1,2,-2,...} and say that elements of this
set are values of the type Integer. We've chosen (somewhat arbitrarily) to include only two
operations, + and ==. To specify the types of their arguments and the type of their results,
we use the standard mathematical notation for functions. The notation
f : Set1  Set2  Set3
specifies that f is a function that takes two arguments, the first is an element of Set1, the
second from Set2, and the operation produces a value that is in Set3. The notation 
specifies that the arguments are members of the Cartesian product of the values of Set1 and
Set2; thus, the arguments are ordered pairs (x,y), where x is in Set1 and y is in Set2. The set
to the left of the  specifies the set of possible arguments of the function (called the
domain of the function) and the set to the right of the  specifies the set of possible values,
(called the range or codomain) of the function. Thus, the specification of + and ==
includes:
+ : N N  N
== : N N  {true, false}
For completeness, an ADT specification must include a description of how the result of
operations are calculated for each operation, but we'll forgo that description and simply
state that we want + to denote addition and == to denote equality. Finally, we'll choose the
constant 0 as a distinguished value, since it is the identity element for the operation +. We
might denote the resulting ADT Integer by <N, {+,==}, {0}>.
1To be complete, we must also keep track of whether each side has castled, and the number of moves since a
capture in order to determine whether a game ends in a draw.
2You may be encouraged to know that not only do you speak prose, you also use abstract data types — only
the concept is new.
Printed February 12, 2016
at 4:14 AM
3
Chapter 7
Abstract Data Types and Classes
Page 4
Once we define the ADT Integer, we could build on its definition by adding additional
arithmetic operations, such as subtraction, multiplication and division, and additional
comparison operations such as less than, and not equals. Many of these have corresponding
Java operations, but we are free to define others. For example, the operation isPositive
could be defined using the (previously defined) operation > as follows:
isPositive : N  {true, false}
isPositive (x) == true if x > 0
isPositive (x) == false if !(x > 0)
or, to put it more succinctly,
isPositive (x) == x > 03
The data type called int in Java, of course, is not the same as the ADT Integer defined
above because Java integers cannot have a magnitude larger than a specified upper bound.
But we can think of int as an ADT with a set of values {0,1,-1,2,-2,...M,-M}, where M is
the largest possible magnitude, and it includes all the operations of Java that use integers.4
The value M is appropriate for inclusion as a constant, along with other special values such
as 0 and 1.
We create instances of the ADT int with the standard declaration
int x;
int y;
and operate on these instances with the familiar arithmetic operations.
x = y + 100;
Thus, when we program with Java integers, we are using an abstract data type implied by
the keyword int. The ADT has not been carefully defined for us, because it is assumed that
our intuitive notion of "integer" will suffice, and a careful definition would be difficult both
to write and to read.
But even if int is a data type, why is it an 'abstract' data type? Precisely because we don't
have to worry about the details. For example,
1. we don't know how integers are represented inside the machine – that is, we don't
know how values of the ADT are represented.
3 Zero is considered neither positive nor negative.
4Note that to specify the ADT completely, we must give both a syntax for expressions denoting values of the
ADT and a semantics that specifies how these expressions can be evaluated. If this semantics leaves
unspecified the value of an expression (such as M+M), then the value of that expression is undefined.
Expressions whose values are not defined can be handled in any of several ways, including adding a
distinguished error value.
Printed February 12, 2016
at 4:14 AM
4
Chapter 7
Abstract Data Types and Classes
Page 5
2. we don't know how the integer representations are manipulated to obtain a result –
that is, we don't know how the operations of the ADT are implemented.
Not needing to know these things relieves us from a host of mundane but crucial details —
we are free to think, for example, in terms of "8*5 = 40" rather than how the patterns of
0000000000001000 and 0000000000000101 are manipulated by the hardware to produce
0000000000101000. This ability to think and program in terms of familiar concepts such as
the integers rather than having to manipulate binary representations of data is supported by
any language for its built-in data types such as int.
Thus each of the built-in data types of any programming language can be viewed as based
on an implicit ADT. The task of abstraction was handled by the language implementers so
that we can think and express algorithms in terms of convenient abstractions – the integers,
real numbers, boolean values, and so forth. The details of the implementation are of little
interest to us, except when we must deal with the consequences of those implementations,
such as that the magnitude of any int in Java is no greater than 231-1.
The ADTs Integer and int illustrate what is needed to specify an ADT: a set of values, a
collection of operations, and a set of constants plus a way to create instances of the ADT
and apply the operations to those instances. For convenience, an ADT generally also has a
name. But the ADTs that we are interested in implementing include things that we may not
initially think of as data structures: graphs, calendars, books and encyclopedias – any
collection of related data that we wish to treat with a computer program. Languages differ
greatly in the support they provide for creating, implementing and manipulating data types
that are not built-in. Java's object-oriented facilities provide an introduction to the kinds of
tools that are becoming widespread in contemporary languages.
The ADTs we'll discuss will be modest, but they will serve to illustrate the ideas. To begin,
we will use Java's built-in data types – that is, the built-in ADTs – to implement our own
ADTs. But each ADT we have defined can itself be used to define new ADTs. Thus each
ADT can be used both as a tool for solving a problem and as a tool for building new tools.
3 How is an ADT specified?
Before we can implement an ADT, we must characterize what is to be implemented. The
only way to specify an abstract structure unequivocally is with mathematics5. Considerable
work has gone into investigating ways of specifying ADTs, and several approaches have
been developed and explored. None has become a clear favorite, however, and none have
made the task simple and intuitive, even for relatively simple and intuitive data structures.
Often, the effort required to provide a precise specification of an ADT is justified and
appropriate, but a discussion of the mathematics needed for that task is beyond the scope of
this text. Our strategy in this chapter will be to present in each case a careful but informal
description of an abstract data type and then develop an implementation based on that
description.
5This is, of course, a truism, for as soon as we have devised a way to express something unequivocally, the
mode of expression becomes 'mathematics'.
Printed February 12, 2016
at 4:14 AM
5
Chapter 7
Abstract Data Types and Classes
Page 6
4 The concept of class and objects
A class is a way of defining an ADT in Java (as well as in other object-oriented languages).
In informal conversation, ‘ADT’ and ‘class’ are sometimes used interchangeably. A class
contains a template for the memory required for the values of the ADT as well as the
methods that operate on those values. We create instances, or values, of the ADT, called
objects, in much the same way that we create instances of primitive types (called variables)
using the declaration statement. In object-oriented jargon, the method calls to an object are
often viewed as messages sent to that object. A function call is a request for information:
the object is being asked to report something about itself through the returned value. A
procedure call is a request for the object to do something, often to alter its state. Hence we
can think of a function call as a message of the form “tell me something about yourself”
and a procedure call as a message “do something to yourself.” Certain functions and
procedures don’t fit this message model too well; we’ll discuss such methods in a later
chapter.
The use of a class is a particularly attractive way to implement an ADT. First, the
definition of the class is generally in a file that is physically separate from the program that
uses it. The separate files that contain the class and the main program are brought together
only when the program is compiled and run. Second, the class mechanism supports good
programming practice by preventing any unintended access to the data structure. Objects
can be accessed only through the interface (that is, the methods) that are defined for that
purpose in the class definition. The class facility also provides a mechanism to specify
some methods to be private, that is, usable only from inside the class and unavailable to the
outside world. These private methods are often those that rely on details of the class
implementation.
5 The ADT Pair
In this section we introduce an ADT that we call Pair that will be used to represent what is
often called “ordered pairs”. An ordered pair consists of two integers, distinguished by
calling one the “first” and the other the “second”. This very simple data type of integer
ordered pairs is not built into Java, but could be easily defined as a class.
5.1 An implementation of the ADT Pair as a class
It will become clear that a class definition is written in terms of a single object, or instance
of the type; in this case, the definition is written in terms of the component values of a
single pair. Thus, variables to hold the component values are declared, and methods are
written to manipulate those variables. Shown below is the class definition for the ADT
Pair. We have included two constructors: one that has no parameters and sets both
elements of the pair to zero, and a second with two parameters that sets the elements of the
pair to the specified parameter values. The operations setFirst and setSecond set the value
of the pair; getFirst and getSecond retrieve the values, and toString returns a string version
of the pair, enclosed in parentheses with the values separated by a comma. The pre and
postconditions for these methods are so simple that we have included them in the code
rather than using separate specification and implementation methods.
Printed February 12, 2016
at 4:14 AM
6
Chapter 7
Abstract Data Types and Classes
public class Pair
{
private int first;
private int second;
Page 7
// First value of the pair.
// Second value of the pair.
public Pair() // No parameter constructor.
{
Assert.pre(true,"");
first = 0;
second = 0;
Assert.post(first == 0 && second == 0,"Constructor error");
}
public Pair(int f, int s) // Two parameter constructor.
{
Assert.pre(true,"");
first = f;
second = s;
Assert.post(first == f && second == s,"Constructor error");
}
// Methods implementing operations
public void setFirst(int f) // Set value of first element.
{
Assert.pre(true,"");
first = f;
Assert.post(first == f,"setFirst error");
}
public void setSecond(int s) // Set value of second element.
{
Assert.pre(true,"");
second = s;
Assert.post(second == s,"setSecond error");
}
public int getFirst() // Return the value of the first element.
{
Assert.pre(true,"");
return first;
// Post: returned value is first.
}
public int getSecond() // Return the value of the second element.
{
Assert.pre(true,"");
return second;
// Post: returned value is second.
}
public String toString()
// Return an appropriately formatted
// String version of the pair.
{
Assert.pre(true,"");
return "(" + first + ", " + second + ")";
// Post: returned value is String version of pair.
}
}
// End Pair class
Printed February 12, 2016
at 4:14 AM
7
Chapter 7
Abstract Data Types and Classes
Page 8
Definition of the Class for the ADT Pair.
Note that the two integer variables, first and second, that hold the value of the pair are
declared outside all the methods and are manipulated as global variables by the methods.
Normally, we frown on communicating information to and from methods via global
variables, but a class definition is constructed solely to manipulate the variables of a single
value of an ADT, and declaring these variables to be global within the class definition
simplifies both the class definition and the use of the resulting ADT.
5.2 Using the class implementation of the ADT Pair
Now that we’ve created the class for the ADT Pair, we can use it as a pre-defined data type
in application programs. In some programming languages, the syntax for creating an object
is exactly the same as for creating a variable. Unfortunately, that is not quite the case in
Java. The statement
Pair p;
creates a reference variable to Pair, but does not actually create the Pair object. An object is
created with the new statement which creates the object, calls the appropriate constructor,
and returns a reference value pointing to the newly created object. Putting the pieces
together:
Pair p = new Pair(5,10);
creates a reference p, creates a new Pair object with values 5 and 10, and sets p to reference
that newly created object.
Once the object has been created, we can call the methods using the Pair reference. For
example, the following code sets the first element of the Pair referenced by p to 20, sets the
second element to 40, displays the Pair, and then calculates and displays the sum of the
values. Recall that when an object reference appears by itself in an output statement, the
method toString() is implicitly called.
p.setFirst(20);
p.setSecond(40);
System.out.println(p);
System.out.println("The sum of the elements is "
+(p.getFirst() + p.getSecond()));
Note that no part of the value, or state, of an object is accessible to direct manipulation by
program statements; for example, we must use
p.setFirst(3);
rather than
p.first = 3;
The latter produces a syntax error.
Printed February 12, 2016
at 4:14 AM
8
Chapter 7
Abstract Data Types and Classes
Page 9
6 A second example: rationals
This section presents a second example of a class and motivates the notion of “helper”
methods. The ADT we’ve chosen is the rational number. A rational number is one that can
be represented as the quotient of two integers: a/b. Thus, for example, all integers are
rational, as are fractions such as 1/2, 2/3, and -7/8, and mixed numbers such as 4 5/8. But
not all numbers are rational. For example,  and e (2.7183.., the base of the natural
logarithms) cannot be represented as the quotient of integers are hence irrational.
6.1 The class for rationals
A basic appropriate set of operations for this class is:
Procedures
setRational
Set the value of a rational number.
Functions
getNum
Return the (integer) value of the numerator.
getDen
Return the (integer) value of the denominator.
getValue
Return the (real) value of the rational number.
toString
Return a String representation of the rational.
The class specification and implementation are shown below. The implementation is based
on representing the rational as a pair of integers (x,y). But the implementor is faced with a
choice because rational numbers do not have a unique representation; for example, the
fractions 1/2, 2/4 and 3/6 are all equal. This could have some ominous implications, since
some representations will cause integer overflow while other representations of the same
fraction may not. For this reason and others, we have chosen to store each rational number
in its reduced form, which is defined to be the representation in which the numerator and
denominator have no positive integer divisors in common other than 1. Thus, 1/2 is
reduced, but 2/4 and 3/6 are not, so the fraction specified as 3/6 will be stored as the integer
pair (1,2) rather than (3,6). This makes clear the need for an additional (private) method
reduce, which reduces the rational by dividing both numerator and denominator by their
greatest common divisor (gcd). The reduce method makes the representation of each
rational number unique by setting the signs of the two integers of the representation as
follows: if the signs of the numerator and denominator are the same, reduce will set both
signs positive. If the signs differ, then the numerator will be set negative and the
denominator positive. Hence 2/4, -3/-6, and 10/20 all reduce to 1/2, while -2/4, 4/-8, and
−10/20 all reduce to -1/2. Each rational number will be reduced every time it gets set or
changed. The reduce and gcd methods are private and can be called only from inside the
class.
We have chosen not to include constructors with this class to illustrate the notion of an
object with an undefined value. We could have written constructors that allowed the
rational to be initialized to some specified value or, if no initial value was specified, to
Printed February 12, 2016
at 4:14 AM
9
Chapter 7
Abstract Data Types and Classes
Page 10
something appropriate like 0/1. But instead, a Rational object is undefined when created
and remains undefined until it has had its value set by a call to setRational. A boolean
variable defined is initialized to false and is set to true in the setRational method.
Preconditions for the reader methods (getNum, getDen, and getValue) require the object
value to be defined. A method, isDefined, indicates whether or not the rational has been
defined. Also, toString returns the value "Undefined" if the object has not had a value
set. While implicit initialization by constructor may be convenient, it can sometimes mask
errors.
With the Rational class, we can see clearly the virtue of defining the implementation
variables as private and allowing access to the objects only through the public methods.
The method setRational guarantees that the Rational object remains in a consistent state:
that the rational value is stored in reduced form and that the defined variable is set properly.
If the outside world had access to num, den, and defined, then we could easily put the
Rational object into an inconsistent state in which, for example, the values were not
reduced or where defined was set true, but the values of the numerator or denominator
were not set.
Printed February 12, 2016
at 4:14 AM
10
Chapter 7
Abstract Data Types and Classes
Page 11
// Class for the rational number ADT.
class Rational
/* Rational numbers are values of the form (x,y), where x
and y are integers and y is not 0. x is called the "numerator"
and y is the "denominator". Rational numbers are stored in
a reduced canonical form such that the greatest common divisor
of x and y is 1, and y is always positive.
*/
{
private int num; // Numerator
private int den; // Denominator
private boolean defined = false; // Has the rational
// been defined yet?
// Set the rational to x/y.
public void setRational(int x, int y)
{
Assert.pre(y!=0,"denominator cannot be zero");
setRationalM(x,y)
Assert.post(getValue()==(double(x)/y && den>0,
"rational set incorrectly");
}
private void setRationalM(int x, int y)
{
num = x;
den = y;
reduce();
defined = true;
}
// Return the numerator of the rational number.
public int getNum()
{
Assert.pre(defined,"rational is undefined");
int res = getNumM();
Assert.post(res == num,"incorect numerator returned");
return res;
}
private int getnumM()
{
return num;
}
// Return the denominator of the rational number.
public int getDen()
{
Assert.pre(defined,"rational is undefined");
int res = getDenM();
Assert.post(res == num,"incorect denominator returned");
return res;
}
private int getdenM()
{
return den;
}
public double getValue()
{
Assert.pre(defined,"rational is undefined");
double res = getValueM();
Printed February 12, 2016
at 4:14 AM
11
Chapter 7
Abstract Data Types and Classes
Page 12
Assert.post(res == (double)num/den,
"value computed incorectly");
return res;
}
private double getValueM()
{
return (double)num/den;
}
// Return gcd of i and j.
private int gcd(int i, int j)
{
Assert.pre(i!=0 || j!=0,
"at least one argument ot gcd must be nonzero");
int res = gcd(i,j);
Assert.post(i%res == 0 && j%res == 0,
// and res is the largest value for which this is true
"gcd incorrect");
return res;
}
private int gcdM(int i,int j)
{
int big=Math.abs(i);
int small=Math.abs(j);
int remainder=big % small;
while (remainder !=0)
{
big=small;
small=remainder;
remainder=big%small;
}
return small;
}
//Reduce rational so that:
//
denominator is > 0 and
//
gcd of numerator and denominator is 1.
private void reduce()
{
Asset.pre(defined,"rational is undefined");
int old_num=num;
int old_den=den;
reduceM();
Assert.post(den>0 && gcd(Math.abs(num),den)==1 &&
getValue()==(double)old_num/old_den,
"reduce incorrect");
}
private void reduceM()
{
if (den<0) // Make sure denominator is positive.
{
den=Math.abs(den);
num=-num;
}
// Divide both num and den by gcd.
final int g=gcd(num,den);
num=num/g;
den=den/g;
}
Printed February 12, 2016
at 4:14 AM
12
Chapter 7
Abstract Data Types and Classes
Page 13
// Has the rational had its value set yet?
public boolean isDefined()
{
Assert.pre(true,"");
// Post: returned value == defined.
return defined;
}
// Return String version of rational.
public String toString()
{
if (!defined) return "Undefined";
else
if (num==0)
return "0"; // Fraction is zero.
else
{
if (Math.abs(num)<den) // Fraction is between -1..1.
return Integer.toString(num)+
"/"+Integer.toString(den);
else
{
// Fraction is >= 1 or <= -1. Represent as a
// mixed number.
String intPart=Integer.toString(num/den);
String fractPart="";
if (num%den !=0) // Fractional part is non-zero.
{
fractPart=" "+
Integer.toString(Math.abs(num)%den)
+"/"+Integer.toString(den);
}
return intPart+fractPart;
}
} // end of toString
} // end of Rational class
6.2 A helper class for the rationals
We noted earlier that the jargon of object-oriented programming often refers to the
operations on objects as “messages” – the functions are messages of the form “tell me
something about yourself;” the procedures are of the form “do something to yourself.” The
operations for the rational numbers provided so far fit the “message” model well, but there
are other operations we naturally want to perform on Rationals that don’t fit this model.
For example, we might want to add two rational numbers. This could be done as a method
in the Rational class that takes a Rational object as its parameter, adds it to the object's
value, and returns a new Rational object with the value of the sum. But that is an awkward
implementation and doesn’t fit the message model, since it asks neither for an object to
change its state nor for the object to report something about its state. There's also an
Printed February 12, 2016
at 4:14 AM
13
Chapter 7
Abstract Data Types and Classes
Page 14
unnatural asymmetry to what is a symmetric operation. It seems much more natural to add
r1 to r2 by calling addRational(r1,r2) than by calling r1.addRational(r2).
Alternatively, we could coerce the operations to fit the message form. For example,
r1.addRational(r2) could instruct the rational referenced by r1 to add the value of the
rational referenced by r2 to itself, essentially implementing the assignment
r1
= r1 + r2;. But this seems contrived and unnatural. Operations such as addition are best
implemented in a separate class outside the Rational class so that the operations are not
associated with a specific object. We call such operations helper methods, and a class that
contains their definitions a helper class. A helper class is associated with a host class, and
contains definitions of methods that are useful to the host, but not appropriate to associate
with the individual objects of the host class. As with other class definitions, helper class
methods can be implemented as specification–implementation pairs so that the syntax and
semantics of the operations can be made available without revealing the implementation,
although these operations are so simple that we have chosen not to separate them. Below
we show a helper class containing functions that perform addition, multiplication and
comparison operations on Rationals. The name of the class is RationalOps. All the
methods are defined as static so that a RationalOps object need not be created in order to
use the methods. Other operations such as subtraction, division, and other comparison
operations such as less than can be similarly implemented in the helper class.
Printed February 12, 2016
at 4:14 AM
14
Chapter 7
Abstract Data Types and Classes
Page 15
// Helper class of Rational operations.
public class RationalOps
{
// Add two Rationals and return Rational object that contains
// their sum.
public static Rational addRational(Rational r1, Rational r2)
{
Assert.pre(r1.isDefined() && r2.isDefined(),
"operands undefined");
// Post: returned value is the sum of r1 and r2.
Rational res=new Rational();
res.setRational(
r1.getNum()*r2.getDen() + r2.getNum()*r1.getDen(),
r1.getDen()*r2.getDen());
return res;
}
// Multiply two Rationals and return Rational object that
// contains their product.
public static Rational mpyRational(Rational r1, Rational r2)
{
Assert.pre(r1.isDefined() && r2.isDefined(),
"operands undefined");
// Post: returned value is the product of r1 and r2.
Rational res=new Rational();
res.setRational(
r1.getNum()*r2.getNum(), r1.getDen()*r2.getDen());
return res;
}
// Are Rational r1 and r2 equal?
public static boolean equals(Rational r1, Rational r2)
{
return (r1.getNum()==r2.getNum() &&
r1.getDen()==r2.getDen());
}
}
Printed February 12, 2016
at 4:14 AM
15
Chapter 7
Abstract Data Types and Classes
Page 16
7 Keeping track of objects
How do we keep track of multiple objects? If we have only a few objects, we can declare
individual references and refer to each object through these references. This is what we
have done in the examples. But if we have a large number of objects, and especially if the
number of objects is not known in advance, we cannot have individual names for each. We
have several alternatives. We can create an aggregate data structure such as an array or
vector of references and access the objects through this data structure. Alternatively, we
could allow each object to refer to one or a small number of other objects. This linked or
chained structure is the subject of a later chapter.
An interesting question is what happens to an object if it is no longer referenced. For
example, in the following code, r1 at first references a Rational object with the value ½.
Then r1 is reset to point to another Rational object with the value 7/8. What happens to the
½ object?
Rational r1 = new Rational();
r1.setRational(1,2);
r1 = new Rational();
r1.setRational(7,8);
The answer is that the ½ object is still around, but since no reference points to it, it is
unreachable – essentially lost in memory. The official jargon word for un-referenced
objects is garbage. In some programming languages, the programmer is responsible for
keeping track of garbage and explicitly de-allocating unused objects, a process called
garbage collection. But Java does automatic garbage collection, periodically searching for
un-referenced objects and adding them to the pool of memory available for re-allocation.
8 Summary
This chapter has introduced the notion of abstract data type (ADT), and a set of language
facilities that support programming with ADTs. The chief characteristic of programming
with abstract data types is the separation of concerns of data representation and the
representation-dependent details of data manipulation from the concerns of programming
algorithms in an application area. Maintaining the separation of these two concerns brings a
number of benefits:

Applications programming is more natural because the data types can be chosen
appropriate to the application and algorithms. Algorithms can be changed or modified
without concern for how the change is reflected in the data structures.

Data representation and implementation of basic operations on the data can be changed
without affecting the correctness of programs in the application area.
Java introduced a number of language facilities to support programming with abstract data
types. We have described these facilities in a limited context.
Printed February 12, 2016
at 4:14 AM
16
Chapter 7
Abstract Data Types and Classes
Page 17

The class facility permits the definition of ADTs, including operations on the ADT
values, to be defined as a unit.

Classes are instantiated with objects using dynamic storage allocation. An object
represents a value of an ADT. Access to individual objects is by means of Java
references.

Operations on objects that are naturally associated with a single object should be
defined as part of the class definition. These operations are either of the form “Tell me
about yourself”, implemented as functions, or “Do something to yourself”, implemented
as procedures.

Operations that are not naturally viewed as “messages” are properly defined in a helper
module associated with a class. The helper module provides support for operations that
deal with two or more objects, such as arithmetic or comparison operations.
The notions of classes and objects introduced in this chapter are fundamental to object
oriented programming.
Printed February 12, 2016
at 4:14 AM
17
Chapter 7
Abstract Data Types and Classes
Page 18
Chapter 7: Abstract Data Types and Classes
1
INTRODUCTION ................................................................................................................................... 1
2
WHAT IS AN ABSTRACT DATA TYPE? .......................................................................................... 2
2.1
THE ADTS INTEGER AND JAVA INT ................................................................................................. 3
3
HOW IS AN ADT SPECIFIED? ........................................................................................................... 5
4
THE CONCEPT OF CLASS AND OBJECTS ..................................................................................... 6
5
THE ADT PAIR ...................................................................................................................................... 6
5.1
5.2
6
6.1
6.2
AN IMPLEMENTATION OF THE ADT PAIR AS A CLASS ................................................................... 6
USING THE CLASS IMPLEMENTATION OF THE ADT PAIR .............................................................. 8
A SECOND EXAMPLE: RATIONALS ............................................................................................... 9
THE CLASS FOR RATIONALS.............................................................................................................. 9
A HELPER CLASS FOR THE RATIONALS .......................................................................................... 13
7
KEEPING TRACK OF OBJECTS ..................................................................................................... 16
8
SUMMARY ........................................................................................................................................... 16
Printed February 12, 2016
at 4:14 AM
18
Download