Abstract Data Types and Java Classes

.. .. .. .. .. New Mexico State University Department of Computer Science CS 272 – Fall 2004 . . Abstract Data Types and Java Classes . . . . . ....
Author: Asher Bell
2 downloads 0 Views 283KB Size
.. .. .. .. ..

New Mexico State University Department of Computer Science

CS 272 – Fall 2004

.

.

Abstract Data Types and Java Classes . . . . . . . Enrico Pontelli and Karen Villaverde

.

.. .. .. .. ..

Abstract Data Types Data Types A Data Type is typically defined as •

a Domain – i.e., a collection of values



a set of operations that can be applied on the values in the domain

Both these components together characterize a data type. For example, if we consider the data type byte we have that •

the domain contains all the integer number between -256 and +255



the set of operations includes the operations: addition, multiplication, subtraction, integer division, and modulo. In addition, we have o

operations that are used to compare elements in the domain (e.g., ==, !=, >) and return a Boolean value

Observe that in a data type, the only way to access and use values belonging to the domain is to make use of the operations provided by the data type. The user of the data type does not have any other way of modifying these data. Furthermore, the data type does not tell us anything about how the data are internally represented (e.g., we do not see what bit pattern is used to store a byte object) and we do not see what kind of algorithm is employed to implement each operation. Thus, when we are dealing with data types, the data type tell us what the values are and what the various operations do, but it does not tell us anything about how all these things are done. This is an example of what is commonly known as Abstraction. Creating an abstraction means providing clear information about how to use a certain entity, allowing others to use it without the need of knowing anything about how the entity is implemented. We have seen already many examples of Abstraction. Each time we create a method, we perform what is known as Procedural Abstraction. The user of a method needs to know only what the method does and how to call it, without the need to know what statements are present inside the method itself. A Data Type is another example of abstraction – we are provided a domain and a set of operations to manipulate the values in the domain, but we do not need to know anything about how the data type has been implemented. The notion of abstraction is typically tied also to the notion of Information Hiding: the developer of an abstraction decided what components of his/her implementation should be

2

visible to others and which components should be kept hidden. When we write a method, automatically we make available to others information about how to call the method (its name, the description of the parameters, the output type) but nothing else – thus, the body of the method is effectively hidden to the users of the method (e.g., we cannot access or affect the variables that are present inside the method). The notion of information hiding is a fundamental principle in software development. By carefully choosing which parts of an implementation should be visible to others and which should be kept hidden, we promote a separation between the creation of the implementation and the use of the implementation. Creating this separation is vital to ensure that complex software systems can be built. Consider what would happen if information hiding was not present, for example in the case of implementing a data type •

if we let people see how the data type is implemented (e.g,, people can see how we are representing the data in memory, and directly access our memory representation), then they will be able to write code that direct access the internal representation of our data, bypassing the operations provided by the data type. As a result, the code written by these users will be dependent on that specific implementation of the data type. If one day we decide to change how the data type is represented (e.g., we discover a more efficient internal representation), even though we support the same set of operations as part of the data type, all the programs written by the users will not work any longer (since they relied on a different internal representation). This prevents the data type to independently evolve.



if we make all the details of the implementation of a data type visible, we might make very difficult for users to understand how to use it – as they will not see a clear separation between the implementation of the data type and the description of how to use it (which is really the only thing they are interested in).

Thus, what we would really like is the ability to implement a data type, but allow the user to learn only how to use the data type, without being able to access any of the implementation details. If a programming language supports this type of construction, then we say that the language provides an Encapsulation mechanism. This idea is also intuitively illustrated in the figure below; the program cannot see the implementation of the method S, but it only knows how it should be used – it has only a well defined little window to access the method. The method’s specification (i.e., its header) becomes a window to access the method, as well as a “contract”: if you use the method this way, this is exactly what it will do for you.

3

.. .. .. .. ..

Program that uses method S

Request operation Result of operation

Implementation of method S

The “contract” that describes how to use an entity is frequently called the Interface. For a method, the header of the method is its interface.

Abstract Data Types An Abstract Data Type (ADT) is a data type that has been created by a programmer – i.e., it is not built-in in the programming language. As any other data types, an ADT is composed of a domain (the set of values belonging to the data type) and a collection of operations to manipulate such values. The only difference is that such data type will be constructed by the programmer. When we build an ADT we really want to apply the principles of encapsulation and information hiding mentioned earlier. This means that, once we have finished building the data type, we wish others to use the data type exclusively through the operations we provide, and in no other way. In particular, to protect our implementation and guarantee the ability to evolve software, we want to ensure that the implementation of the ADT is hidden from other users. Let us make an example. We would like to create an ADT that represents fractions; the domain should be the set of all the possible fractions (since these are the values we are interested in manipulating), and the operations we would like to perform are: •

check if two fractions are equal



read the numerator and denominator of the fraction



simplify a fraction



add and multiply fractions

The result should be a new data type (e.g., called Fraction), so that we can declare variables of this type and use them in our program.

4

ADT Specification The first step in the creation of an ADT is the development of its Specification. The specification of an ADT is a precise description (in English) of the ADT. The description should clearly identify what is the domain of the ADT and what are the operations associated to the data type. For each operation, we need to clearly describe what the operation does, what kind of inputs it expects, what kind of result it will produce. Observe that the specification does not say anything about how we are gong to implement the ADT. It just describes what the ADT does and how it can be used. The people that are interested in using the ADT in their programs (e.g., they need to operate on fractions) need to only read the specification in order to proceed with their programs (THEY DO NOT NEED TO KNOW ANYTHING MORE!). Thus, one can think of the specification of the ADT as the interface to the ADT.

Domain of the ADT We will try to follow a standard pattern in developing the specification of the ADT. The first component of the specification is the description of its domain. The domain is the set of values that we can store in objects of this type. For example: Fraction ADT DOMAIN: the set of all the possible fractions As another example, if we are building an ADT to represent points in the 2-dimensional space, then Point ADT DOMAIN: the set of all the points in the two-dimensional space Note that in both cases we are careful about providing an intuitive description of the set of values, without committing to any specific internal representation (e.g., we do not say how the points are going to be represented internally – they could be represented as Cartesian coordinates or as polar coordinates…).

Operations of the ADT The second part of the specification should describe the operations that belong to the ADT. This is a very delicate part in the development of the specification. We need to ensure that •

We provide all the operations that are needed to use the ADT in a meaningful way (Completeness). Remember that the operations will be the only way to operate on the ADT, thus if we forget an important operation then the users will not be able to use the ADT in their programs.



We provide only the necessary operations; if we provide too many unnecessary operations, we will end up making the ADT very complicated and hard to use.

For the sake of simplicity, we will typically distinguish four classes of operations

5

.. .. .. .. .. 1.

Constructors: these operations are used whenever we want to create a new object of that particular type. Constructors are required as the creation of a new object belonging to an ADT requires a sequence of steps

2.

Destructors: these are operations that are going to be executed when an object is removed from the system (because it is not used any longer)

3.

Inspectors: these are operations that allows one to inspect the content of an object or test properties of the object (without modifying it)

4.

Modifiers: these are operations that either modify an object or generate new objects.

Let us illustrate this to the case of the Fraction ADT: •

Constructor: we can assume one constructor that generates a brand new object of type fraction; the operation requires as input the numerator and denominator of the fraction we wish to create. The result should be the new fraction created. We will denote this as follows: Fraction createFraction (int num, int den) // creates a new fraction, having num as numerator and den as denominator // the result is an object of type Fraction



Destructors: we will ignore this for the moment



Inspectors: we will consider the following inspectors: int getNumerator() // the operation, applied to a fraction, returns the numerator of the fraction int getDenominator() // the operation, applied to a fraction, returns the denominator of the fraction boolean isEqualTo (Fraction f)

ƒ

Modifiers: we will use the following modifiers: // the operation, applied to a fraction, return true if the fraction has the same // value as the fraction f, false otherwise void simplifyFraction() // this operation, applied to a fraction, simplifies the fraction to its normal form void incrementFraction (Fraction f) // this operation, applied to a fraction, change its value by adding to it the value

6

// of the fraction f Let us illustrate another example of an ADT specification. Let us consider an ADT called AppointmentBook. Its specification can be as follows DOMAIN: the set of all possible appointment books OPERATIONS: Constructor: createAppointmentBook() // it produces a brand new appointment book, initially empty Destructor: none Inspectors: boolean isAppointment (Date date, Time time) // applied to an appointment book, the operation returns true if there is an appointment // at the specified date and time String checkAppointment(Date date, Time time) // applied to an appointment book, the operation returns the textual description of the // appointment at date and time Modifiers: boolean makeAppointment(Date date, Time time) // the operation adds an appointment at the given date and time; it returns true if the // appointment is successfully added, false if it fails (because there is already an // appointment at that date and time) boolean cancelAppointment (Date date, Time time) // it removes the appointment at date and time; returns true if it is succesfull, false // if there is no appointment at that time and date

7

.. .. .. .. .. Implementing ADTs: Java Classes The previous sections emphasized the specification of an ADT. When you design an ADT, you concentrate on what its operations do, but you ignore how you will implement them. The result should be a set of clearly specified ATD operations. How do you implement an ADT once its operations are clearly specified? The process requires developing a data structure and a collection to store the values and perform the operations. In order to ensure that the result is an ADT, we need also to enforce the principle of information hiding – i.e., ensure that the users of the ADT are not able to directly access the implementation of the ADT. For example: let us assume that, in the example of the appointment book, we use an array appt to store the various appointments. If we do not information hiding, then a program could be simply access the elements of the array, e.g., by writing something like appt[0] = 0 which could destroy the integrity of the implementation of the ADT (e.g., that operation might not make any sense when we talk about appointments, but if the implementation is not hidden nothing prevents the user from doing this type of bad things). Java classes provide us with a mechanism to create information hiding and easily implement ADTs.

Java Classes In the case of an ADT, encapsulation combines data (representing one entity of that type) with the operations that are used to manipulate such data. The resulting entity (data+operations) is called an object. You can think about an object as in the figure below, where the data are hidden and they can be accessed exclusively through the provided methods (operations).

Request

Methods

Data

OBJECT

Results

In Java, a class is a new data type, whose instances are objects. A class contains •

data fields



methods

Data fields and methods are collectively known as class members.

8

Methods typically act on the data fields. By default, all members in a class are private, which means that they are not directly accessible by anybody outside of the class. If one wants to make a member accessible from outside (i.e., open a window that allows to access such member), then it should be declared as public. If we use a class to create an ADT, then we will probably want to declare as public the methods that correspond to the operations of the ADT (as we want them to be usable from outside). Note that the methods present inside a class can access all the members of the class, independently from whether they are public or private. You should almost always declare the data fields of a class as private. If the value of a data field is required outside of the class, then you will need to create a method (public) specifically to read the value of the data field and return it. Similarly, if there is the need of being able to change the value of a data field, then you will need to create a method specifically to perform this task. In short, an object is a specific combination of data and methods. Classes define the types for objects; hence, objects are frequently referred to as instances of their defining classes. Some important observations regarding the use of classes to build ADTs: •

In Java, the constructor operation is represented by a special method, which always has the same name as the class. This method is automatically executed each time we request the creation of a new object belonging to the class



In Java there is an automated mechanism, called garbage collector, which is in charge of discovering objects that are not used any more. This relieves the programmer from having to worry about erasing objects. It is possible to specify a method that will be executed when an object is destroyed; this method is always called finalize.

Let us start with a simple example of an ADT used to describe Spheres. The specification of the ADT is as follows: DOMAIN: the set of all spheres OPERATIONS Constructor: createSphere(double radius) // create a new sphere with the specified radius Destructor: none Inspectors: double getRadius( ) double circumference( )

// returns the radius of the sphere // compute the circumference of the sphere

9

.. .. .. .. .. double area( )

// compute the area of the sphere’s surface area

double volume( )

// compute the volume of the sphere

Modifiers: void growSphere(double factor)

// grow the sphere by the given factor

The implementation of this ADT can be realized by building a class representing the Sphere. In particular •

whenever we create an object of a class, that object will represent one individual sphere



since each sphere can be represented by simply maintaining its radius, we will introduce one data member in the class, called radius. Note that each time we create one object from the class, that object is going to have its own radius member.



the various operations belonging to the ADT will be encoded as methods of the class. Since the operations will be executed by whoever wants to use this ADT, these methods will have to be public.

The complete class is described next: class Sphere { // data member private double radius; // constructor Sphere ( double rad ) { radius = rad; } // inspectors // getRadius: returns the radius of the sphere public double getRadius( ) { return radius; // read the value of the radius and send it out } // compute the circumference of the sphere public double circumference ( ) { return (Math.PI * 2.0 * radius); // compute the circum and send it out } // compute the area public double area ( ) { return ( 4.0 * Math.PI * radius * radius); //sends out the value of the area

10

} // compute the volume public double volume ( ) { return (4.0 * Math.PI * Math.pow(radius,3.0))/3.0; } // MODIFIERS // grow/shrink the radius by a given factor public void growSphere ( double factor ) { radius = radius * factor; // modify the radius } } // end of the Sphere class

Some comments about this code •



observe that the constructor must have the same name as the name of the class, and no return type; in our example, the constructor requires one argument, the value of the initial radius. This means that each time we create a sphere object, we will need to provide the initial radius as input, otherwise the sphere cannot be created. It is possible to have multiple constructors if one desires so. The different constructors must have all the same name and they can only differ in the parameters they request. In our example, we could have a second constructor that does not have any parameter, which creates a sphere with a default radius. We can accomplish this by adding another method in the class that looks as follows: // second constructor: creates a sphere with default radius Sphere ( ) { radius = 10.0; } note that the methods can freely access, read, and modify the data member radius; this is possible because both the radius data member as well as the methods are part of the same class.

How can we use this class? Let us write another class, containing a main method, which makes use of spheres class TestSphere { public static void main(String args[ ]) { // create one sphere with radius 5.0 Sphere s1 = new Sphere(5.0); // create another sphere with default radius

11

.. .. .. .. .. Sphere s2 = new Sphere( ); // let us print the radiuses of the two sphere System.out.println(“First sphere has radius” + s1.getRadius() ); System.out.println(“Second sphere has radius “ + s2.getRadius(); // change radius of the second sphere by doubling it s2.growSphere(2.0); // print the volume of the first sphere System.out.println(“The volume of the first sphere is “+s1.volume() ); } } Some comments •

note that we can declare variables of type Sphere; each one is a container that can contain one Sphere. Initially the contain does not contain anything.



to create a new Sphere, we need to apply the operation new, which creates a new instance of the class. This will also immediately execute the constructor method. In the first sphere (s1) we request the first constructor to be executed, since we are specifying one parameter (to be used as radius). For the second sphere we do not provide parameters, and this will lead to the execution of the second constructor (the one with no parameters).



each operation has to be applied to a specific object. So if we want to perform the operation getRadius, we need to apply such operation to one specific sphere; this is accomplished using the notation: s1.getRadius() which executes the method getRadius within the object s1.

Another example Let us go back to the example of the fraction. The implementation can be as follows: class Fraction { // data member; they store the num and den private int numerator; private int denominator; // constructor; used to generate a new fraction; need to specify its components public Fraction(int num, int den) {

12

numerator = num; denominator = den; } // inspectors // Read the numerator public int getNumerator() { return numerator; } // Read the denominator public int getDenominator() { return denominator; } // compare two fractions to see if they are equal public boolean equal(Fraction f) { return ( (f.getNumerator() == numerator) && (f.getDenominator() == denominator) ); } // modifiers // simplify a fraction public void simplifyFraction() { int g; g = gcd(numerator,denominator); numerator = numerator / g; denominator = denominator / g; } // add a fraction to the current one public void addFraction(Fraction f) { denominator = denominator * f.getDenominator(); numerator = numerator * f.getDenominator() + denominator * f.getNumerator(); } // auxiliary operations; these are used only internally and should not be made // available outside private int gcd (int x, int y) { int i; int g = 1; for (i=2; i