9. Programmer-Defined Types

advertisement
Objects
COMP 14, Fall 1998
Prasun Dewan1
9. Programmer-Defined Types
As we have seen before, Java provides us with a variety of types such as int, double, boolean, char and String.
Compared to other languages, this is a rich set of types. However, these types do not meet all of our needs.
Point
To understand why we need to define our own types, consider the problem of following the trajectory of a rocket we
have launched. Assume the trajectory is confined to a plane whose origin is the launching point. Let us say we are
interested in statistics about the highest point of the rocket: the angle a straight line to it from the origin makes with the
horizontal axis, and its total, horizontal, and vertical distance from the origin. As you may remember from Math, these
are referred to as the angle (), radius (R), X, and Y coordinates, respectively, of the point (Figure 1). The angle and
radius of a point are called its Polar coordinates and the X and Y coordinates are called its Cartesian coordinates. Each
of these pairs of coordinates completely represents the point, and it is possible to convert one representation to the
other.
Highest pt.
X
.
R
Y

.
Launching pt.
Figure 1: Tracking the Highest Point
We can imagine two versions of the program, one in which the users enter the Cartesian coordinates of the point:
and another in which they enter the Polar coordinates.
1
 Copyright Prasun Dewan, 1998.
1
Objects
Algorithm
A high-level algorithm for this problem would be:
1. Get a representation of the point.
2. Print out the angle, radius, X and Y coordinates of the point.
To do step 1, we must decide how the point is input. We could use either the Cartesian representation, which consists
of its X and Y coordinates, or the Polar representation, which consists of its radius and angle coordinates. Let us, for
now, decide on using the Cartesian representations.
This means that in step 2, we must be able to convert from Cartesian coordinates to Polar coordinates. From Figure 1
we can see that:
R = sqrt (X2 * Y2)
 = arctan (Y/X)
Java Implementation
We are now ready to write the Java code for this problem:
class ARocketTracker {
public static void main (String args[]) {
System.out.println("Please Enter Cartesian Coordinates of Highest Point");
double highestX = AKeyboard.readDouble();
double highestY = AKeyboard.readDouble();
double highestRadius = Math.sqrt(highestX*highestX + highestY*highestY);
double highestAngle = Math.sqrt (Math.atan(highestY/highestX));
print (highestX, highestY, highestRadius, highestAngle);
}
public static void print(double x, double y, double radius, double angle) {
System.out.println ("Highest Horizontal Distance
" + x);
System.out.println ("Highest Vertical Distance " + y);
System.out.println ("Highest Total Distance
" + radius);
System.out.println ("Highest Angle
" + angle);
}
}
A Different Representation
If we chose Polar coordinates, we must know how to convert from them to Cartesian coordinates:
X = radius*cos();
Y = radius*sin();
2
Objects
A Polar implementation is:
public static void main (String args[]) {
System.out.println("Please Enter Polar Coordinates of Highest Point");
double highestRadius = AKeyboard.readDouble();
double highestAngle = AKeyboard.readDouble();
double highestX = highestRadius*Math.cos(highestAngle);
double highestY = highestRadius*Math.sin(highestAngle);
print (highestX, highestY, highestRadius, highestAngle);
}
Problems with using predefined types
We did this problem without defining any new type, using the existing Java types such as double. As a consequence,
our two solutions have several problems:





No get function: While the code to output the point information is in a separate method, the code to input a point is
in the main method. Ideally, we want a function that inputs this data for us. However, a function can return only
one value, whereas in both representation, we need to return two double values. It would be nice if these two
values could be combined into one composite value of type Point.
Passing multiple representation parameters: If we had such a type, we would need to pass a single point
parameter to our print method, rather than four different values, which is more tedious and error prone (since we
may accidentally put the parameters in the wrong order.)
Difficult to change representation: As we switched from representation to another, we had to change every line of
the main method except the call to print. It would be nice if we could change our choice of the representation
without changing any other part of our program.
Mixing tracking and conversion: The tracking program must worry about converting between the two
representations of a point. It would be nice if this task was the responsibility of the type Point.
Difficult to reuse: Because both tracking and conversion are mixed to together, it is difficult to reuse one without
the other. By creating the type Point, we would be able to use the conversion code in other applications such as a
drawing application.
Using the type Point
Assume we can define a type, Point, that has the properties above. Our main method could then be:
public static void main (String args[]) {
Point highest = getPoint();
print (highest);
}
Instead of declaring multiple double variables for storing the representation of Point, this method simply declares a
single variable of type Point. It calls getPoint() to create a new instance of this type based on what the user inputs, and
then calls print to output it.
Let us next look at print():
public static void print (Point p) {
System.out.println ("Higest Horizontal Distance " + p.getX());
System.out.println ("Highest Vertical Distance " + p.getY());
System.out.println ("Highest Total Distance
" + p.getRadius());
3
Objects
System.out.println ("Highest Angle
" + p.getAngle());
}
This method assumes that the type Point defines methods to get the x, y, radius, and angle coordinates of a point.
Multiple Type Implementations
So far, we have not yet picked a representation for Point. Let us assume we have available to us two implementations
of type Point, ACartesianPoint and APolarPoint, which use the Cartesian and Polar representations, respresentations of
the point. Based on the format in which the input data is available, we decide which implementation to choose. This
choice is made in getPoint(). It could pick the Cartesian implementation:
public static Point getPoint () {
System.out.println("Please Enter Cartesian Coordinates of Highest Point");
return new ACartesianPoint (AKeyboard.readDouble(), AKeyboard.readDouble());
}
or the Polar implementation:
public static Point getPoint () {
System.out.println("Please Enter Polar Coordinates of Highest Point");
return new APolarPoint (AKeyboard.readDouble(), AKeyboard.readDouble());
}
In either case, this method returns new instance of type Point. In the first case, the instance is implemented by
ACartesianPoint while, in the second case, it is implemented by APolarPoint.
Invoking a Constructor
To better understand the statements:
return new ACartesianPoint (AKeyboard.readDouble(), AKeyboard.readDouble());
and
return new APolarPoint (AKeyboard.readDouble(), AKeyboard.readDouble());
we need to define the concept of a constructor. A constructor is a method associated with the implementation, I, of a
type T, with the following two properties:
 its name is I, that is, the name of the implementation.
 it returns a new value of type T based on the parameters passed to it.
Thus, the first statement calls the constructor of the implementation ACartesianPoint, passing it the values for the X
and Y coordinates read from user. The constructor returns a new instance of Point represented by these two
coordinates. We must use the keyword new before the invocation of a constructor to signify that we are creating a new
instance. Similarly, the second statement, calls the constructor of APolarPoint the values for the radius and angle
coordinates we read from the user, which returns a new instance of Point described by these coordinates.
Benefits of Using a Higher-Level Type
The difference between this solution and the previous one is that we are using a higher-level type here, that is, a type
that matches more closely the entity, point, defined by the problem domain. As a result, our main method:
4
Objects
public static void main (String args[]) {
Point highest = getPoint();
print (highest);
}
matches closely our top-level algorithm:
1.
2.
Get a representation of the point.
Print out the angle, radius, X and Y coordinates of the point.
More specifically, it has none of the disadvantages of using the lower-level predefined types we mentioned earlier:





Get function: We can now define a get function that returns a point, allowing our main method to be focus on the
higher-level algorithm.
Passing single instance of high-level type: We can pass to print a single instance of type Point rather than multiple
representation parameters.
Easy to change representation: As we switched from representation to another, all we had to do was change
getPoint. The rest of the program could be used unchanged. This is perhaps the most striking benefit of the second
solution.
Separating type definition and usage: The conversion between the two representations is done by type Point,
freeing the type user from it. There is, thus, a good separation between the code that defines the type and
associated conversion operations and the tracking code that uses it.
Reuse: The type Point can be reused by other applications that need this general type.
Therefore:
High-Level Typing Principle: Create types that describe as closely as possible the entities in the problem domain.
Defining Point
It would have been nice if Java had provided the type Point together with the associated implementations. In fact, Java
does provide a type called Point, but it is not the same as the one we used above. In general, it is impossible to
anticipate all the types a programmer might want. For instance, what if we wanted a type that captures your favourite
colours or your favourite animals. There is no way for the language to know this information.
This scenario is very similar to the situation we saw earlier with operations. Java provides some predefined
operations such as && and !, and allows you to define your own operations such as nand. Similarly, Java provides
some predefined types and allows you to define your own types.
Defining a Type
To understand how a new type may be defined, consider the information we need to use and understand an existing
type. We need to know the:
 name of the type (e.g. int)
 the operations that can be performed on instances of the type (e.g. +).
 constants of the type, both literals (e.g. 5, Integer) and named constants (e.g. Integer.MAX_VALUE).
We call this information the specification of the type. When we define a new type, we must provide Java with this
information. In addition, we must give Java one or more implementations of the type. Thus, defining a new type
involves two steps:
 provide a specification,
 provide one or more implementations
5
Objects
To understand the difference between a type specification and implementation, consider the development of a new kind
of car. Describing the features of the car such as cruise control and anti-lock braking corresponds to specifying a type,
and building a factory to produce the car corresponds to implementing the specification. Just as we can have multiple
factories manufacturing products that follow the same specification, we can have multiple type implementations, such
as ACartesianPoint and APolarPoint, producing instances that conform to the same type specification.
Specifying a Type: Interface
In Java, a type is specified by declaring an interface. The following interface specifying the type Point illustrates the
nature of an interface:
interface Point {
public double getX();
public double getY();
public double getRadius();
public double getAngle();
}
The keyword interface tells us that we are declaring an interface. An interface declaration is similar to a class
declaration in with the following main differences:
 Headers Only: It contains headers of the methods that can be invoked on instances of the type . Method bodies are
not allowed since it cannot contain implementation details.
 Public Methods Only: All methods declared in it must be declared public. Otherwise they would not be visible
outside the interface, and users of the type such as ARocketTracker, would not be able to invoke them. Declaring a
non-public method is similar to providing an operation in a car that its users cannot use. Of course a car can have
operations such as change the mileage meter that a user cannot directly invoke. However, these are implementation
operations, which do not belong to the specification. An interface describes only those operations on its instances
that a user can directly invoke.
 Instance Methods Only: None of the methods can be declared static. Recall that static is used to declare a class
method, which is invoked on a class such as AKeyboard:
AKeyboard.readDouble()
If the word static is omitted, then the method is invoked an instance such as highest:
highest.getX().
A class method corresponds to an operation on a factory, while an instance method corresponds to an operation a
product produced by a factory. An interface is a specification of the operations on its instances, hence it can only
declare instance methods.

Final Variables Only: An interface cannot declare non-final variables, since the specification should remain
constant during the execution of a program. We will later see an example that declares constants in an interface.
Implementing a Type
An implementation of an interface must declare the bodies of the method headers declared in the interface. We already
have a construct to define method bodies – a class. The following declaration of the class ACartesianPoint illustrates
how a class may be made an implementation of an interface:
class ACartesianPoint implements Point {
double x, y;
public ACartesianPoint (double initX, double initY) {
x = initX;
y = initY;
}
6
Objects
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getRadius() {
return Math.sqrt(x*x + y*y);
}
public double getAngle() {
return Math.atan(y/x);
}
}
The variables, x and y store the Cartesian coordinates of a point. These variables are initialized by the constrictor
ACartesianPoint. The functions getX and getY simply return the values of x and y, respectively. The functions
getRadius and getAngle convert the Cartesian coordinates to the radius and angle coordinates, respectively.
Thus, like other classes we have seen so far, this class is a collection of complete methods (with both headers and
classes) and some variables declared outside the methods. There are, however, several important differences:
 Implements Clause: The class header has an implements clause, consisting of the implements keyword and an
interface name, to indicate the type specification it is implementing.
 Instance Methods: As in the case of the interface it is implementing, the class defines instance methods. So far, we
have used a class to implement only class methods. In general, a class can define both instance methods and class
methods. If it is responsible for implementing an interface, it must implement all the methods specified in the
interface. The headers of a method specification and implementation must match, that is, they should share the
keywords, method name, and method signature specified in their declarations. The names of the arguments and
order of keywords can be different.
 Instance Variables: The variables x and y, declared outside all method headers, do not have the keyword static in
them. This is because they are instance variables rather than class variables. Instance variables store the data
specific to an instance, while class variables store the date relevant to the whole class. A separate copy of an
instance variable is created each time we instantiate a class using the new operation, while a single copy of a class
variable is created when a class is defined. Thus, there may be zero or more copies of an instance variable at any
one time, but exactly one copy of a class variable at all times. In general, a class can define both instance and class
variables
To better understand instance variables, consider Figure 3. It shows the result instantiating of the class twice:
Point lowest = new ACartesianImplementation (0,0);
Point highest = new ACartesianImplementation (10,20);
7
Objects
highest.x
highest.y
10
20
0
lowest.x
lowest.y
0
Java allocated two copies of the instance variables, x and y, of the class: one for lowest and the other for highest.
When an instance method such as getX accesses x:
return x
which copy of x should it return, given that there are multiple copies of it? The answer depends on which instance
it is invoked on. The invocation;
highest.getX()
will access highest.x, while:
lowest.getX()
will access lowest.y. This is analogous to a method with a formal parameter such as n being bound to different
values actual parameters such as 5 and 6. In fact, the instance on which an instance method is invoked can be
thought of as an implicit parameter of the method.

Constructor method: It defines the constructor ACartesianPoint. Notice, that the header of a constructor method
does not have a type name. This is because the type of the value returned by it (Point2) is implicit in the class
declaration. If you accidentally put a type name in the header, Java will not recognize the method as a constructor.
The header of a constructor is not declared in an interface. This is because construction of an instance of an
interface is an implementation detail. It is by naming the constructor that a user of a type determines which
implementation to use. As we saw earlier, executing:
new ACartesianPoint (AKeyboard.readDouble(), AKeyboard.readDouble());
chooses the implementation provided by ACartesianPoint.
When we use new to call a constructor of a particular class, Java takes two steps:
2
Strictly speaking it is ACartesianPoint, since every class is also a type, as we shall see later.
8
Objects


Create Instance Variables: It first creates new copies of the instance variables declared in the class. In the use of
new above, it creates new copies of the instance variables x and y declared in ACartesianPoint
Call Constructor: It then calls the constructor method, which typically initializes the instance variables based on its
parameters. In our example, it initializes x and y to the values input by the user.
The new value is considered not only an instance of the type implemented by the class, but also an instance of the class
itself. Thus, in our example, the new value is considered an instance of both the interface Point and the class
ACartesianPoint. We refer to the process of creating a new instance of a particular class as instantiating the class. This
is consistent with treating classes as types themselves, discussed later.
Alternative Implementation
The class APolarPoint is very similar, except that it uses Polar coordinates to represents a point:
class APolarPoint implements Point {
double radius = 0;
double angle = 0;
public APolarPoint(double initRadius, double initAngle) {
radius = initRadius;
angle = initAngle;
}
public double getX() {
return radius*Math.cos(angle);
}
public double getY() {
return radius*Math.sin(angle);
}
public double getRadius() {
return radius;
}
public double getAngle() {
return angle;
}
}
Primitive vs Object Types
A type such as Point that is implemented by a class is called an object type T and its instances are called objects.
Programming using objects is called object-based programming.
In Java, not all types are object types. None of the primitive types we studied earlier is an object type. But there are
predefined Java types such as String and DataInputStream that are object types. We know a type is an object type if we
use the method invocation syntax to invoke operations on them. Moreover, it is the convention in Java do start the
name of an object type with an uppercase letter and a primitive type with a lowercase letter. The following figure
classifies some of the types we have seen.
9
Objects
Types
Primitive
Types
int
double
Object
Types
String
Point
Defining a Generic Type
Our specification of type Point was motivated by the rocket-tracking problem we had to solve. By defining this type,
we have been able to separate the implementation and use of points. Another advantage is that other programs that also
wish to manipulate points can use many features of our type.
We did not put any feature into the type Point that we would not use in our problem. As a result, it may not be
sufficient for the needs of other programs. Therefore, once a type has been defined to solve a particular problem, it is
useful to revisit it’s definition and make it more generic, that is, identify and add other features that other users of the
type may want.
To understand the nature of this process, let us see the kind of features that can be added to this type
Constants & Mutual Dependencies
Recall that each of the predefined types defined its own constants. Similarly, a programmer defined type may also
provide constants. For instance, we might want to define a constant that represents the origin of the plane in which the
points are defined.
It is possible to define the origin in the interface Point or in the two classes, ACartesianPoint and APolarPoint. Since
an origin exists in any implementation, and would be expected to be the same value in all implementations, we will
define it in the interface. A straightforward way to define it is to declare doubles that store it’s X and Y coordinates:
interface Point {
// interface constants
public static final double ORIGIN_X = 0;
public static final double ORIGIN_Y = 0;
// instance methods
…
}
These constants are declared static because they store information regarding the whole type and not a specific instance.
An interface constant can be named outside the interface by prefixing it with the interface name. Thus, a program can
use these constants to define a point encapsulating the origin:
public static final Point ORIGIN = new ACartesianPoint(Point.ORIGIN_X, Point.ORIGIN_Y)
However, it requires the user of the type to convert these two double constants to a point. What if we wanted the origin
to be defined in the interface Point as a Point? It is, in fact, possible for a type, T, to define constants of type T:
10
Objects
interface Point {
public static final Point ORIGIN = new ACartesianPoint(0,0);
// other members
…
}
We have to choose an implementation to construct this point, and we have arbitrarily chosen ACartesianPoint here.
Can this interface declaration work? We are in the process of specifying a type. Presumably the implementation will
come later. But to construct the constant, we have to assume an implementation exists. To put this another way, we
have created a mutual dependency between Point and ACartesianPoint. We cannot fully define ACartesianPoint unless
Point is defined, since the former refers to the latter in the implementation clause. On the other hand, we cannot fully
define Point unless ACartesianPoint is defined, since it is used in the constant definition.
The solution is to define the interface in two stages, as we have done. First create and compile the interface without
constants. Then create and compile an implementation. Now go back and construct constants in the interface using the
implementation.
11
Download