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