CS 334
|
Guy Steele identified the major problems as the lack of parametric polymorphism and operator overloading.
We'll be looking at GJ which is an extension of Java which attempts to remedy the first problem.
Java supports "interfaces" for classes, closer to real types. Interfaces contain only the constants and methods of classes that are intended to be publicly available.
Here is a simple example of an interface:
interface EmployeeSpec{ // Declares interface implemented by class Employee String getName(); double getWkPay(); }
Any class implementing EmployeeSpec must provide public methods getName() and getWkPay().
When one class extends another, all of the instance variables and methods of the superclass are implicitly "inherited" in the extension. Methods with the same name and signatures "override" the corresponding methods of the superclass.
Constructors are NOT inherited (and, in fact, they must have the same name as the class, so couldn't usefully be inherited). The first line of a constructor of a subclass must be a call of the constructor of the superclass. This is written as super(...). Why?
Because Java objects keep track of their own method suites, the static type of an object may not tell you what code will be executed when you send a message to that object.
At run-time the contents of a variable of type (class or interface) C may actually be an object created by an extension. Thus one typically cannot determine statically whether the code from C (presuming C is a class) or one of its extensions will be executed. If C is an interface, then one has even less information statically about what code may be executed. This is generally considered a GOOD thing in object-oriented programming! (Though there may be an execution-time price to be paid!)
If class SC extends class C, then an object created from SC (i.e., of type SC) can be used in any context expecting an object of type C. Thus SC is treated as a subtype of C. This is sound because objects of the subclass have at least all the methods of the superclass (and the signatures are the same!).
By our discussion of subtyping earlier, it would be possible to allow covariant changes to return types of methods and contravariant changes to parameter types and still have subtypes. No changes are allowed to instance variable types because they are variables.
Unfortunately, Java's subtyping rule for arrays is statically inconsistent! If B extends A then Java assumes that B[] is a subtype of A[]. For example, suppose B has a method void bMeth() that is not contained in A. Look at the following example:
public void m(A[] a) { a[0] = new A(); } B[] b = new B[10]; m(b); b[0].bMeth();The last method call is statically safe, but results in a run-time error because b[0] consists of an element of A, which does not understand bMeth.
If class C implements interface IC then objects of type C can be used in any context expecting an object of type IC. Can also have extensions of interfaces -- gives relatively complicated subtyping relation involving classes and interfaces.
Subtyping is by declaration (name) in Java. Thus one type may be used as a subtype of another only if there is a chain of extends and implements going from the first to the second. For example if we have
SC extends C C implements IC IC extends IBCthen an object from class SC can be used in any context expecting an object of interface IBC.
An example is a class for graphic objects which can be dragged around the screen. Each of these will have x and y coordinates as well as move and draw methods. The move method will be the same for all graphic objects as it will just involve changing the x and y coordinates, while the draw methods will depend on the actual kind of object.
Most (but certainly not all) experts in object-oriented programming believe that multiple inheritance is more trouble than it is worth. Horrible problems can arise, for example, with name conflicts and with the same code being inherited in different paths through superclasses. We will look at some of these issues when we consider Eiffel later.
In order to provide support for this situation, Java includes interfaces. As we have seen, an interface is a specification of public entities. A class implementing an interface must have a public entity corresponding to each element of the interface.
An abstract class with no instance variables and all methods public and abstract is really the same thing as an interface, and should always be declared instead as an interface. I often see books which include such purely abstract classes without thinking (usually because they have just translated code from C++). This is generally considered bad Java style for two reasons, the first principled and the second pragmatic:
On the other hand, if you really need to inherit code from two or more classes you don't have any nice options. One way around this is to just copy and paste code from one of the classes you wish to inherit from. The bad part of this is that changes in the code of the superclass won't be reflected in the new class. The other option is to have the new class contain an instance variable initialized to be an object of the desired superclass. You can then write adapter methods to "forward" all calls of the superclass methods and send them to the instance variable. Sometimes this works nicely and sometimes it doesn't.
In either case, if you want subtyping to hold, you must make sure that the new and old class implement the same interface and that you use the interface consistently in place of the class for variable and parameter declarations. The bottom line is that there are no great solutions to emulating multiple inheritance. Luckily, the real need for it shows up pretty rarely, and most observers agree that including multiple inheritance adds more complications to a language than it is worth. We'll see some of the complications that can arise when we discuss Eiffel, which does support multiple inheritance.
Interfaces support the same kind of information hiding as ADT languages. Generally, in order to write an algorithm, one needs only to know the methods that the objects respond to. As a result, using an interface rather than a class as the type of variables and parameters should cause no difficulty. It also makes it easier to make changes to your program later on if you wish to change the implementation of objects from one class to another or even if you wish to mix and match different implementations as long as they support the same interface. For example you might have a method which takes a Point (class) as a parameter. While it will handle all extensions of Point, we would be unable to get it to accept a completely different implementation (e.g. polar point) which did not inherit from Point.
The general rule should be to use classes where you need to be certain of a particular implementation (e.g., in creating objects or extending classes) and to use interfaces elsewhere. This does have the disadvantage of causing you to create interfaces in situations where a class name would work fine, but has the advantages of flexibility in the long term.
class C{ E m(A a){...} F m(B b, D d){...} }Overloaded methods are treated by the compiler as though they had entirely different names (e.g., as if the compiler renamed them as m1 and m2).
One restriction is that different "overloadings" of a method m must have different parameter types. For example, it is illegal for a class to contain both of the following methods
int m(A a){...} Point m(A a){...}This is the same restriction as would be found in C++ (though Ada would presumably allow it). It is possible to overload across class boundaries in Java (which is not true in C++).
class A{ void m(A a){...} -- implementation 1 } class B extends A{ void m(B b){...} -- implementation 2 }Inside class B, both versions of m are visible. However that does not mean that it is easy to see which would be called under a various circumstances. One thing you must keep in mind is that overloading is resolved statically (at compile time), while dynamic method invocation (choosing which of the overridden method bodies to execute) is done dynamically (at run time). Adding a method in a subclass with exactly the same signature as one with the same name in the superclass results in overriding (dynamic method dispatch), while adding one with the same name but different signature results in overloading (resolved statically). The example above is complicated by the fact that the parameter of the version of m added in B is an extension (and hence treated like a subtype) of the version in A. Suppose we have the following declarations of variables using the classes A and B defined above:
A a1 = new A(); A a2 = new B(); -- legal because B extends A B b = new B();Exercise worked in class: Each of the following are legal. Please determine which method body is executed for each message send.
a1.m(a1); a2.m(a1); b.m(a1); a1.m(a2); a2.m(a2); b.m(a2); a1.m(b); a2.m(b); b.m(b);The first and last are pretty easy. The others are not as obvious as they may first seem. Only rarely have I seen anyone get them all right the first time (though most people don't have too much trouble understanding the explanations as to why the correct answers are indeed correct). See me for answers or ask someone who was in class.
As a result of these confusions between static resolution of overloading and dynamic method dispatch I strongly recommend that you avoid static overloading as much as possible.
In this case the instanceof method is exactly what you need:
if (emp instanceof HourlyEmployee){...}The method instanceof (note the unusual lack of capitalization) is written as infix and returns a boolean value. It returns true iff the value of the argument on the left side is from the class (or interface) on the right side or is from any of its extensions.
If instanceof returns true, then it is OK to "cast" the expression to the type you checked. Type casts are indicated by placing the (static) type you want the value to be treated as in parentheses in front of the expression.
if (emp instanceof HourlyEmployee){ HourlyEmployee hemp = (HourlyEmployee)emp; hemp.setHourlyPay...} else {...}The purpose of a cast is to inform the static type-checker that it is OK to treat an expression as though it had a different type. In the example above, the type-checker would not allow emp to be assigned to hemp without the cast. Similarly the static type-checker would not allow the message setHourlyPay to be sent to emp. A cast which succeeds has no run-time effect! (Note that this is different from many casts in languages like C and C++.) However the cast does initiate a run-time check to make sure that the value of the expression is of the type it is being cast to. If it is not, then it will raise an exception.
As you might guess, Java will allow you to do a cast without first checking the run-time type using instanceof, however you must then be ready to handle the exception if it is possible you will ever be wrong.
Casts for base types work differently from references. There the cast will actually have a semantic effect. For instance you can convert a double to an int by writing (int)dbleValue.
There is one more complexity with casts and arrays. Suppose we have:
Object[] obs = new Object[20] for (int index = 1; index <20; index++) obs[index] = new Integer(index); Integer[] ints = (Integer[])obs; //?????????In spite of the fact that the entire array obs is filled with Integer's, the cast on the last line will fail! The problem is that array casts do not involve dynamic checks of each element of the array (probably because it would be too expensive). Also, recall that successful casts are not supposed to have any run-time effect. They merely signal to the static type-checker that it is OK to perform certain operations. Because arrays carry around their types, an Object array filled with Integers is not the same as an Integer array filled with exactly the same values. Their "run-time" types (or classes) are different! Thus you must be very careful with type issues related to arrays.
Usually you can avoid casts of references if you redesign your methods a bit so that the overridden methods in subclasses do different things than in the superclass. However, sometimes casts just can't be avoided.
Omitted in most OOL's: Smalltalk, Eiffel, C++.
Classes typically used instead.
What do modules provide?
Aren't classes enough?
No!
What's wrong with Java Packages? Supposed to provide facilities of modules.
Details of Java Packages and visibility: A class (or interface) belongs to a package if it includes a declaration at the top of the form:
package SomePackage;
Visibility restrictions in Java: public, default, protected, or private
Notes: