Static vs. Dynamic Issues in Object-Oriented Programming Languages

By Antonio Corradi, Letizia Leonardi

07/13/2001

Any decision in the design of both object-oriented programming languages (OOPLs) and their environments must face the choice between static and dynamic issues. The aim of this article is to discuss the implications of the static vs. dynamic choices. Despite the fact that dynamic choices induce runtime costs, the resulting flexibility permits the rapid prototyping of applications and reduces application development time. On the other hand, static strategies may even increase development time in applying early controls to all phases of development steps, but can greatly shorten maintenance and lifetime consistency phases.

This article tries to establish a trade-off between static and dynamic perspectives both to help programmers choose the most convenient OO language/environment for their applications and to help designers introduce static/dynamic property melding into their project developments. Well-known OOPLs are used to exemplify static and dynamic propertiesthe chosen languages are Smalltalk-80, Eiffel, C++, and Java. Their selection stems from their relevance within the OO community.

While OOPLs have acquired wide acceptance, there are still issues under discussion.16 One of them is the relationship between two contrasting viewpoints intrinsic to OOPLs and environments: the static perspective and the dynamic perspective. Static properties stem from any choice made at development time, i.e., before program execution, while dynamic aspects depend on choices and options that can be validated only at runtime, i.e., during program execution. On the one hand, one can assert that dynamic properties favor a perspective more oriented toward rapid application development7; on the other, static characteristics, associated with intrinsic control, enable the detection of problems in the early development phases of programs. We intend to identify the static/dynamic aspects present in OO programming environments.

Typically, OO programming systems distinguish two different levels: objects and classes.8 Thus, static and dynamic properties must be explored at both levels. The object level is the one of normal execution; at this level, objects execute and communicate by message passing. At the object level, static and dynamic aspects can be found in any significant object action, i.e., object creation, client/server binding, and operation dispatching. The class level represents a metalevel, because classes contain the description of the entities' behavior (see instances in Fig. 1) at the object level. At the class level, static and dynamic aspects influence the instance behavior description and the relationship between objects and their classes.

We expect that this article will reinforce the idea that flexibility resulting
from dynamic strategy is a valuable property even with the disadvantages of
runtime cost, while static policies introduce strict constraints but can help
in the development/maintenance steps. The intention is to explore the area between
the two extremesfrom extremely loose policies with controls delayed at
runtime, to rigid static strategies that require a complete set of decisions
from users before execution.

The goal is to evaluate a frontier between the static and dynamic strategies where new OOPLs and extensions to old ones can collocate themselves, keeping an eye on the new proposals connected to distributed systems. This analysis can help programmers and designers to make the right choices.

After a brief overview of the basic characteristics of OOPLs, we consider the static/dynamic aspects both at the object level and at the class level. Several examples expressed in different OOPLsSmalltalk-80, Java, Eiffel, and C++help in the explanation of the presented concepts. These programming languages have been chosen for their relevance in the OO community and because they represent particular perspectives in the wide range of possibilities.

OOPLs
Several programming languages are considered to be object oriented. In particular, we refer to those that satisfy the Wegner's properties3:

object-oriented = objects + classes + class inheritance

We consider a programming language object oriented if it provides the following features (see Fig. 1):

Objects are data abstractions that encapsulate two parts: state and operations. From the outside of objects, only their interfaces are visible; the interface may be either a subset or the whole operation set.

Any object must be created from a specific class, following the abstract data type approach.9

Inheritance defines relationships among classes.

One object, O1, in need of an operation defined by another object, O2, has no choice apart from invoking the O2 interface. In OO terminology, one expresses this behavior by saying that the computation is communication-driven: The object O1 sends a message to the object O2. During this communication, O1 plays the role of the client and O2 of the server that provides the operation. The OO client/server relationship is limited to this request and does not identify a rigid role division (as rigid client/server architectures do10).

By assuming the properties listed, traditional programming languages, such as Ada,11 cannot be considered object oriented. Instead, hybrid OO languages, such as C++,12 which is derived from the traditional programming language C, get the OO title. On the other front, we consider Smalltalk-80,13 Eiffel,14 and Java15 to be typical OO programming languages.

Motivations for the chosen OO languages
We use Smalltalk-80, Java, Eiffel, and C++ as examples of OO programming languages.16 These particular OOPLs have been chosen because:

Smalltalk-80 defines a programming environment designed completely around the object concept. The complete uniformity of Smalltalk-80 imposes this language as a milestone that cannot be neglected when dealing with OO technology.13

Eiffel is an OO language with a Pascal-like syntax where almost all entities are objects. Moreover, Eiffel is an important attempt to introduce the formal properties of abstract data types, together with class specifications, by enforcing the use of assertions and invariants. Its (more significant) goal is the correctness of abstract data type definitions.14,17

C++ is a hybrid language that extends C. The object features are mixed with traditional data structures and functions. C++ tries to embody OO concepts in a traditional language without the unifying approach of Eiffel. The result is sometimes too complex to be tamed. However, the C community has adopted it, which gives more momentum to this language.12,18

Java has been introduced with the goal of achieving the same diffusion as C++ by overcoming its major problems and difficulties. While Java semantics are inspired by Smalltalk, its syntactic flavor is closest to C++. Most troublesome points of C++ have been reconsidered and sources of difficulties have been ruled out.15,19 In addition, Java addresses some of the basic problems of object-distributed systems.

A few uniform terms
Since there is no complete uniformity, even in the dictionary of terms used in the four programming languages considered, we use the following general unifying terms:

State part consists of state variables that indicate the encapsulated (and therefore normally invisible from the outside) information of one object. Smalltalk-80 calls them instance variables. Eiffel uses the term attributes. C++ defines the public, private, and protected parts of objects, and variables can be in any one of the three parts (all the variables declared in the nonpublic parts are invisible from the outside). Java uses the same terms as C++ with a slightly different semantics.*

Object operations refers to the operations an object offers in its interface to manipulate its internal state, which can be invoked from the outside. Smalltalk-80 and Java call these operations instance methods. In Eiffel they are routines, and in C++, member functions.

The chosen OO languages adopt a different approach to type control for both variables and operation parameters: Smalltalk-80 does not enforce any static type checking while the others do.

Two levels of analysis
As usual in OOPLs/OO environments, two different levels can be considered: the object level and the class level.20 At the object level, two concepts are relevant: object existence and communication. The significant actions for static/dynamic properties are creation of objects, client/server binding, and operation dispatching.

Object creation is the mechanism to create an object from its class: An object must always be generated from a specified class that dictates the object's behavior. That establishes a long-life relationship between any object and its class, usually called instance-of (see Fig. 1). This is the reason why objects are also called instances.

Communication determines two aspects: the decision to bind the client object to a specific server object and, successively, the server decision to dispatch the action for the requested operation. Client/server binding is established between two objects for the duration of one communication. The client (in need of a service from another object) becomes bound to the server that currently offers the needed service. Operation dispatching is the connection between the server object and the requested operation. When a server receives (from a client) a request for a given operation, it must determine the action to execute.

The specification of behavior of all instances of one class is at the class level. This behavior describes both the state and operations of its instances. At the class level, the aspects more related to the static vs. dynamic choices are the search for the instance operation code to be executed, the possible modification of the instance behavior by changing its classes, and the definition of new classes. The search for the code to execute is the mechanism that finds the suitable instance operation code among all of those available. The changes and new definitions of classes determine the capacity of an OO environment to accommodate modified and new requirements.

STATIC/DYNAMIC ISSUES AT THE OBJECT LEVEL
Object creation
The creation of objects is the starting action in any OO language scenario. In general, the creation does not associate a name (i.e., an identifier) with the newly created object. The lack of static names for objects is a property that distinguishes objects from modules of other languages such as Modula-221 and Ada.11 In Ada, for example, even the instances created from a generic module receive a static user-defined name. A generic instantiation is only a static mechanism that obliges the user to explicitly handle module names.

Therefore, even if different OO languages adopt different semantics (as we show), objects do not own any intrinsic name, but are identified via container variables. Different systems can build even complex name services upon these basic mechanisms.10
In OO systems, object destruction is as important as object creation. Despite that, destruction does not have a general solution at the language level. Some OO systems provide garbage collection mechanisms to reclaim memory when objects are no longer used. Others furnish implicit/explicit object-deletion mechanisms. In the latter case, a methodology must avoid the use of incorrect references (such as dangling references).

In object creation, before concentrating on the object's static/dynamic properties, several issues must be dealt with including:

Object creation can return either a reference to the created object or the object itself (see the next subsection).

Object creation can be performed either in an explicit or implicit way (see Explicit/implicit object creation subsection).

By-reference and by-value semantics. In OO systems, variables present by-reference semantics. This means that variables contain object references (also called object pointers), and are distinguished from the objects themselves22there might be several different variables that refer to the same object. In a more traditional perspective, variables are value containers (by-value semantics), i.e., variables are the objects themselves.

In the first case, the creation action returns a reference to the newly created object, while in the second case, the creation returns the object itself. In both cases, the creator object can maintain either the reference or the object in a variable and pass it around as a parameter in communications. The by-reference case is normally considered more OO than the by-value case because it does not introduce constraints on objects. In the by-reference case, objects are self-contained and not nested. This choice influences not only object creation but also variable assignment and parameter passing, as shown in the Type conformance subsection.

So the first difference among the considered OO languages stems from the semantics of variables:

In Smalltalk-80 and Java, all variables have by-reference semantics23 according to the uniform perspective; each entity is an object. Smalltalk is untyped, while Java adopts the class to which the variable belongs as a type indication.

In Eiffel, variables declared by using class types have by-reference semantics; by-value semantics can be forced by using the expanded keyword in variable declarations.14

In C++, variables are containers of values and thus have by-value semantics; only pointer variables can obtain by-reference semantics.

In one sentence, C++ adopts a more traditional point of view than the other considered OO languages. In fact, one C++ by-value variable (that contains an object) is similar to one C struct variable.

Explicit/implicit object creation. Object creation can either require an explicit invocation of an ad hoc primitive or be implicitly caused by the definition of variables.

The examined OO languages also differ in the expression of object creation. Let us suppose we have a class named STACK. The four considered OO languages create an object from this class as follows (see Fig. 2):

a. Smalltalk

Sref <- STACK new: stackSize.
"Sref is a Smalltalk global variable because its name begins with a capital letter"

The statement in line 1 explicitly creates a Smalltalk-80 instance of the class STACK. Its reference is saved in the variable Sref, which does not need any definition because of its untyped nature. Any class in the Smalltalk-80 environment inherits the primitive new operation from a system-defined class (Behavior). The class STACK offers the operation new: called in line 1 with the constant stackSize. This operation should invoke the primitive new for instance creation.

After the statement in line 4, the variable Sref refers to the explicitly created Eiffel object (instance of the class STACK) while the make operation completes the specific initialization. The definition of the variable Svalue, implicitly creates an instance of the class STACK because of the expanded keyword.

The definition of Svalue in line 7 implicitly creates a C++ instance of the class STACK. The language support automatically calls the class constructor to initialize the created instance. The definition of the object pointer Sptr is not connected to any object creation. The creation requires the explicit allocation of the memory for the referred by Sprt C++ object (via the new operator, see line 11). Again the STACK constructor is invoked.

Sref in Java is a by-reference variable of class STACK: The user must explicitly invoke the object creation as in line 13. Available constructors are invoked.

In Smalltalk-80, variables do not have a type or declaration. In the preceding example, the (global) variable Sref does not need any declaration before being used. The first time Sref is used, the space for the object reference is reserved. After the assignment in line 1, the variable Sref refers to the newly created object of class STACK. The same variable Sref can contain references to different instances of any class at different times (see Polymorphism subsection for more details). Any use of Sref in the left-hand side of an assignment modifies only the reference variable and not the object referred to. At the same time, many other by-reference variables can refer to the same object.

Eiffel, Java, and C++ are, instead, typed languages, and enlarge the language type space with defined class names. All three of these languages assume a strict correspondence between classes and types (for a discussion on this topic, see "A Behavioral Approach to Subtyping in Object-Oriented Programming Languages"24). Despite the way in which these languages are typed, they approach creation differently according to their variable semantics. Eiffel and Java separate the necessary declaration of the variable Sref, which uses by-reference semantics, from the creation step: Only after the explicit creationstatements in lines 4 and 13does the variable Sref refer to a newly created STACK instance.

C++, by deriving its class construct from the C struct, associates creation with the definition in line 7 of the variable Svalue, using by-value semantics by default.

By-value semantics can constrain object lifetime: Containing and contained objects' creation/destruction are closely tied, and, normally, should be more or less explicitly handled by the programmer (see next section).

Let us consider that apart from default policies, there are variations on the theme. Recent versions of Eiffel have introduced by-value variables with implicit creation, as in line 5, for optimization purposes. C++ provides an explicit creation, separated from declaration, when the variable involved is a C pointer, as in line 11. Java rules out any possibility of mixing approaches and follows a unique clean strategy similar to Smalltalk-80's.

Creation and object lifetime. After considering these issues, we can better consider whether object creation can be considered static and/or dynamic. To start we introduce a lateral but related issue: persistency. With persistency we indicate the possibility of preserving the state of some objects between different working sessions. A programming environment generally achieves its persistency based on a robust disk support. Normally, initial objects are persistent and their state is retrieved at the beginning of the session. In the following, we neglect all persistency issues. Initial objects can be considered to be automatically created at the initialization time (in other words, before the user takes control). These objects have the lifetime of the whole session, and they make possible the management of and interaction with the whole runtime support. A typical example is the Smalltalk-80 SystemBrowser,25 which the user can employ to examine all the classes in the environment.

Object creation is always a dynamic operation in Smalltalk-80, Java, and Eiffel. Objects are created as needed during the execution and have a lifetime that depends on their references. One object lives until there are references for it to prevent the dangling-reference problem. In any other case, it is ready to be destroyed because nothing can reach it. Differently from explicit creation, the system garbage collector destroys objects implicitly to re-collect the memory. In Java, the user can also express a finalize operation to contain the last-will actions.

During the development of an Eiffel application, the user of Language for Assembling Classes in Eiffel (LACE) can specify a class as the root class.14 The language support automatically creates one instance from this specified root class and executes its initial routine, if it exists.17 Therefore, the initial object is implicitly created as the unique instance of the root class, and it drives the application and determines its lifetime.

In C++, dynamic object creation derives from by-reference pointer variables. The programmer should explicitly use the new operator and, because there is no garbage collection, should explicitly destroy objects with the delete operator. In addition, C++ presents static and dynamic object creation due to by-value variables and scope rules. The next section discusses these peculiar aspects.18

Lexical scope rules of C++. Among the considered OO languages, C++ maintains the traditional scope rules of structured languages, derived from C. This choice is motivated by the aim of accompanying programmers in the transition from traditional applications to less conventional ones. The by-value, class-typed variables do not refer to objects but are objects, and that has important implications on the object state and lifetime.

C++ allows one object to enclose directly nested objects whose existence is tied to the containing objects (determining lexical scopes)26; in other words, the nesting relationship constrains object lifetime. A nested object should be automatically created when its container is created, and analogously destroyed. C++ by-value variables and their corresponding objects have a lifetime determined by the scope of the variables themselves.

Therefore, C++ objects identified by global by-value variables are statically created and have the same lifetime as the whole application. The use of automatic by-value variables in C-like blocks introduces an additional dynamic object creation: Differently from the by-reference pointer variables, both allocation and deallocation are implicit actions. In the following C++ example, the automatic variable dynVar of type T is defined in the function Foo. If T is a class, one instance of T is dynamically created (i.e., allocated) in the support stack when the Foo invocation begins. It is also initialized using the T constructor (if present). At the completion of Foo, all automatic variables are deallocated from the support stack and so is the object dynVar, after the execution of the T destructor (if present).

Eiffel also introduces the definition of nested objects to build composite objects (expanded keyword),14 but they are always implicitly destroyed by the system garbage collector.

Client-server binding
Communication in OO systems connects objects by defining their role in a client/server
relationship
(see Fig. 3). To establish a communication, the client object has to know
the server22 in two ways:

The client has a reference to the server, in case of by-reference semantics,
in Smalltalk-80, Java, Eiffel, and in C++ when using pointers.

The client owns the server object directly in itself, in C++ and in Eiffel
with by-value variables.

OO programming systems define a set of entities all objects know to provide a default visibility. In Smalltalk-80, a set of objects is globally visible (an example is the SystemBrowser object). Apart from default knowledge, one object acquires visibility of the objects it creates and that have been passed as parameters in object operations.

All examined OO languages permit assignments to by-reference variables. The variable can change in the sense of referring to different objects at different times.§

Let us suppose that a client object, at a given time, knows a server object, O1, via its by-reference state variable, Vref. After an assignment to Vref, the client object refers to a different object, O2, with the same variable Vref. The binding with the current server has to be dynamically solved.27

As an example extracted from one simulation application, the class CAR defines two instance variables, streetref (which identifies the current street the car is moving along) and position (which expresses the relative position in the street). In the following, we give the (partial) Eiffel code of this class; the same could be done in all chosen OOPLs. In particular, this simulation has been developed by using the parallel OO language/environment PO.28 Any instance of this class owns a reference to one street object in the variable streetref. Since each CAR instance advances in its simulated path, streetref can refer to different STREET objects, via assignment (see line 13).

1. Class CAR
2. feature {NONE} - invisible features
3. streetref: Street; position: REAL; ...
4. - streetref refers to the street of current transit of the car
5. - position is the current position of the car in the street
6.
7. feature - visible features
8. simulstep () is - a step in the simulation
9. do
10. if streetref.end (position)
11. - when the car reaches the end of the street, it refers to
12. - the new street and updates its position in the new street
13 then streetref = streetref.newstreet();
14. position = streetref.initposition();
15. end;
16. - in any case, one move in the current street
17. position = streetref.go (position);
18. end; - simulstep
19.
20. .....
21. end - CAR

The binding between one CAR instance (the client) and its current STREET instance (its current server) must be solved at runtime; i.e., streetref cannot be bound to the referred object before the execution, but its binding is dynamic. The same dynamic binding also occurs in Smalltalk-80, Java, and C++. In all cases, the flexibility resulting from the possibility of changing the binding implies the need for server identification at runtime.

There are also cases suitable for static client/server binding. C++ and Java define constant by-reference variables (const and final keywords respectively): An identifier Constref always refers to a given server. When the client asks for one service by using Constref, the binding can be statically established. This static binding can become a great advantage to reduce overhead in case of repeated invocations . Note that static analysis tools can transform dynamic bindings into static ones to improve performance.29

In addition, for C++ and Eiffel by-value variables, the binding is immutablevariables contain objects directly. Any assignment to a by-value variable means to copy object contents (see following subsection).

Type conformance. In a typed programming language, assignment and parameter passing are ruled by the type system. In a strongly typed programming language, one variable Vref of type T can identify only entities of the same type T. However, the type equivalence in typed OOPLs is generally relaxed and replaced by the notion of type conformance.14 The type-checking mechanism considers type compatibility, whose rules derive from subtyping relationships and generally from class inheritance.#

Type conformance establishes that a variable of type T can accept not only values of the same class/type T, as in any strongly typed language, but also values of any subclass/subtype of T.

The conformance rule introduces another static/dynamic aspect in typed OOPLs. In the preceding example, the variable Vref has static type T, and, after the assignment in line 17, a different dynamic type, T1 (subtype of T). This is possible because T1 is a subclass of T, so T1 conforms to T. The conformance rule can be justified by the fact that, T1 being a subtype of T, each instance of T1 represents a specialization of a T instance. Each operation invoked on Vref can be legally invoked if it refers to a T1 instance. The following section discusses what impact the type conformance rule has on the behavior of Vref (i.e., on its polymorphism).

The conformance rule applies to by-reference variables, but not to by-value variables of C++ and Eiffel. Thus, the following C++ code sequence with by-value variables does not work in the same way as the corresponding Eiffel one.

C++ adopts the same by-copy rule as C in parameter passing: When an object (maintained in a by-value variable) is passed as a parameter, the server object obtains one copy of it (a copy constructor is used). As an additional possibility, C++ implements by-reference parameter passing by using the reference mechanism.

Let us note that any OO language with only by-reference semantics must provide operations to copy objects. In Smalltalk-80, for example, the statement:

Vref1 <- Vref2 shallowcopy.

creates a copy of the object referred to by the Vref2 variable and the Vref1 variable refers to it. Vref1 and Vref2 refer to objects that, even of identical in content, are not the same object. For a discussion of the shallow and deep copy semantics see "The Role of Opaque Types to Build Abstractions."31

From an abstract point of view, conformance applies only if the class/type system is monotonic, where subclasses can only add behavior to superclasses, without obscuring any inherited property. However, most OO languages may alter monotonicity with combinations of visibility rules and inheritance. Monotonicity is too strict a rule to be rigidly followed in general-purpose applications. For example, C++ provides private subclassing and, when one class, C1, derives via private subclassing from a class, C0, the inherited public part of C0 becomes private in C1 and no longer visible from the outside. This causes nonmonotonic inheritance and type conformance no longer applies: The assignment in line 11 of the immediately preceding C++ code example becomes illegal.

In Smalltalk-80, as in any typeless language, all assignments are considered legal because all entities are objects and all variables refer to objects. In other terms, the variable conformance rule is always guaranteed. Consider the following Smalltalk-80 code||:

The variable Sref initially refers to a STACK instance, and, after the assignment in line 3, refers to an integer value. Of course, the Smalltalk-80 environment checks runtime errors. In the example, an error (doesNotUnderstand: push:) is raised when one request of a push: operation reaches the number 10 referred by Sref (see Search for the code to execute subsection for details). The checks induce runtime overhead that typed OOPLs avoid a priori, but pay for with a consequent loss of flexibility.

With respect to typed OO languages, Java follows the conformance rule, but deserves further explanation. The approach to conformance is based on two different coexisting type signatures: classes and interfaces. A class represents the concrete implementation of an abstract data type, while an interface represents its abstract counterpart; the two different concepts define a precise separation of concern. Classes can inherit from one another in a strict single-parent relationship. In contrast, interfaces also recognize multiple inheritance.

The point of contact between the two inheritance hierarchies is that classes can answer to any number of interfaces and provide all of their instances with type conformance to all the implemented interfaces. Any object, created from a class, can derive from the class its capability of answering to a number of interfaces.

Any variable can be tagged either with a class name or with an interface name (but no instance can be created from an interface). Conformance stems from the analysis of the inheritance either among classes or among interfaces. Let us consider the following sketched Java example:

The iref1 variable can contain only references to instances of classes that conform to the interface I1. All classes that inherit from T1 are capable of honoring the interface itself (see line 13). An interface value can be assigned only to a conformant variable (see line 18). There is no way of assigning an interface value to typed variables, even of a conformant class type. Therefore, all the following two assignments are illegal:

tref1 = iref1; tref2 = iref1; // illegal assignments

Server operation dispatching
Server operation dispatching is the action that establishes the binding between a server and the requested object operation.** When a variable, Vref, refers to an object, O1, the operation requested via Vref depends on the current server object O1. In any typed OO language, the operation should depend on the type of the variable Vref.32

These aspects determine two different policies for the decision of binding the server object to the requested operation.33 If the decision is taken before the execution, the binding is called early. For typed OO languages, the operation is dispatched only on the basis of the static type/class of the by-reference variable. If the decision is taken at runtime, the term late binding is used to imply that the selected operation depends on the current server object. In typed OO languages, this means that the executed operation is dispatched on the basis of the dynamic type of the by-reference variable.

Smalltalk adopts a late-binding perspective and dynamic server operation dispatching. C++, Eiffel, and Java can use either early or late binding.

Early binding. C++ derives from C and is oriented toward early binding for the sake of efficiency. When a C++ application is compiled, the server bindings of all C++ by-reference variables are solved. Therefore, the operations to execute are statically decided and bound independently of currently referred objects. By default, even if C++ recognizes both a static and dynamic type for pointer variables, operations are dispatched on the basis of their static type. This is true even in the case of operation overriding. The following C++ code is an example:

The statements in lines 25 and 32 always execute the draw operation of the Polygon class, but line 32 executes for the instance of Rectangle created in line 27.

Late binding. C++ provides late binding only for overridden operations.
In the preceding example, the draw operation must
be declared virtual in the base class Polygon to
force dynamic binding. The virtual property is automatically inherited
by all derived subclasses, which may also override virtual operations with their
own implementations. Therefore, C++ defaults to early (static) binding and permits
late binding via virtual definitions.

In this case, the statement in line 28 executes the draw operation of the Rectangle class because of the assignment in line 27 and because draw is a virtual operation. This example points out that the same statement (in lines 24 and 28) exhibits different behavior depending on the dynamic type/class of the object referred to by the variable Pptr.

Though Eiffel and Java have typing properties similar to C++, they always adopt a dynamic perspective in case of overriding: The server operation dispatching is based on late binding.

Eiffel requires one to explicitly announce the overriding of operations: A subclass must tag an operation defined in a superclass with the redefine clause to be overridden. Let us note that another reason to motivate static analysis tools is the need to transform late bindings into early ones for performance's sake.29 In Eiffel, for example, an optimizer can be used to achieve early binding.17

In Java, all operations can be overridden with one exception. An operation can be declared final, renouncing to the possibility of changing its behavior by inheritance. In other words, a final declaration makes the entity immutable for all direct or indirect subclasses. In the case of final, private, and static methods, early binding is a likely choice and can even result in a significant optimization.

Polymorphism. The possibility of having different behavior for the same request at different times is usually known as polymorphism.17 We distinguish polymorphism due to inheritancevertical polymorphismfrom horizontal polymorphism, present in typeless OO languages such as Smalltalk-80.34 The vertical attribute derives from the pictorial representation of classes, where subclasses are lower than their corresponding superclasses; the polymorphic behavior is due to a class hierarchy. The horizontal attribute conveys the idea that polymorphic behavior can be anywhere within the class graph, i.e., contained in different classes not tied by any inheritance relationship.

As we have shown, C++ achieves vertical polymorphism only with by-reference (pointer) variables and virtual operation definitions. In fact, the designer of the base class defines the possibility of operation polymorphism only by using virtual declarations. In Eiffel and Java, the point of view is incremental: One class is completely unaware of the fact that subclasses can override operations and that they are responsible for the explicit redefining.

In Eiffel, Java, and C++, type checking for operations is always statically performed based on the static type of variables. Let us consider the C++ example of the Polygon and Rectangle classes with virtual functions. A compile-time error is raised because the diagonal() operation is requested via a variable whose (static) type does not offer it, even if the dynamic type of Pptr is correct (Rectangle):

Pptr = Rptr; // legal assignment
Pptr -> diagonal();
/*COMPILE-TIME ERROR: Pptr refers to a Rectangle instance, but
it is of (static) type Polygon and this class does not define the
diagonal operation in its public part*/

Smalltalk-80 always adopts a late binding perspective for operation dispatching. For example, let us suppose that Vref refers to the object O1.

Vref draw.

is a request for the operation draw to the object O1 referred by the Vref variable. The executed draw operation depends on the O1 class. If O1 is a Rectangle instance, the effect of this operation is the drawing of the specific rectangle on the screen. After an assignment to Vref, Vref can refer to an object O2 of a different class; the previous request will produce another effect. For example, if O2 is a Car instance, the draw operation (if it exists) produces a different effect.

Classes such as Rectangle and Car are unlikely to be related by inheritance. "Vref draw." can produce different behavior at different times due to Smalltalk-80's horizontal polymorphism.34 This can be highly valuable in a rapid prototyping environment but is less suitable for a development cycle where correctness is stressed.

The two different metalevel hierarchies of Java permit both kinds of polymorphism. Vertical polymorphism stems from single-parent class inheritance and horizontal is available to expert users through defining classes that implement several interfaces, as the following simple Java example shows:

The iref1 variable can refer only to instances of classes conformant to the I1 interface. Several classes, even classes totally unrelated to one another (such as T1 and T2 in the preceding example), can be accepted as long as they implement the correct interface. Therefore, the same request on iref1 can produce different effects depending on the dynamic class of the referred object. Classes implementing several interfaces determine as many subsets of conformance as needed.

Additional issues
Another peculiar source of dynamicity is offered by the possibility of deciding, even deciding dynamically, the identity of the requested operation. This kind of dynamicity is usual in LISP-based environments and can become fruitful to express very dynamic behavior.35

Smalltalk-80 gives this possibility with the perform: family of primitives. These primitives allow one to request an object to perform an operation whose name is given as a parameternot only the server but also the name of the requested operation is dynamically computed.

Let us consider the following example, where Sref is a variable that refers currently to one RECTANGLE object:

The perform: feature introduces metalevel parameterization. Not only the receiving server and the invocation parameters are decided at runtime, but also the name of the invoked operation. Java provides the same possibility with the invoke operation available in support.36

In addition, other systems37,38 can base operation dispatching not only on the receiving object, but also on the types of the parameters. Again, these useful features have been neglected in traditional OO systems for ease of use.

As a last comment, let us note that we have excluded from this analysis the possibility of dynamic operation grouping generally called method combination offered by Lisp-based OO proposals32,35,39 in which one invocation on one object could produce the execution of a collection of operations defined in its class and superclasses.

All of these features introduce other dimensions of dynamicity and code reusability.

STATIC/DYNAMIC ISSUES AT THE CLASS LEVEL
Classes specify and contain object behavior. A class describes the representation (we have called the state) and the code of the operations available on its instances.

We need a few initial considerations to understand the static/dynamic aspects
present at this level. In general, one should deal with an OO system by considering
not only its language but must also examine the integrated environment. For
example, Smalltalk-80 is a complete programming environment oriented toward
rapid prototyping and uniformly built around the unique concept of object (see
Fig. 5).25 This has a deep impact on
the class role. All Smalltalk-80 classes are objects in their turn (see Fig.
6) and this implies13:

the requirement on classes to furnish an operation of creation, and

a further descriptive level: the metaclass level.

Therefore, instance behavior descriptions are both present in the first development phases and available at runtime. Objects are created from one class and are connected with their class for their whole lifetime by a permanent relationship similar to an umbilical cord (see Fig. 6).

Java follows the Smalltalk-80 example. Any Java class is maintained in a separated, named file. Java dynamically loads classes as needed from the file system (and from the network). At this point, the classes can be considered objects under any circumstances. The uniform perspective completes the expressive capacity of an OO language and its environment. In this case, classes are first-class entities that can be passed as parameters and obtained as results.

Even if all considered OO languages maintain a tight instance-of relationship, there are rapid application development (RAD) environments where this relationship can dynamically change. To connect objects to different classes at different times can deeply impact on programming. We do not consider this source of dynamicity.

Search for the code to execute
In the Server operation dispatching subsection, we described operation dispatching at the server site. The requested operation should belong to the interface of the server instancepart is locally defined in its class, part can derive from direct superclasses and from farther ancestors. This section shows where dispatched operations are stored and then searched.

All OO languages presented adopt the same approach with regard to operation codes. The instantiation of an object implies the need to allocate room for its state, while the operation codes are maintained in its class (see Fig. 7). When an operation is requested to an object, its operation code is searched in its class by following the implicit instance-of link. The implementation of the inheritance graph does not imply any replication of the object operation codes.40 There is only one copy of an operation, even if it is inherited several times.

The static type checking of typed OO languages such as Eiffel, Java, and C++ guarantees the presence of requested operations at runtime. Smalltalk-80 variables are not subjected to any static check, and the operation code search can fail during execution. If no suitable operation is found, the user is notified with a dynamically generated error (the error is doesNotUnderstand:...).

All OO languages considered implement late binding, but not all with a dynamic
search for the operation code in the inheritance graph. In Smalltalk-80, the
dynamic search is based on the presence of classes as objects at runtime. The
search for the corresponding operation code starts from the class of the object
that received the request. If the code is not found in its class, this search
goes up to the superclasses and so on, until the root of the inheritance tree
is reached (the class Object). This approach produces
high runtime costs because it can recursively explore a large number of superclasses
(see Fig. 7). Java uses a similar point of view, compatible with variable typing
and conformance.

To optimize the operation search, of course, caching strategies can be used in any OO language support.41 The adoption of these techniques may clash with the possibility of changing the class behavior.

For their search, C++ and Eiffel adopt a solution that avoids dynamic searches. The language support creates internal class tables (called class descriptors in Eiffel) that at runtime maintain pointers to all operation codes (only for virtual operations for C++). By using those tables, the support is capable of directly dispatching the operation code on the base of the dynamic type of the by-reference variables (see Fig. 8). Of course, in case of operation overriding, the support dispatches the most specific operation code.

When the target is a distributed architecture, the implementation of the inheritance
graph should take into account distribution and locality. An important requirement
is to avoid operation dispatching that spans over several sites. The usual solution
is to replicate in any site the operation codes of all classes in use by locally
maintaining an inheritance graph, similar to the preceding explored solutions.42
But other new solutions can be designed.43

Changes and new definitions of classes
The RAD requirement suggested the possibility of changing instance behavior while executing to adapt to the evolution of the application and, in particular, to the need of adding new behavior and expressing new functionalities. This introduced the feature of dynamically adding behavior and of modifying available classes in their descriptive part.

Typed OO programming languages (such as Eiffel and C++) introduce the notion of class/type as a static descriptive entity. Apart from a few exceptions, the information described in a class is used up in the first phases of software development and not available at runtime. Notable exceptions are the internal class tablesthey are, in effect, a first step toward the presence of classes at runtime. Nevertheless, Eiffel and C++ do not consider classes first-class entities; classes cannot be used as objects and changed at runtime, as Smalltalk-80 classes can.

Moreover, only the assumption of classes as first-class entities makes possible instance behavior dynamic modification to satisfy changed requirements. Classes, being objects, play two roles: They are objects in themselves (i.e., they contain their class state; class operations are in their metaclasses), and they contain the description of their instances. In Smalltalk-80, the instance description is part of the class state, and class operations can act on it. Therefore, classes may change the instance description. All instances created after a change can only refer to the new behavior; the change is also propagated to already created instances. Examples of nondisruptive changes are either the addition or deletion of instance operations or their modificationsthese do not imply any intervention on the memory of already created instances. Behavior changes might sometimes produce severe inconsistencies. A possible disruptive change is the addition or deletion of instance variables. In particular, the addition of instance variables would imply an enlargement of the memory of old instances.

Some reduced OO environments, such as Smalltalk-V,44 decide not to allow any change in the instance description of a class, C1, as soon as the first instance has been created from C1. Other OO environments, more oriented toward office automation, instead introduce class versioning45 to deal with class changes.

Java considers classes to be first-class objects, allowing them to be passed as in/out parameters, but no modification is possible at runtime. Even if the Java support can load classes at runtime, once loaded, the class code cannot be replaced. A distributed application in Java might have many versions of the same class on different nodes in a network system, but Java does not support any automatic enforcement of consistency.

As the last remark on the change issue, we recall that classes are related by inheritance: Changes at the level of instance operations are automatically inherited by all subclasses. The number of instances involved in such a change increases with the deepness of the inheritance graphnot only direct instances of the updated class, but also any instance of any subclass. In the case of OO distributed systems, this change modifies only the local class copy. To guarantee system consistency, the OO distributed system environment should propagate the change to all class copies.

The extreme class dynamicity of Smalltalk-80 can be better explained considering its software development cycle. In fact, its cycle does not follow the traditional separated phases (editing, compilation, linking, and execution) proper in Eiffel and C++. Smalltalk-80 phases are reduced essentially to editing and execution with the possibility of intermixing.25 Therefore, another dimension of dynamicity derives from the nonexistence of a boundary between development and execution phases. This makes it possible not only to change the definition of the available classes (as we have discussed), but also to define new classes at runtime in a complete RAD perspective. Let us note that the absolute lack of boundary between the static phases of development and the dynamic execution makes the system extremely flexible but, at the same time, can introduce runtime errors. This possibility is not present in Eiffel and C++ due to a more static development model.

Java presents a development model similar to those of Eiffel and C++, but its dynamic class loading makes it possible to incrementally enlarge the number of available classes during the execution, a feature similar to the Smalltalk-80 environment.

CONCLUSION
This article has explored the static and dynamic aspects of OOPLs by using a few significant representatives: Smalltalk-80, Java, Eiffel and C++ (see Table 1). Apart from the obvious consideration that a static approach stresses more correctness while a dynamic point of view suits more rapid-prototyping requirements, we have achieved interesting insights.

Even typed OOPLs should not neglect dynamic properties (and corresponding runtime overhead) for object creation, client/server binding, and server operation dispatching, as Eiffel, Java, and C++ do. The importance of dynamicity in OO environments is recognized by the extension of the basic Wegner's properties (objects, classes, inheritance) with polymorphism and dynamic binding.46 Nevertheless, static features, as in C++ and Eiffel, are likely to diminish the runtime overhead.

However, the static perspective tends to determine a rigid programming development cycle that misses the notion of an integrated OO programming environment. Only such a programming environment could recognize classes as objects and permit class modifications by taking care of global consistency and by allowing incremental definition of new classes, as in Smalltalk-80. The Java programming environment (despite its typed perspective) may obtain something similar to Smalltalk-80.

A possible trend in the design of OOPLs and their integrated environments is to make static and dynamic features coexist and to balance their effects depending on user requirements (optimized execution vs. rapid prototyping).

FOOTNOTES* This article does not cover all the details of the chosen OO languages and environments; for example, entity visibility and object persistency are only touched upon.

 Of course, both languages avoid the infinite reference problem by introducing primitive values. In any case, some predefined classes that describe primitive types make the programmer point of view completely uniform.

The duration of the client/server relationship is the same for all the considered languages: The client waits for operation completion by the server. Other OO proposals release synchronicity and acquire greater dynamicity.10

§ By-reference variables can even refer to objects of different types (see Type conformance subsection).

# Let us recall that we assume a direct correspondence between classes and types.30

|| For Smalltalk-80 classes we use the report form that comes from the file-out environment command.