CS 334
Programming Languages
Spring 2000

Lecture 16


Object-oriented programming languages

Roots in languages supporting ADT's

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)

Called object-based languages.

Goals:

Stepping back a bit further - support development of high-quality software.

Qualities Desired in Software

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:

  1. It supports objects that are data abstractions (like Modula 2, Ada).

  2. All objects have an associated object type (often called classes). (Bit different from Modula 2, Ada).

  3. Classes may inherit attributes from superclasses. (Very different from Modula 2, Ada)

  4. Computations proceed by sending messages to objects.

  5. Routines may be applied to objects which are variants of those they are designed to be applied to (subtype polymorphism).

  6. Support dynamic method invocation (will be explained later )

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).

Main idea of object-oriented programming:

Independent objects cooperate to perform a computation by sending messages (requests for action) to each other.

Object-oriented programming:

Object-oriented languages built around following concepts:

Object
like "internal representation of abstract data types - all data encapsulated in objects - first class!

Message
request for object to carry out one of its operations.

Class
template for set of objects - similar to type

Instance
object described by a class

Method
operation associated with an object - specified in its class.

Subtype
A subtype of B (A <: B) if A represents a specialization of B (e.g., cars <: vehicles, ColorPoint <: Point) - An element of a subtype can be used in any context in which an element of the supertype would work.

Subclass
An incremental modification or extension of a class and its methods. Methods not changed are inherited.

In more detail:

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.

Java

Java is an extension of C. Originally known as Oak, it was designed to be a platform independent language for quite simple chips, e.g., those designed for your toaster. Later, as people were getting more frustrated with using C++, its target audience shifted to more general purpose computing, and it gained popularity when it provided support for "Applets", programs that could be embedded in web pages. (Sun's HotJava browser was the first to support applets.)

In my opinion, three factors worked toward the sudden popularity of Java:

  1. Web applications.
  2. Use of C syntax.
  3. Growing frustration with C++.
The reasons that should have contributed to the rise of Java:
  1. Clean and (relatively) simple language design
  2. Use of garbage collection
  3. Built-in support for event-driven programming and concurrency.
Java is not, by any means, a perfect language. The main problems include (in my opinion)
  1. Lack of support for parametric polymorphism.
  2. Weak support for modules.
  3. Overly complex.
  4. Use of C syntax
Guy Steele of Sun, in an invited lecture at OOPSLA '98, 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.

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:

  1. Employee
  2. HourlyEmployee
  3. ExemptEmployee
  4. Date
  5. EmployeeTester
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.

The final keyword

A class, method, or instance variable declaration may be prefixed with the keyword final. A feature declared final may not be changed.

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.

Type equivalence and subtypes

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.

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 IBC
then an object from class SC can be used in any context expecting an object of interface IBC.

Dynamic method invocation

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.

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!

Objects are references

As in most purely object-oriented programming languages, objects in Java are held as implicit references. This means that if you test two objects are "==" each other, then the answer will be true iff they are the same object. Similarly, assignment (=) of objects in Java represents sharing, not copying. Thus if emp is an expression whose value is different from null and whose static type extends Employee, and e is a variable of type Employee, then
   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!

... but base types are not

Base types in Java (int, float, char, bool, etc) do not contain objects. These are normal values of the sort you would find in any procedural programming language. Thus assignment with variables of these types has the usual copy semantics and equality tests for the same value rather than the same location in memory.

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.

... though base types can be wrapped up as objects!

Later on we will see that it is sometimes awkward to have base types held differently from object classes. (This has to do with the handling of "generic" container classes, classes like List, Stack, Tree, etc. which can be designed to hold elements of any object type.) As a result, for every base type, Java provides a corresponding object type. They typically start with a capital letter and are spelled out in full. Thus Integer corresponds to int, Boolean to bool, Double to double, Character to char, etc. Note that String is already an object type, so there is no need to wrap it.

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!

Parameter passing

Like most pure object-oriented languages, Java only supports by "call-by-sharing". At the implementation level, this is the same as call-by-value. I.e., the actual parameter value is copied into the formal parameter before executing the body. At the end of the execution of the body, the formal parameter just goes away - no copy back occurs. I prefer to describe it as call-by-sharing because, while you may not replace the value via an assignment in the method body, you may change the state of an object value. In particular, if a parameter evaluates to an object, one can send messages to it which can change its state.

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.

This: the name I call myself

Inside the methods of a class, the keyword can be used to refer to the object executing the method (i.e., the receiver of the message). In particular, one can get access to the instance variables of that object, send messages, or even use this as a parameter in another message send. In fact, every time you access an instance variable in a method by just writing its name, it is treated as an abbreviation for the same name prefixed by this. Here is an example using a class representing a doubly-linked node:
   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.

Data structures in Java

Java does not need records or structures, because we can think of them as just being very stupid objects. (Recall that, unlike C++, Java is only intended to be an object-oriented language. C++ is intended to support several different styles of programming, including its use as a "better C".) Similarly linked structures are obtained by just creating classes with instance variables holding all of the objects you wish to link to.

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, becomes an array with 75 slots (ranging from 0 to 74), each of which contains null(!). If you wish to insert elements into the array, you must create them. Thus one would expect to see some sort of loop which fills each slot with a non-null employee value.

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:

   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.

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!).


Back to:
  • CS 334 home page
  • Kim Bruce's home page
  • CS Department home page
  • kim@cs.williams.edu