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