Documente Academic
Documente Profesional
Documente Cultură
Chapter 1
A Revisionist History of
Programming
or Modeling Reality with Finite, Deterministic State
Machines
1.0 Overview
We present a very abbreviated and incomplete summary of the development of computer pro-
gramming. Purists will abhor the omissions and oversimplifications. Nonetheless, we think
that the revolution being wrought by object-oriented programming (and Java in particular)
can only be appreciated within a historical context.
In section 1.1 we begin, as do all revolutions, with a preamble justifying the overthrow of
the old regime. Most programmers are ensconced somewhere in the span of third or fourth
generation languages. They believe that the next generation of features is just what they need
to write the killer app or to deliver their project on time and within budget. Yet many design-
ers and managers, and most users who pay the bills, know otherwise.
In section 1.2 we make the concept of a finite deterministic state machine our philosophi-
cal foundation. We realize that the academic literature adopts the more general concept of a
Turing machine for the formal development of computing theory. Since engineering as cur-
rently understood can construct only finite machines, we limit ourselves to what is realistic.
1
2 Chapter 1: A Revisionist History of Programming
The thesis of our entire presentation is twofold. First, we view practical computers as
finite state machines; indeed, the underlying computing hardware falls into this category. Sec-
ond, translating the abstract state machine concept into software leads directly and naturally
to the concepts of object-oriented programming.
The main reason programmers didn’t discover object-oriented-programming sooner is
that a singular event occurred as electronic computing was being born. That event was John
von Neumann’s entry into the field. In section 1.3 we briefly introduce his computing insights
and architecture. In section 1.4 we present a simplified example of a von Neumann machine
along with the binary codes to control its operation.
In sections 1.5–1.7, starting with the first level of machine code, we retrace the steps of the
first three generations of software as it traipses further into the maze of the input-process-out-
put model. Our retelling culminates in third generation languages with structured analysis
and design.
In the actual history of programming, the development forked into several different paths
at this point, including early forms of object-oriented programming. Ironically, object-ori-
ented concepts could have been discovered and adopted from the very beginning of program-
ming, were it not for the overwhelming success of von Neumann’s approach. Indeed, the
object-oriented paradigm provides a most direct correspondence between hardware to soft-
ware.
In section 1.8 we present a simplified model of data structure in a third generation lan-
guage. In section 1.9 we discuss why this notion is inadequate and the problems that arise.
In section 1.10, we presume that most readers are familiar with third generation program-
ming, so we exercise poetic license to rewrite history as if third generation led directly to the
concepts of object-oriented programming as embodied in Java. We spare the reader the inter-
vening years of wandering through the wilderness of C++ and other near misses.
Having wed program and data, which were separated in von Neumann’s original creation,
in section 1.11 we provide a brief synopsis of the concepts of object-oriented programming.
We then survey the Java Virtual Machine architecture, which was designed to facilitate dis-
tributed systems running across a network.
The possible transitions between states can be represented graphically in a state transition
diagram.
Summarizing, a finite state machine is one having a finite number of attributes whose
(finite) set of values characterize it at any time. A finite state machine can theoretically be rep-
resented by a (possibly very large and complicated) state transition diagram. In practice, we
usually look at a subset of the entire diagram to specify or understand the workings of the
machine.
A state machine is deterministic if there is some set of rules — procedures or formulas that
prescribe the maching’s behavior — by which one can transform any given state into the next
state. Put negatively, there is no random element in the behavior of the machine. Whether
human beings or the universe are deterministic has been the subject of endless philosophical
and religious debate, much of it content-free. Classical physics said that the universe was
entirely deterministic; quantum mechanics denies that even any single event is deterministic.
Computers are deterministic by design, but random events do sometimes intrude.
We need to add one more concept to complete our initial idea of computing: time. In addi-
tion to being finite deterministic state machines, computers are also governed by a clock. At
each regular tick of the clock (or multiple thereof), the state of the machine makes a transi-
tion to a new state as determined by the transition rules. The frequency of the clock tick is
called the cycle time of the machine. As of this writing, cycle times of personal computers are
in the 500 MHz range, meaning that roughly 200 million state changes can occur each sec-
ond.
For ease of use, most computers implement access control, in which attribute values can
be set or read from outside the machine in a controlled fashion. This is traditionally called
input/output management but we can also think of knobs, dials, sliders, etc. Computers also
implement some methods for invoking important state transition rules. In the traditional pro-
gramming world, this involved opcodes (operation codes) and programs; we can think of but-
tons and wheels that cause some action to take place.
CA CC I/O world
M
6 Chapter 1: A Revisionist History of Programming
The basic operation of this architecture is as follows. First, a sequence of binary digits rep-
resenting data and opcodes is hand assembled by humans and fed into the input device. In the
earliest days, this was via binary switches, but punch cards soon became the standard method
of input. The data is transferred from the input device to storage.
Next, the control unit executes the loaded opcodes. It begins by addressing a predeter-
mined location in memory, loading the opcode at that location into its instruction unit, and
then executing it. The opcode might load data from memory into a register; or it might per-
form an arithmetic operation on the data in two registers placing the result in a third; or it
might store data from a register to memory. Based on the result of this execution, the control
unit determines the address of the next instruction, loads it, and executes it. This process
repeats until the end of the instruction sequence or until an opcode aborts (e.g., divide by 0).
During this process, certain instructions place data onto the output unit, enabling people
monitoring the computer to inspect the results. Paper tape was the early standard for output
display.
Instruction
Opcode Opcode Opcode
State Change
Binary Hex Mnemonic
Description
00000000 00 NOP no operation
00000001yyyyyyyy 01YY LD 00,YY load data YY into register
00
00000010yyyyyyyy 02YY LD 01,YY load data YY into register
01
00000011 03 ADD 00,01,02 add data in register 00 to
data in register 01 and
place result in register 02
00000100 04 MUL 00,01,02 multiply data in register 00
by data in register 01 and
place result in register 02
Our First Machine Language Programming Steps 7
Instruction
Opcode Opcode Opcode
State Change 1
Binary Hex Mnemonic
Description
00000101yyyyyyyy 05YY STM 02,YY store data in register 02 to
memory location YY
000000110yyyyyyyy 06YY IN YY copy data from input
device to memory location
YY
000000111yyyyyyyy 07YY OUT YY copy data from memory
location YY to output
device
111111111 FF END end execution
Here yyyyyyyy stands for an arbitrary binary string and YY its corresponding hex representa-
tion. The collection of binary opcodes is the machine language for our virtual machine. We
cheat a bit according to the binary purist, and create the instruction mnemonics next to the
opcodes to help us remember what the opcodes stand for. We will extend this initial instruc-
tion set in the next section.
We arrange that when we press the start button, our virtual machine will read the pro-
gram opcodes we feed it and copy them into contiguous storage starting at location 100. The
control unit will fetch the opcode value it finds at location 100 and execute it. After executing
this opcode, the control unit fetches the next opcode, executes it, and so on. It continues this
cycle until it encounters the opcode ‘FF’, when it stops execution.
computing illustrating the first erroneous programmer bias: real programmers don’t need
productivity tools or readable programs.
In fact, most of us are not capable of dividing eight-digit numbers in our minds at age six
or computing 40-digit sums mentally faster than the early computers, as could von Neumann.
It is easier for us to read the mnemonic codes than the corresponding hex codes. This realiza-
tion leads to the first step above machine language. It would be more productive for program-
mers to write programs using the mnemonic codes and have these translated into machine
code. How could this be done?
We first determine a binary encoding to represent characters. Then we choose a character
representation for each opcode. Next we hand assemble a machine language program that
reads character representations of opcodes and assembles them into machine codes. This
enables us to write our application program using the character opcodes and feed it into this
assembler program, which will then assemble the corresponding machine code. This
approach shows the beauty of interpreting binary streams as either data or opcodes; the out-
put produced by the assembler program can be fed back into the machine as data, interpreted
as machine opcodes, and executed.
Surprise! Since our mnemonics serve perfectly as character representations of the opcodes,
they serve as an assembler language for our virtual machine. To program in our new assem-
bler language, we first construct the sequence of mnemonic codes providing the desired pro-
gram function. After loading and executing the assembler program, we feed it our character
stream of mnemonic codes followed by the input data. Finally we feed the resulting machine
code output back into the machine to be executed. In terms of our previous table diagrams,
we start with the second column and the assembler produces the first column. Here is our
two-function program in assembler.
IN 00 ; copy data value from input device to location 00
IN 01 ; copy data value from input device to location 01
LDM 00,00 ; load data value from location 00 into register 00
LDM 01,01 ; load data value from location 01 into register 01
ADD 00,01,02 ; add data in register 00 to data in register 01 and place
result in reg 02
STM 02,02 ; store data in register 02 to memory location 02
OUT 02 ; copy data value at location 02 to output device
END ; end execution
We agree to use the semicolon character ‘;’ as a statement terminator. If we further arrange
that the compiler ignore any characters from the terminator to the end of line, we can use this
“dead space” for comments. Our program can be written more compactly as:
IN 00;IN 01;LDM 00,00;LDM 01,01;ADD 00,01,02;STM 02,02;END;0203
The assembler translates it to the equivalent machine code sequence as:
06000601080009010305020702FF0203
Although this assembler program won’t win a Pultitzer prize, it is certainly more readable
for most of us than the corresponding machine code. We decide to write all new programs in
assembler language. Unfortunately, we’re still stuck with the machine code for the assembler
A Third-Generation Programming Language 11
itself. But wait! Why not rewrite the code sequence for the assembler in our new language?
The result is a source program that we can feed into the original assembler, which will (amaz-
ingly) generate its own opcode sequence as output. Even the assembler itself can be written in 1
assembler language! Hence all machine language programs can be (re)written into assembler,
and only machines need read machine code.
This process of programmatically pulling ourselves up by our own bootstraps seems
miraculous but is common in the development of computing. The seeming magic occurs in
the two-stage process. First, write a preliminary version of a next-level program-generating
tool in the existing language. Once the new program is fully functional, reverse engineer it
using itself. The result is functionally equivalent to, and can replace, the preliminary version.
This can be quite tricky to do in practice, particularly when correcting errors and making
design changes during the process.
The addition of “branch,” “test,” “branch conditional” and other instructions to our
assembler language allows very sophisticated assembler programs to be constructed. In par-
ticular, it is possible to package commonly used code sequences so that they do not have to be
rewritten for each program. The idea is to create a block from the opcode sequences for a
general algorithm or procedure. Examples of such a block include I/O routines, mathematical
calculations such as exponentiation, and program initialization and cleanup. Using the
“branch” instruction with a destination address of this block, we can “call” to the corre-
sponding block from any location in our program, eliminating the need for duplicate code
in-line. Such a block is called by names such as “subroutine”, “procedure”, and “function.”
There are many common steps involved in the execution of programs including loading
the program, allocating storage, and reading and writing data from I/O devices. These rou-
tines are collected together into one (large) program called an operating system (OS), which
controls the various system resources and functions, including loading and executing pro-
grams. The OS is loaded first, when the computer is turned on. We then feed our program
opcode sequence into the OS as input data. The OS, in turn, sets up our opcodes and data
bytes and executes the program. It also provides a set of system subroutines, such as I/O, that
our program can call. When execution completes, the OS returns the machine to its initial
state (i.e., as it was before our program was loaded) by performing cleanup procedures. A
well-designed OS relieves the average program from handling mundane hardware-related
tasks and frees it to focus on solving its particular problem. A poorly designed OS will allow
user programs to place the machine in an inconsistent state, leading to a crash.
The desire for increased efficiency on the human side of the process led to the next innovation
in programming languages.
While assembler looks more like a human language than machine code, it hasn’t won any
literary prizes. Software developers realized that they could extend the power of the assem-
bler to allow program statements that are more readable than simple opcode mnemonics.
Memory addresses are replaced by variables; test and branch instructions become
“if-then-else” statements; code sequences that are often branched become sub-routines which
are “called” with passed data parameters and return values. Such a souped-up assembler is
called a compiler. Early compilers translated a series of statements from the new format into
assembler language, which was then assembled to produce machine code. Two of the first
compilers/languages were COBOL (Common Business Oriented Language), which was cre-
ated to help programming in business applications, and FORTRAN (FORmula TRANsla-
tion), which was used for scientific computing. Both are still quite popular among
programmers of a certain generation.
Let’s take a cut at our own higher-level language. In order to achieve our first objective —
to replace memory locations with named variables — the compiler must associate a name
with a data type and a chunk of storage. To do this, the compiler will keep an internal map of
the memory associated with each name. This map also tracks what type of data is stored at
each location, so the compiler can figure out how many bytes to retrieve or store, as well as
how to interpret the data. Our first statements might look something like:
int operand1;
int operand2;
int result;
As before, we use the semicolon as a statement terminator to delineate statements to the
compiler and us. The three previous statements each tell the compiler to reserve enough stor-
age for an integer value and associate it with the specified name. For example, we can read
the first statement as, “Reserve space for an integer and associate the name ‘operand1’ with
that storage.”
Under the covers, our compiler creates a table and adds a new entry for each statement. A
row contains the (hex) representation of the characters for the name, a (hex) address for the
starting location in memory, a (hex) code standing for the “integer” data type, and a (hex)
length. This enables the compiler to generate the appropriate instructions to load or store
data at the associated address when it encounters a reference to the name. Borrowing termi-
nology from mathematics, we say that we have declared a variable with the given name.
The value of the variable is the resulting value of the data at the associated location when
the hex string of the appropriate length is interpreted as the specified data type. How do we
load specific values into storage? Again borrowing from mathematics, we say that we assign a
value to the variable. Unfortunately, many languages use the equals sign ‘=’ to represent the
assignment. This is not an accurate analogy and we think the best choice is that of APL (A
Programming Language — no joke) which uses the symbol, ‘‹ ’ that clearly shows the direc-
tion of assignment. However, when in Rome:
operand1 = 2;
operand2 = 3;
A Third-Generation Programming Language 13
The first statement tells the compiler, “Generate the instructions to load the (integer) value
2 into the storage location associated with the name ‘operand1’.” This style of interpreting
statements becomes reasonably straightforward if we read from right to left, “take the value 2 1
and store it in (the location associated with the name) ‘operand1’.”
Next we want to add (the values in) the operands and place the result in (the location
associated with) the result. We’ll do this with the single statement:
result = operand1 + operand2;
The compiler and we read this as follows. “Retrieve the (integer) value from (the location
associated with the name) ‘operand2’; add it to the (integer) value retrieved from (the loca-
tion associated with the name) ‘operand1’; store the sum (as an integer) into the (location
associated with the name) ‘result’.” The compiler allows us to drop the parenthetical parts
because it understands and implicitly fills them in.
We now come to one of the crowning achievements of third-generation languages. A func-
tion is modeled on the eponymous mathematical construct and has the same syntax; it is an
improved version of the assembler subroutine construct. If we are clever enough, we can
design our compiler to allow us to pass values to, and return a value from, a program func-
tion in much the same as a mathematical function.
We first declare the function to the compiler, so it knows the name of the function as well
as the names and data types of input parameters (if any) and the return value (if any). In our
language, the data type of the return value will be listed to the left of the function name, by
analogy to the way we declared variables. As with a mathematical function, the parameters
will be listed to the right of the function name within parentheses, separated by commas. For
example, following is a function that takes no input parameters but returns a value.
int getinput( )
This can be read as “declare a function named ‘getinput’ that takes no input parameters
and returns a value of integer type.” Another example that takes one input parameter and
doesn’t return a value is:
void setoutput( int value )
This is read, “Declare a function named ‘setoutput’ that takes a single input parameter of
data type integer named ‘value’ and returns nothing.” Another example is:
int add( int op1, int op2 )
This is read as, “Declare a function named ‘add’ that takes two input parameters of inte-
ger type named ‘op1’ and ‘op2’ and returns a sum of integer type.
Next, we define the function by listing the instructions of the subroutine block inside a
matched pair of braces.
int add ( int op1, int op2 ) {
return op1 + op2;
};
This says, “The definition of the function add() consists of a single statement that adds the
values of the two passed parameters and passes back the result as its return value.”
14 Chapter 1: A Revisionist History of Programming
The compiler associates the function name with the subroutine block and allows us to use
this name as shorthand for jumping to the subroutine block. At the point in our program
where we want to execute the subroutine block, we invoke or call the function by listing its
name and substituting actual values for the appropriate input parameters. The compiler gen-
erates code to associate the actual values with the parameter names. The usual way to pass
parameters is by constructing a stack and pushing them on the stack, although the IBM main-
frame architecture is a notable exception. For example, we can invoke the add() function
from another program.
int result;
result = add( 2, 3 );
This says, “Execute the subroutine block defined in add, substituting the value 2 for the
first parameter and the value 3 for the second parameter, and assign the resulting return value
of the execution to the variable result.”
As an exercise, define and declare a multiply() function analogous to add(). With it,
along with a few auxiliary I/O routines, we can write the third-generation version of our pro-
gram.
int operand1;
int operand2;
int result;
operand1 = getinput();
operand2 = getinput();
temp = calculator.operand1;
The first statement is read, “Assign the value 2 to the operand1 element of the calculator
structure.” The second is “Retrieve the value of the operand1 element of the structure calcu-
lator and assign it to the variable temp.”
16 Chapter 1: A Revisionist History of Programming
calculator.operand1 = getinput();
calculator.operand2 = getinput();
add( calculator );
setoutput( calculator.result );
multiply( calculator );
setoutput( calculator.result );
We can develop complex data forms by iterating the process of collecting data into struc-
tures; put otherwise, we nest data structures. For example, we can define a new structure that
contains integers as well as a calculator structure.
struct simple_scenario {
struct two_function_calculator calculator;
int rounding;
};
The “dot notation” extends easily to cover the compound case.
struct simple_scenario scenario;
scenario.rounding = 0;
scenario.calculator.operand1 = 2;
scenario.calculator.operand2 = 3;
Trouble in Structured City 17
add( scenario.calculator );
1
setoutput( scenario.calculator.result );
multiply( scenario.calculator );
setoutput( scenario.calculator.result );
Very sophisticated processes can be modeled using this technique.
puting, data was the neglected stepchild, viewed merely as input and output to the program.
As the field emerged, the main directions of research were in writing programs, creating lan-
guages, and designing assemblers and compilers. The subordination of data to program is
reflected in the name given to the discipline, “programming.” Most attention was paid to
simulating the dynamic behavior of a system in code, while the was often an afterthought.
Here is the first wrong turn taken in the journey of software engineering development: the
data and the program were separated and data was subordinated to program. Ironically, one
of von Neumann’s initial observations was that the opcodes could be placed into and exe-
cuted from the storage in which the data resided. His further observation — that opcodes
could be treated as data and modified during program execution — resulted in the concept of
self-modified code, which was theoretically useful in his development of automata theory. It
leads to enormously flexible programs, but programmers quickly realized that dynamic mod-
ification of opcodes led to programs that were virtually impossible to verify or correct. The
program ate its own trail of breadcrumbs and erased its own tracks! Consequently, the stan-
dard procedure is to load program opcodes into a portion of storage that is write-protected in
order to prohibit deliberate or accidental modification of instructions. This code segment is
separate from the data segment in which storage was regularly modified.
One serious problem with separating and subordinating the data is that the data structure
is often jammed into the program structure. When the reuse of program code is paramount,
subroutines are written to accept input in the most general form, i.e. separate parameters of
atomic type. In order to use these pre-packaged routines, programs often keep variables to
shadow the parameters as well as variables to synchronize state between the main program
and the various subroutines. It is difficult to collect these variables into a natural structure.
The resulting “global” variables are artifacts of the cobbling together of the subroutines. The
fidelity between the program and the real-world process it seeks to represent is degraded by
this plumbing code. Program structure can become so arcane as to be undecipherable to any-
one other than the author; sometimes even this is a stretch.
The second major problem is a result of the (mis)application of the structured analysis and
design process. In applying the analytical approach to representing a real-world process on a
computer, structured analysis and design makes a major implicit assumption: the structure of
the process being represented is essentially stable. We are unlikely to be successful in reducing
a target entity into components and then retarget each constituent if it is changing during the
analysis. The rapidly mutating retroviruses (e.g., HIV) have proved particularly difficult for
modern medicine, partially for this reason.
In the everyday world of program development, the stability assumption is rarely realized.
Design specifications are often subject to change from a number of sources. The phenomenon
under study can change. It could become necessary to model a larger or smaller portion of the
phenomenon. Our understanding could change. The choice of modeling tools or language
could change. Most likely of all, capricious users and programmers change their minds.
Changes to the specifications of a top-down system design cause effects that amplify down
the design hierarchy. This is understandable since each iteration of the approach reduces an
entity into smaller constituents. If the entity changes, these components need to change —
many will be completely invalid. Injure the trunk and all the branches suffer, perhaps die.
Another problem with classical programming is that the opcodes and atomic data types
are tightly coupled with the machine architecture and consequently are machine-dependent.
Code developed for one machine will not run on another. Even passing data from one
Back to the Future: Object-Oriented 19
machine to another is problematic due to different interpretations of so basic a concept as
byte order. Although languages such as C abstract the basic instructions and data types, there
are still problems with different word sizes. In addition, the utilities provided by the underly- 1
ing operating system differ in function and calling convention. Hence, even if code reusability
is achieved on a single platform, reusability across platforms is a Herculean task.
These and other problems surfaced in isolation over many years. Collectively they became
major obstacles in the 1980s when the levels of sophistication of hardware, software, and
networking made distributed systems feasible. The advent of the client-server paradigm broke
the stalemate between centralized and decentralized systems. The commercial acceptance of
Unix and C brought the first reality of multi-platform systems. Data sharing and program
reuse became paramount as systems became more complex.
It has become apparent that structured analysis and design don’t scale well in a changing,
distributed, heterogeneous environment. Many of the difficulties have a common theme: shar-
ing. It is relatively straightforward for a single programmer to design and implement an appli-
cation that receives its data from a single source and has a single user in one environment.
Things become much more difficult, however, when there is a team of programmers, multiple
data sources, many users, or a heterogeneous environment for development and deployment.
Combining these factors results in an exponential increase in complexity.
The difficulties arising from sharing are often synchronization problems. A variety of tech-
niques has evolved to manage synchronization. Source code management librarians require
modules to be checked out and in, preventing developers from stepping on each others’ code.
Databases provide locking to prevent multiple update requests from conflicting. Re-entrant
code and thread locking prevent multiple processes from tripping over each other. Transac-
tion management subsystems allow multiple users without conflicts. Unfortunately, these
techniques are complicated and are often grafted onto or forced into the development pro-
cess.
contrast it to these languages. We will instead retrace our revisionist history and develop OO
from first principles.
Recall that computers are state machines. A (finite) collection of attribute values (from a
finite range) characterizes the machine at any time. A (finite) set of rules determines the tran-
sition of the machine from one state to the next. Often a limited number of attributes charac-
terize the most important states of the machine; these are called state variables. Some
attributes are internal to the working of the machine and are called private. Other attributes
are exposed to the external world and are called public.
One way to visualize this is to think of attributes as displays on the machine. The private
ones are inside, available only to the mechanic. The public ones are right on the front panel;
in most science fiction films these are blinking colored lights. It’s more convenient to use dials
and gauges. Some of these gauges, such as a power meter, only display data; these are called
read-only. Others, such as a clock, can be set as well; these are called read-write.
The way most everyday machines work is that there are controls such switches, buttons or
knobs that effect the state transitions in which we are interested. For example, you press
down the gas pedal of a car and it goes faster. What is actually happening is that mechanical
and electronic linkages are feeding more gas into the engine. In a computer, programs effect
the state transitions.
Our premise is that a computer is useful when it can reproduce or simulate the behavior of
some phenomenon that interests us. In other words, we want to can simulate some behavior
on the computer. With our understanding that a computer is a state machine, we must model
an external entity on a finite deterministic state machine. The easiest way to do this, it would
seem, is to map features of the entity onto the features of our machine. This amounts to mod-
eling the entity as a finite state machine.
How do we model an entity as a state machine? First, identify the attributes of the entity
and determine which are public, private, read-only, and read-write. These attributes are
mapped onto data types in the computer. Then we characterize the dynamic behavior of the
entity in terms of state transitions. These state transitions are implemented as functions. In
our new approach, we call the model an object, a data variable a field, and a function a
method. Note that both fields and methods are intrinsic to the object; this is called encapsula-
tion.
In practice, the creation of objects is a two-step process. First, declare to the compiler a
template or blueprint for the object, called a class. A class instructs the compiler about the
form of the fields and methods but, like the blueprint of a building, is neither the building nor
the construction crew. Once the class has been declared, we tell the compiler to construct an
object using the blueprint. Just as many houses can be built from a blueprint, so can many
objects be built from a class. At least in the case of object construction, we are guaranteed
that the general contractor constructs every object exactly according to the specification.
After construction, we can customize the object by setting attributes; we can also invoke its
dynamic behavior by calling methods with appropriate parameters.
Back to the Future: Object-Oriented 21
We return to our previous example to see how the two-function calculator looks using the
object-oriented approach. In fact, it’s similar to the third-generation structure, with some
important differences. First, we define the class. 1
class Calculator {
int operand1;
int operand2;
int result;
void add() {
result = operand1 + operand2;
}
void multiply() {
result = operand1 * operand2;
}
}
This tells the compiler that we are defining a new class (i.e., object blueprint) whose name
is ‘Calculator’. For clarity we agree on the convention that class names start with an initial
capital and the names of objects, fields, and methods start with a lowercase letter.
Next, we tell the compiler about the fields — how to reserve storage and interpret it as
data — for the object attributes. In our case, we have three integers named ‘operand1’,
‘operand2’ and ‘result’. We have simplified things temporarily by ignoring whether they
should be private or public.
Finally, we tell the compiler about the methods. We have two, named ‘add’ and ‘multiply’,
that do just what you’d expect. The add() method retrieves the values of the operand1 and
operand2 attributes, adds them and places the sum in the result attribute. The void keyword
preceding the method name tells the compiler that the method returns no value. Similarly,
multiply() places the product of the operand1 and operand2 attributes into the result
attribute.
Now we declare and construct a Calculator object with the following code:
Calculator calculator;
Finally, the assignment statement tells the compiler to associate the newly constructed (pun
intended) object with the name ‘calculator’.
At this point we can use the object:
Calculator calculator;
calculator = new Calculator();
calculator.operand1 = 2;
calculator.operand2 = 3;
calculator.add();
System.out.println( calculator.result );
calculator.multiply();
System.out.println( calculator.result );
The two statements with equals signs tell the compiler to assign the values 2 and 3 to the
attributes operand1 and operand2 of the object calculator. The next statement invokes the
add() method of the object calculator. In pure object-oriented parlance, we send a message
to the object calculator telling it to add itself. The next statement invokes a Java system out-
put routine to print to the console a line containing the value contained in the result
attribute of the object calculator. Similarly, we tell the object to multiply itself and then we
output its result.
The object-oriented code we have created is actual Java code that will execute. To run the
code, let’s start JBuilder, an Integrated Development Environment (IDE) for Java from Bor-
land, that makes developing Java programs easy. Here are the steps. Pull down the File menu
in JBuilder. Click on the New Project… item. In the Project Wizard dialog box, change the
File entry so that it ends with ‘\tutorial110a\ Tutorial110a.jpr’ and change the Title entry to
read ‘Tutorial 1.10.a’. You can also type your name in the Author entry if you wish. Now
click on the Finish button and JBuilder creates the project file.
Pull down the File menu again and click on the New... item. In the New… window, double
click on the Class icon. In the New Java File dialog box that appears, change the Class Name
entry to ‘Application’ and click the OK button. JBuilder adds a file named ‘Application.java’
to the project; it can be seen in the Navigator pane in the upper-left corner. The source code is
displayed in the Source pane.
Now enter the following source code into the Source pane:
Tutorial 1.10.a
class Calculator {
int operand1;
int operand2;
int result;
void add() {
Back to the Future: Object-Oriented 23
public Application() {
Calculator calculator;
calculator = new Calculator();
calculator.operand1 = 2;
calculator.operand2 = 3;
calculator.add();
System.out.println( calculator.result );
calculator.multiply();
System.out.println( calculator.result );
}
with a particular signature. At run time, control is initially transferred to this main() method.
Normally executed Java programs have a class with such a main() method.
chip is characterized by its input/output specifications. Chips can be bought off the shelf and
combined in predictable fashion, with no knowledge of design or manufacturing techniques.
A JavaBean is a software component, the analogue of a hardware integrated circuit. Com-
plexity of behavior of a collection of discrete components (the fields and methods) is bundled
together into a single object. By following encapsulation, data hiding, and other principles of
object-oriented design, off-the-shelf objects can be synthesized into user-designed programs of
increased richness. Using the event model to connect objects corresponds to connecting chips
using their I/O specs.
On the downside, this makes the resulting code nonportable and lessens the benefits of Java,
particularly in a distributed system or a network environment.