CS61B, Lecture #5, 6/26/00. Michael Brudno based on P. N. Hilfinger's notes. Administrative Announcements -------------- ------------- 1. Everyone must now be enrolled in a section. 2. Reading assignments are on the class syllabus. "Object-Oriented" Programming ----------------- ----------- So far, we have talked about several primitive types, each with its own distinct properties, and about how to create new types, each presumably with its own distinct properties. As you get further into the type-creation business, you will increasingly find yourself creating SIMILAR types---so much so, in fact, that you will start asking yourself questions like "Why must I write still another Gadget class that looks just like the last twenty Gadget classes I wrote, except for these one or two annoying differences?" A related question: why must I write still another doThisAndThat method when it looks exactly the same as my last doThisAndThat method, except that it applies to arguments of type Gadget instead of to arguments of type Widget? A cluster of constructs that go under the rubric "object- oriented programming language" (OOPL) address a part of this issue. Let us imagine that you are writing a program that simulates the lives of many people. All of the people can do certain things: for instance eat and work, but while all people may eat in a similar manner, all of them work in slightly different ways. Also all people get hungry. Hence we will define a class worker: class Worker { int hunger = 0; static int workDone = 0; Worker() { } void live() { this.work(); this.eat(); this.haveFun(); // OR just work(); eat(); } void eat() { hunger -= 1; System.out.println("yum"); } void work() { hunger += 1; // Or this.hunger workDone += 1; // Or this.workDone or Worker.workDone System.out.println("whistle"); } void haveFun() { System.out.println("Martini, shaken not stirred"); } } ---------------------------------------- Worker aWorker = new Worker(); aWorker.live(); ---------------------------------------- Here, the class `worker' defines methods for eating, working, living, and having fun. Now lets look at a TA class. TAs are capable of doing slightly more: than other workers. They can, for instance, grade exams. Also some things other people do TAs cannot. Hence we wiil define the TA class as follows: class TA extends Worker { TA() { super(); } void work() { super.work(); System.out.println("Let me help you"); } char grade(String student) { super.work(); if (hunger < 2) return 'A'; return 'F'; } void haveFun() { } } ----------------------------------------- TA aTA = new TA(); Worker aliasTA = aTA; aTA.live(); // Does the same thing as aliasTA.live(); aTA.grade("eric"); // OK aliasTA.grade("eric"); // ERROR ----------------------------------------- The TA class defines methods for working, grading exams, and having fun. However, because of its `parent' clause, the class TA "inherits" definitions for living and eating as well. When I ask aTA to `live', I invoke the `live' method defined in the class worker. This in turn calls the `work' method followed by the `eat' method on the object pointed to by `self', which in this case will be the TA object pointed to by aTA. When `live' asks aTA to `work', we get the `work' method defined in the class TA, which is said to have "overridden" the definition of `work' in its parent class. aWorker.workDone == 2, aTA.workDone == 2, aliasTA.workDone == 2 Worker.workDone == 2, TA.workDone == 2 aWorker.hunger == 0 Worker.hunger // ILLEGAL Worker.eat() // ILLEGAL All instances of classes worker and TA share a single variable called work-done, which is why all the calls to work-done get the same result. On the other hand, it makes no sense to ask for the value of `hunger' of the CLASS worker, because there are many instance variables `hunger', one for each instance of worker. The same applies to asking the CLASS worker to eat without saying WHICH worker is supposed to eat. The correspondences between the new Java stuff and Scheme constructs are quite direct: Scheme Java ------ ---- A. (class-vars (work-done 0)) | static int workDone = 0; | B. (parent (worker)) | class TA extends Worker { | TA() { super(); } "Worker is the parent of TA. | "Worker is the superclass of TA TA is a child of Worker." | TA is a subclass of Worker." | C. (usual 'work) | super.work(); | D. self | this | E. (ask self 'work) | this.work(); or work(); | F. (ask aWorker 'work-done) | aWorker.workDone (ask worker 'work-done) | Worker.workDone | G. (ask aWorker 'hunger) | aWorker.hunger NOTES: * You might wonder why I call Worker the "superclass" when it has fewer methods and fields than does TA. The reason is that although every TA is a Worker (and responds to all the methods of a Worker), not every Worker is a TA. Hence, the set of Workers is a superset of the set of all TAs. * Clause C means ``call the work() method that would get called if I had were purely an object of my superclass.'' In B, the construct `super();' means ``call the constructor for my superclass.'' * I explicitly wrote out the constructor for TA in this example. a constructor with no parameters is called a "default constructor". When you have a class in which you have not explicitly supplied any constructors, the language specifies that a default constructor having exactly the contents shown (i.e., a call to super()) is implicitly added. Method Overriding ------ ----------- The "extends" relation creates a tree of classes. At the root of this tree in Java is the class Object---everything having a reference type (everything but ints, longs, doubles, floats, chars, bytes, shorts, or booleans) is an Object. Whenever you don't have an `extends' clause in a class definition, the compiler inserts `extends Object' automatically. The class Object has the following definition, in part: public class Object { public final native Class getClass(); // N.B. `Class' is capitalized public native int hashCode(); // Some weird integer. public String toString() { return getClass().getName() + "@" + Integer.toString(hashCode() << 1 >>> 1, 16); } ... lots of other stuff } The toString member function is non-static (i.e., its declarations does not contain the qualifier "static"). Since it is non-static, if you define a toString routine on your class that has the same signature as the one in Object (i.e., takes no arguments, returns String), this function definition is said to "override" the one in Object, and is used whenever toString is called on an object of your class. Because of how 'print' works, overriding toString automatically defines an output format for the class. For example, the project suggests producing a toString method for class rational. If you didn't care about whether you printed an improper fraction, you could define it as class Rational { ... public String toString() { return this.numer()+ "/" + this.denom(); } ... } and then output a rational number, R, as System.out.print(R); But this raises an interesting question: How is the method print declared? In fact, the print method that gets called here (defined in class java.io.PrintStream) has a declaration something like this: void print (Object obj) { String toPrint = obj.toString(); // print the string toPrint ... } (the real one checks for obj==null as well). Its argument is of type Object. The rules of Java allow this method to accept any value that "is an" Object. Since all reference types in Java (including arrays) extend Object (through one or more steps), EVERY reference value "is an" Object. When 'print' calls the toString member function of OBJ, it calls the function determined by the type of object that was actually passed (thus, when print is called with Rational argument R, it is the toString method defined in class R that is called). Static and Dynamic Types ------ --- ------- ----- [WARNING: Really, Really Important Points approaching! Re-read this section, and Section 5 of the "A Model for Memory, Names and Types" chapter in the class reader as many times as it takes to understand them.] The behavior just described can sound very confusing at first: "Function print takes an Object as its parameter---it says so in the parameter list---so why is it (a) that I can pass a Rational instead and (b) that when print calls toString, it gets the toString function defined in Rational rather than the one in Object? Here is where it is vital to have a coherent model of what is really going on here. So here it is: When the call System.out.print(R) is executed, the system creates a new variable (container) named obj and puts the value R (a pointer) into it: +------------+ +--+ | | obj: | *+------------------------------>| | +--+ | | +------------+ The <> labels are types. The container obj has type Object (or to be precise "pointer to Object"). Its value (a pointer) has the type . We say that "Object is the *static type* of container obj" and that "Rational is the *dynamic type* of obj" or ("Rational is the type of the value in obj"). The static type of a container determines the allowed dynamic types of values you store into it. Because class Rational extends class Object, we say that "a Rational *is an* Object", and that any container having static class Object may contain any value that "is an Object". The dynamic type of a value determines what (non-static) function gets called when you perform an "ask" operation (Scheme terminology) or a member function call (Java terminology). Expressions in general also have a static type: the static type of an expression consisting: * of a container (field, parameter, or variable) is the static type of that container; * of A[x], where A has static type T[], is T; * of a function call has is the declared return type of the function; * of the special variable "this" is the class in which the member function that contains it is defined. * of the expression new T(...) is T. A Java compiler uses static types to perform a "sanity check" on your program that is known as *static type checking*. When T1 is a reference type, it requires, in particular, for x = E; where x has static type T1 f(E); where the formal parameter of f has static type T1 that if E has STATIC type T2, it is required that any T2 "is a" T1. Now you might wonder about this rule: "You said that the static type of a container determines what (dynamic) types of values it can contain, so why not check the DYNAMIC type of the value"? The reason is that such a check is in general impossible, and can be extremely difficult even when it is possible. For this reason, among others, you must sometimes explicitly convert values ("coerce" is the term of art) so that the compiler knows that what you ask is sane. The Java construct for converting values is called a *cast*. The Meaning of Names --- ------- -- ----- [WARNING: More Really, Really Important Points approaching!] The following two classes and the calls that follow illustrate the somewhat subtle rules that govern the meaning of a method or field name. class Parent { class Child extends Parent { int x = 1; int x = 2, y = 3; void f () { S1; } void f () { S3; } static void g () { S2; } static void g () { S4; } } static void f(int q) { S5; } } Parent P1 = new Parent (); Child C = new Child (); Parent P2 = C; P1.x /* == 1 */ P1.f () /* do S1 */ P1.g () /* do S2 */ P2.x /* == 1 */ P2.f () /* do S3 */ P2.g () /* do S2 */ C.x /* == 2 */ C.f () /* do S3 */ C.g () /* do S4 */ C.y /* == 3 */ C.f(2) /* do S5 */ P1.y /* ILLEGAL */ P1.f(2) /* ILLEGAL */ P2.y /* ILLEGAL */ P2.f(2) /* ILLEGAL */ In summary, in anything of the form E1.Q, where E1 is some expression whose static type is T1 and whose dynamic type is T2, * If Q is a field, E1.Q refers to the definition of Q in T1 (this is true for both static and non-static fields). * If Q is a static member function, E1.Q refers to the definition of Q in T1. * If Q is a non-static member function, E1.Q refers to the definition of Q in T2. Casts ----- Consider the following List type: public class List { /** A List with head and tail. */ public List (Object head, List tail) { this.head = head; this.tail = tail; } /** A List with null head and tail. */ public List () { } // Or: public List() { this(null, null); } public Object head; public List Tail; } I can now write, e.g., Rational x = new Rational(1,3); List L = new List(x, new List("is the answer", null)); and now L is a list whose first element is the Rational representing 1/3 and whose second element is the String "is the answer". This is all perfectly legal. I can also write, e.g., System.out.print (L.head); System.out.println (L.tail.head); since print is expecting anything that is an Object, and L.head is known to be one. But what about `L.head.numer()'? We can see that this ought to work, since we happen to know that the head of L is, in fact, a Rational. However, Java does not allow it; it is a compile-time error. [Why? Because the static type of L.head is Object, and there is no numer() function defined for Object]. Instead, one must write ((Rational) L.head).numer(); The construct `(T) E' (T is a type and E is a unary expression other that doesn't start with + or -) is called a "cast", (in the sense of "cast the value E in the role of a T"). The cast expression has static type T. This will pass muster with the compiler. If, however, the value of E happens NOT to be a Rational, as in (Rational) L.tail.head the cast construct will fail during execution, throwing the standard exception ClassCastException. The type T in a cast may also be one of the primitive types. Any numeric type may be converted to any another. More on this in a later lecture. For now, we'll just say that a container whose static type is primitive can contain only values whose dynamic type is the same. However, the language softens this position a bit by inserting coercions implicitly in cases like this int x = 'c'; in which all possible numeric values of type char (the type of 'c') are also represented in the type int. Type Wrappers ---- -------- Another problem with writing a general List class is that the primitive types (like int) are NOT reference types, and are NOT extensions of Object. (There are various technical reasons for this, having largely to do with performance (speed) issues.) For each of the primitive types, therefore, the standard Java library provides a wrapper type whose purpose is to provide a way to get a kind of Object that can hold a primitive value so that it can be used in things like List. For example, the type Integer allows one to put int's into a List (as defined above) by writing List L = new List(Integer(1), new List(Integer(42))); One extracts the int back out of the Integers with the appropriate method call: ((Integer) L.tail.head).intValue() == 42 There are other similar classes for other primitives: Byte, Short, Long, Boolean, Character, Double, and Float.