CS 334
|
Biggest loss in moving from FORTRAN to Pascal is lack of support for modules with persistent local data.
Clu, Ada, and Modula 2 attempted to remedy this by adding clusters, packages, and modules.
In Ada & Modula 2, objects (i.e. packages, and modules) were late additions to an earlier paradigm (Pascal-like)
ADT languages provide reasonable support for all but extensibility (in particular if want minor extensions - but rest is same), some limitations on reuseability.
Object-oriented languages are an attempt to make progress toward these goals.
A programming language is object-oriented if:
Simula 67 first object-oriented language - designed for discrete simulations.
Up until recently, Smalltalk best-known - Alan Kay at Xerox (now at Apple).
Gone through Smalltalk-72,-74,-76,-78,-80.
C++, object-oriented extensions to Pascal, C, LISP, etc.
One of nicest is Eiffel - discuss later (See Meyer's Object-Oriented Software Construction). Also Sather (public-domain variant of Eiffel). Of course Java is now becoming the most popular (and one of best).
Object-oriented languages built around following concepts:
Objects are internal data abstractions - only accessible to outer world through associated procedures
Object types
Classes
Most current OOL's identify object types and classes (in particular subtypes and subclasses).
See later this can lead to holes in typing system and/or restrictions in expressibility.
In typical object-oriented programming language, everything is an object.
Abstraction preserved since no object can make changes to the internal state of another object - though some languages don't enforce this - just send messages using methods in the public interface.
We will first investigate Java and Eiffel, as examples of object-oriented programming languages and then come back and discuss object-oriented languages in general, especially issues in type systems for object-oriented languages.
In my opinion, three factors worked toward the sudden popularity of Java:
Unlike most other OO languages, Java supports "interfaces" for classes, which play a role similar to that of module interfaces for implementations. 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().
Here is a simple example of a class implementing this specification:
public class Employee implements EmployeeSpec{ /** Class to represent an Employee. Should be abstract class since getWkPay isn't really defined here. */ // fields protected String name; /** name of employee */ protected Date birthDate; /** employee's date of birth */ // constructors -- note that constructors do not have return type! public Employee (String emp_name, Date emp_bday){ /* post: creates Employee object from name and birthdate */ name = emp_name; // initialize fields birthDate = emp_bday; } // methods public String getName() // post: Returns name of Employee { return name; // return name field } public int getAge() /** post: returns the age of the Employee */ { // declares today and initializes to today's date Date today = new Date(); // declare yearDiff and initialize to diff of years int yearDiff = (today.getYear() - birthDate.getYear()); if ((today.getMonth() > birthDate.getMonth()) || (today.getMonth() == birthDate.getMonth() && today.getDay() > birthDate.getDay())){ return yearDiff; // already had birthday this year } else{ return yearDiff - 1; // adjust age if not had birthday yet this year } } public double getWkPay() /** post: Return weekly pay. No value assigned for Employees. Will be overridden in subclasses. Really should be abstract method, but fix later! */ { return 0.0; } /** This is the main program. It is written as a method and usually stuck at end of central class of the program. It is a procedure since it returns type void. The parameters are values included on the command line durign the actual call of the program. They must be included, but are not used here. Main is declared to be static because it is only included in the class and not in each object (because it is the main program). */ public static void main(String args[]){ // Create and print my birthday. Printing implicitly calls toString Date kbday = new Date(1948,10,16); System.out.println("My birthday is "+kbday+"."); // Create new employee Employee emp = new Employee("Kim Bruce",kbday); // Print out info on the employee System.out.println("Today, the employee, "+emp.getName()+", is "+emp.getAge() +" years old."); System.out.println("He makes $"+emp.getWkPay()+" per week!"); } }In the first line of the class, it is indicated that the class implements the interface EmployeeSpec. This means that an instance of the class can be used in any context expecting an element of type EmployeeSpec. In Java, both class name and interface names can be used as the types of variables and parameters.
This class contains 2 instance variables, a constructor (used to create "instances" of the class), 3 methods, and a static routine main which can be used to test the class.
The two instance variables, name and birthDate, are of types String and Date and are "protected". They represent the state of instances of the class.
The "constructor" Employee takes a String and Date and uses them to initialize the instance variables. Constructors are used to create new instances of the class. Constructors always have the same name as the class and never have return types.
The (function) methods getName(), getAge(), and getWkPay() are all public, and represent the operations which instances of the class can perform. Their return types are written in front of the method name, while parameters follow.
The "main" routine is not a method of the class. It can be used as the main program to test the class.
By looking inside main, we can see how to create and use instances of Employee. First, a new Date is created (Date is another class of the system not shown) with the new Date(...) expression and it is assigned to the newly declared variable emp of type Employee. Date is the name of the class, so all of its constructors also have that name. As you will see later, Date has a constructor which takes three ints as parameters.
System.out.print(...) and System.out.println() are used for screen output. The only difference between them is that the second adds a carriage return after it is done printing the string inside. The operator "+" in Java is overloaded and here represents string concatenation. Each object has a built-in method toString(), which is automatically called when it is used in a context expecting a string. Thus writing "+kbday" inside println generates an implicit call of toString which returns a string. Most classes will "override" the definition of toString() in order to make sure the desired information is returned. We'll see how this is done below with WeeklyEmployee.
Next a new employee is created with a call to the constructor of Employee which takes a String and Date as parameters. The following line prints out a string which is constructed by concatenating 5 strings. Two of the strings result from message sends to emp.
Notice that constructor calls always use the keyword new, while message sends are written with the receiver followed by ".", followed by the name of the message and the parameters. There are a number of points in the syntax above that we have not yet mentioned. Each instance variable, constructor, and method is labeled with information on its visibility outside of the class. Public means that it is accessible everywhere. Because the constructor and methods are all labelled public, they can be accessed in main or indeed in any other class. Protected features are only accessible inside methods of the class or extensions of the class. Thus name and birthDate will not be accessible in main. Information hiding in Java is actually more complicated than this. We'll get into more of the details later.
When a new Employee is created, space is allocated for that object which includes slots to hold the instance variables. Different objects of the same class all have the same number of slots for instance variables, but the values of the instance variables are independent between the objects. Thus we can think of a class as a factory or template used for stamping out different objects which all have the same shape. The keyword "static" used in the definition of main indicates that this routine is not associated with individual instances. Instead there is one copy which is just associated with the class itself. This makes sense for main since it is just used to create new instances and test the class. If a variable were declared to be static then there would just be one copy no matter how many instances of the class existed.
The type void has a single value which is normally ignored. Thus a routine whose return type is void is really a procedure. The parameters of main are used to pick up command-line parameters to the execution of the program, just as in C. We probably won't use them here.
The above class and interface are part of a collection of files which form a program involving different kinds of classes. Note that the method getWkPay() doesn't really do anything useful. The real reason for defining the class Employee is so that we can collect together some common code which will be used in extensions. Here is an example of such an extension:
/** Subclass of Employee for employees paid by the hour. Written January 25, 1997, by Kim Bruce. */ public class HourlyEmployee extends Employee{ // fields protected double hourlySalary; /** hourly wage of employee */ // constructors public HourlyEmployee (String emp_name, Date emp_bday, double hourlyWage) /** Construct hourly employee from name, birthday and hourly pay */ { super(emp_name,emp_bday); // Call constructor from superclass hourlySalary = hourlyWage; } // methods public void setHourlyPay(double new_salary) // post: hourly pay set to new_salary { hourlySalary = new_salary; } public double getWkPay() /** post: Returns weekly pay. */ // Note this overrides method in Employee { return (hourlySalary * 40.0); } /** This time we test with an hourly employee */ public static void main(String args[]){ // Create my birthday. Date kbday = new Date(1948,10,16); // Create new employee HourlyEmployee emp = new HourlyEmployee("Kim Bruce",kbday,5.25); // Print out info on the employee System.out.println("Today, the employee, "+emp.getName()+", is "+emp.getAge() +" years old."); System.out.println("He makes $"+emp.getWkPay()+" per week!"); } }When one class extends another, we often call the original class the superclass and the extending class a subclass. All of the instance variables and methods of the superclass are implicitly "inherited" in the extension. Thus we do not have to rewrite getName() or getAge() as they are automatically inherited. HourlyEmployee "overrides" the method getWkPay() because we wish to compute the weekly pay based on the hourly salary.
Constructors are NOT inherited (and, in fact, they must have the same name as the class, so couldn't usefully be inherited), so we must include a new constructor, HourlyEmployee. The first line of a constructor of a subclass must be a call of the constructor of the superclass. This is written as super(...).
The new main procedure tests out HourlyEmployee.
I have included on-line several other classes which are part of a system using objects representing several type of employees. They are:
Please look at these to see how they can be used together. Note: The version of the Employee class available through the link is an abstract class rather than the concrete class shown above. We will discuss abstract classes in more detail later.Here are a few general points about Java which are relevant to these examples.
A variable declared to be final must clearly be a constant (be sure to initialize it in the declaration or you will never be able to set it). A final method is one that may not be overridden. A final class is one that may not be extended.
In each case, the declaration of an entity to being final provides a guarantee to both the programmer and the compiler that the entity will never be changed again. This may result in a gain of efficiency for the compiler and better understanding for the programmer, but if mis-used can result in a loss of flexibility (the usual binding time trade-offs!).
A constant is almost always declared as public static final ... as it is generally visible, there only needs to be one copy for the entire class (there need not be a separate copy in each object) and it may not be changed.
If class C implements interface IC then objects of type C can be used in any context expecting an object of type IC.
While we haven't shown examples yet, one interface may also extend another. This also results in subtyping.
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.
As above, 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!
e = emp;results in e being a reference to the same value as originally given by emp.
It is worth noting here that constants holding objects aren't necessarily very constant. If e is a constant holding an HourlyEmployee then you may not assign to it, but you may send it messages like setHourlyPay(6.25) which will result in a change to the instance variables of the object. Thus constants do not change their identity, but object constants may change state!
Java does not have explicit pointers, as these are unnecessary. New objects are created by preceding the constructor of a class with the keyword new. Like ML, objects that are no longer accessible are swept up by the garbage collector and recycled.
You can create objects of these types using the fairly obvious constructors: new Integer(3), new Character('z'), etc. You can extract the values with fairly obviously named methods: i.intValue(), abc.charValue(), etc. Objects of these types are immutable. Thus you cannot change the value held inside an object of the wrapper classes. Of course if you have a variable of type Integer, you can create a new value of the type and assign it to replace the old value. Make sure you understand the distinction I am making here!
For those not used to it, this can seem very restrictive, especially since one cannot cheat like in C and pass an explicit pointer. However, one soon adjusts to the style and I suspect you will grow comfortable with it quite quickly.
public class DbleNode{ protected int value; protected DbleNode next, previous; public DbleNode(int value, DbleNode next){ this.value = value; this.next = next; }Let's take a quick time-out before writing the rest of the definition of the class. Notice that within the body of the constructor, DbleNode, the parameters value and next block the visibility of the instance variables value and next. Luckily we can refer to the instance variables by prefixing them with this. The net result is that the two assignment statements assign the parameter values to the corresponding instance variables.
This is a common idiom in Java that allows you to be lazy about thinking up new names for parameters that are different from the instance variables, but close enough to be suggestive of their intended meanings. Let's continue with the definition of the class:
protected void setPrevOnly(DbleNode pred){ previous = pred; // could have written this.previous, but not necessary. } public void setNext(DbleNode newNext){ next = newNext; if (newNext != null) newNext.setPrevOnly(self); } }The last method is interesting in that self is sent as a parameter in a message to newNext. While Java would not prevent me from assigning to newNext's instance variable (a method can access all date - no matter what the protection - of other objects of the same class!), it is considered very bad style to access the instance variables of other objects. Hence my use of the setPrevOnly method.
Thus of the basic data structures or building blocks for data structures, only arrays must be dealt with. Arrays in Java are objects, and hence must be created. The declaration for an array of employees would be written:
protected Employee[] emp;Technically, you can also write the declaration as in C++:
protected Employee emp[];but I prefer the former as it makes it clear what the type is and makes a bit more sense when the declaration also initializes the array:
protected Employee[] emp = new Employee[75];Notice that the declaration of the array does not include the size of the array, only the type of elements that it holds. Thus at various points in the program emp could hold arrays of different sizes. It is only the call to the constructor that determines the size of the array to be created. In particular, then, Java avoids Pascal's problems with passing arrays as parameters since the size of the array is not part of the type.
The programmer does not need to declare a constructor for arrays (in fact it's hard to imagine where one would put such a constructor). Thus, once the Employee class exists it is easy to construct an array of employees. However, one must be sure not to confuse creating an array with creating its elements.
Before the call of the constructor above, the value of emp was
null. After the execution of the constructor,
Multi-dimensional arrays are obtained by creating arrays of arrays. Java
does not provide the same nice abbreviations as Pascal to make you think of
them as multi-dimensional arrays. However, it does provide a bit more
flexibility:
If you read the previous comments about parameter passing in Java, you may
be wondering how you can change the values in an array if the array is
passed as a parameter. Luckily there is no problem. You still can't
assign to an array parameter as a whole (well, actually you can but it
won't have any impact on the original array ...), but you can assign to
individual elements of the array. Thus if emp is a formal
parameter of array type, then emp[3] = newEmployee changes the
entry in slot 3 of the actual parameter, at least if it has a third slot.
By the way, Java, unlike C++, automatically checks array boundaries when
accessing array entries. Arrays do keep track of their length, and you can
get access to the length of an array by writing emp.length. Be
sure not to include parentheses after length as it is an instance
variable rather than a method (bad Java designers, bad!).
Employee[][] squareEmps = new Employee[75][75];
// this builds a 75 by 75 array.
Employee[][] triangleEmps = new Employee[75][];
// this builds an array with 75 rows, each of which is currently empty.
for (index = 0; index < 75; index++)
triangleEmps[index] = new Employee[index+1];
The for loop assigns row index the size of index+1. Thus the 0th row has
size 1 up to the 74th row having size 75. Of course you still need to
initialize the individual elements of the array. You would get access to
an individual element by writing triangleEmps[3][2], for example.
Back to:
kim@cs.williams.edu