| Thinking in Java source ref: work2.html |
We cut nature up, organize it into concepts, and ascribe significances as we
do, largely because we are parties to an agreement that holds throughout our
speech community and is codified in the patterns of our language ... we cannot
talk at all except by subscribing to the organization and classification of data
which the agreement decrees. Benjamin Lee Whorf (1897-1941)
The genesis of the computer revolution was in a machine. The genesis of our
programming languages thus tends to look like that machine.
But computers are not so much machines as they are mind amplification tools
(bicycles for the mind, as Steve Jobs is fond of saying) and a different kind
of expressive medium. As a result, the tools are beginning to look less like
machines and more like parts of our minds, and also like other forms of
expression such as writing, painting, sculpture, animation, and filmmaking.
Object-oriented programming (OOP) is part of this movement toward using the
computer as an expressive medium.
This chapter will introduce you to the basic concepts of OOP, including an
overview of development methods. This chapter, and this book, assume that you
have had experience in a procedural programming language, although not
necessarily C. If you think you need more preparation in programming and the
syntax of C before tackling this book, you should work through the Foundations for Java training CD ROM, bound in the back
of this book.
This chapter is background and supplementary material. Many people do not
feel comfortable wading into object-oriented programming without understanding
the big picture first. Thus, there are many concepts that are introduced here to
give you a solid overview of OOP. However, other people may not get the big
picture concepts until theyve seen some of the mechanics first; these people
may become bogged down and lost without some code to get their hands on. If
youre part of this latter group and are eager to get to the specifics of the
language, feel free to jump past this chapterskipping it at this point will not
prevent you from writing programs or learning the language. However, you will
want to come back here eventually to fill in your knowledge so you can
understand why objects are important and how to design with them.
All programming languages provide abstractions. It can be argued that the
complexity of the problems youre able to solve is directly related to the kind
and quality of abstraction. By kind I mean, What is it that you are
abstracting? Assembly language is a small abstraction of the underlying
machine. Many so-called imperative languages that followed (such as FORTRAN,
BASIC, and C) were abstractions of assembly language. These languages are big
improvements over assembly language, but their primary abstraction still
requires you to think in terms of the structure of the computer rather than the
structure of the problem you are trying to solve. The programmer must establish
the association between the machine model (in the solution space, which is the
place where youre modeling that problem, such as a computer) and the model of
the problem that is actually being solved (in the problem space, which is the
place where the problem exists). The effort required to perform this mapping,
and the fact that it is extrinsic to the programming language, produces programs
that are difficult to write and expensive to maintain, and as a side effect
created the entire programming methods industry.
The alternative to modeling the machine is
to model the problem youre trying to solve. Early languages such as LISP and
APL chose particular views of the world (All problems are ultimately lists or
All problems are algorithmic, respectively). PROLOG casts all problems into
chains of decisions. Languages have been created for constraint-based
programming and for programming exclusively by manipulating graphical symbols.
(The latter proved to be too restrictive.) Each of these approaches is a good
solution to the particular class of problem theyre designed to solve, but when
you step outside of that domain they become awkward.
The object-oriented approach goes a step further by providing tools for the
programmer to represent elements in the problem space. This representation is
general enough that the programmer is not constrained to any particular type of
problem. We refer to the elements in the problem space and their representations
in the solution space as objects. (You will also need other objects that dont
have problem-space analogs.) The idea is that the program is allowed to adapt
itself to the lingo of the problem by adding new types of objects, so when you
read the code describing the solution, youre reading words that also express
the problem. This is a more flexible and powerful language abstraction than what
weve had before.[2] Thus, OOP allows
you to describe the problem in terms of the problem, rather than in terms of the
computer where the solution will run. Theres still a connection back to the
computer: each object looks quite a bit like a little computerit has a state,
and it has operations that you can ask it to perform. However, this doesnt seem
like such a bad analogy to objects in the real worldthey all have
characteristics and behaviors.
Alan Kay summarized five basic characteristics of Smalltalk, the first
successful object-oriented language and one of the languages upon which Java is
based. These characteristics represent a pure approach to object-oriented
programming:
An object has state, behavior and identity.
This means that an object can have internal data (which gives it state),
methods (to produce behavior), and each object can be uniquely distinguished
from every other objectto put this in a concrete sense, each object has a
unique address in memory.[3]
Aristotle was probably the first to
begin a careful study of the concept of type; he spoke of the class of
fishes and the class of birds. The idea that all objects, while being unique,
are also part of a class of objects that have characteristics and behaviors in
common was used directly in the first object-oriented language, Simula-67, with
its fundamental keyword class that introduces a new type into a program.
Simula, as its name implies, was
created for developing simulations such as the classic bank teller problem. In
this, you have a bunch of tellers, customers, accounts, transactions, and units
of moneya lot of objects. Objects that are identical except for their state
during a programs execution are grouped together into classes of objects and
thats where the keyword class came from. Creating
abstract data types (classes) is a fundamental concept in object-oriented
programming. Abstract data types work almost exactly like built-in types: You
can create variables of a type (called objects or instances in
object-oriented parlance) and manipulate those variables (called sending messages or requests; you send a message and the object figures out
what to do with it). The members (elements) of each class share some
commonality: every account has a balance, every teller can accept a deposit,
etc. At the same time, each member has its own state: each account has a
different balance, each teller has a name. Thus, the tellers, customers,
accounts, transactions, etc., can each be represented with a unique entity in
the computer program. This entity is the object, and each object belongs to a
particular class that defines its characteristics and behaviors.
So, although what we really do in object-oriented programming is create new
data types, virtually all object-oriented programming languages use the class
keyword. When you see the word type think class and vice versa.[4]
Since a class describes a set of objects that have identical characteristics
(data elements) and behaviors (functionality), a class is really a data type
because a floating point number, for example, also has a set of characteristics
and behaviors. The difference is that a programmer defines a class to fit a
problem rather than being forced to use an existing data type that was designed
to represent a unit of storage in a machine. You extend the programming language
by adding new data types specific to your needs. The programming system welcomes
the new classes and gives them all the care and type-checking that it gives to
built-in types.
The object-oriented approach is not
limited to building simulations. Whether or not you agree that any program is a
simulation of the system youre designing, the use of OOP techniques can easily
reduce a large set of problems to a simple solution.
Once a class is established, you can make as many objects of that class as
you like, and then manipulate those objects as if they are the elements that
exist in the problem you are trying to solve. Indeed, one of the challenges of
object-oriented programming is to create a one-to-one mapping between the
elements in the problem space and objects in the solution space.
But how do you get an object to do useful work for you? There must be a way
to make a request of the object so that it will do something, such as complete a
transaction, draw something on the screen, or turn on a switch. And each object
can satisfy only certain requests. The requests you can make of an object are
defined by its interface, and the type is what determines the interface.
A simple example might be a representation of a light bulb:
Light lt = new Light(); lt.on();
The interface establishes what requests you can make for a particular
object. However, there must be code somewhere to satisfy that request. This,
along with the hidden data, comprises the implementation. From a procedural programming
standpoint, its not that complicated. A type has a method associated with each
possible request, and when you make a particular request to an object, that
method is called. This process is usually summarized by saying that you send a
message (make a request) to an object, and the object figures out what to do
with that message (it executes code).
Here, the name of the type/class is Light, the name of this particular
Light object is lt, and the requests that you can make of a
Light object are to turn it on, turn it off, make it brighter, or make it
dimmer. You create a Light object by defining a reference (lt)
for that object and calling new to request a new object of that type. To
send a message to the object, you state the name of the object and connect it to
the message request with a period (dot). From the standpoint of the user of a
predefined class, thats pretty much all there is to programming with objects.
The preceding diagram follows the format of the Unified Modeling Language (UML). Each class is
represented by a box, with the type name in the top portion of the box, any data
members that you care to describe in the middle portion of the box, and the methods (the functions that belong
to this object, which receive any messages you send to that object) in the
bottom portion of the box. Often, only the name of the class and the public
methods are shown in UML design diagrams, so the middle portion is not shown. If
youre interested only in the class name, then the bottom portion doesnt need
to be shown, either.
While youre trying to develop or understand a program design, one of the
best ways to think about objects is as service providers. Your program itself
will provide services to the user, and it will accomplish this by using the
services offered by other objects. Your goal is to produce (or even better,
locate in existing code libraries) a set of objects that provide the ideal
services to solve your problem.
A way to start doing this is to ask if I could magically pull them out of a
hat, what objects would solve my problem right away? For example, suppose you
are creating a bookkeeping program. You might imagine some objects that contain
pre-defined bookkeeping input screens, another set of objects that perform
bookkeeping calculations, and an object that handles printing of checks and
invoices on all different kinds of printers. Maybe some of these objects already
exist, and for the ones that dont, what would they look like? What services
would those objects provide, and what objects would they need to
fulfill their obligations? If you keep doing this, you will eventually reach a
point where you can say either that object seems simple enough to sit down and
write or Im sure that object must exist already. This is a reasonable way to
decompose a problem into a set of objects.
Thinking of an object as a service provider has an additional benefit: it
helps to improve the cohesiveness of the object. High cohesion is
a fundamental quality of software design: It means that the various aspects of a
software component (such as an object, although this could also apply to a
method or a library of objects) fit together well. One problem people have
when designing objects is cramming too much functionality into one object. For
example, in your check printing module, you may decide you need an object that
knows all about formatting and printing. Youll probably discover that this is
too much for one object, and that what you need is three or more objects. One
object might be a catalog of all the possible check layouts, which can be
queried for information about how to print a check. One object or set of objects
could be a generic printing interface that knows all about different kinds of
printers (but nothing about bookkeepingthis one is a candidate for buying
rather than writing yourself). And a third object could use the services of the
other two to accomplish the task. Thus, each object has a cohesive set of
services it offers. In a good object-oriented design, each object does one thing
well, but doesnt try to do too much. As seen here, this not only allows the
discovery of objects that might be purchased (the printer interface object), but
it also produces the possibility of an object that might be reused somewhere
else (the catalog of check layouts).
Treating objects as service providers is a great simplifying tool, and its
very useful not only during the design process, but also when someone else is
trying to understand your code or reuse an objectif they can see the value of
the object based on what service it provides, it makes it much easier to fit it
into the design.
It is helpful to break up the playing field into class
creators (those who create new data types) and client programmers[5] (the class
consumers who use the data types in their applications). The goal of the client
programmer is to collect a toolbox full of classes to use for rapid application
development. The goal of the class creator is to build a class that exposes only
whats necessary to the client programmer and keeps everything else hidden. Why?
Because if its hidden, the client programmer cant access it, which means that
the class creator can change the hidden portion at will without worrying about
the impact on anyone else. The hidden portion usually represents the tender
insides of an object that could easily be corrupted by a careless or uninformed
client programmer, so hiding the implementation reduces program bugs.
The concept of implementation hiding cannot be overemphasized. In any
relationship its important to have boundaries that are respected by all parties
involved. When you create a library, you establish a relationship with the
client programmer, who is also a
programmer, but one who is putting together an application by using your
library, possibly to build a bigger library. If all the members of a class are
available to everyone, then the client programmer can do anything with that
class and theres no way to enforce rules. Even though you might really prefer
that the client programmer not directly manipulate some of the members of your
class, without access control theres no way to prevent it. Everythings naked
to the world.
So the first reason for access control is to keep client programmers hands
off portions they shouldnt touchparts that are necessary for the internal
operation of the data type but not part of the interface that users need in
order to solve their particular problems. This is actually a service to users
because they can easily see whats important to them and what they can ignore.
The second reason for access control is to allow the library designer to
change the internal workings of the class without worrying about how it will
affect the client programmer. For example, you might implement a particular
class in a simple fashion to ease development, and then later discover that you
need to rewrite it in order to make it run faster. If the interface and
implementation are clearly separated and protected, you can accomplish this
easily.
Java uses three
explicit keywords to set the boundaries in a class: public,
private, and protected. Their use and meaning are quite
straightforward. These access specifiers determine who can use the definitions that
follow. public means the following element is
available to everyone. The private keyword, on the
other hand, means that no one can access that element except you, the creator of
the type, inside methods of that type. private is a brick wall between
you and the client programmer. Someone who tries to access a private
member will get a compile-time error. The protected
keyword acts like private, with the exception that an inheriting class
has access to protected members, but not private members.
Inheritance will be introduced shortly.
Java also has a default access, which comes into play if you dont use one
of the aforementioned specifiers. This is usually called package
access because classes can access the members of other classes in the
same package, but outside of the package those same members appear to be
private.
Once a class has been created and tested, it should (ideally) represent a
useful unit of code. It turns out that this reusability is not nearly so easy to
achieve as many would hope; it takes experience and insight to produce a
reusable object design. But once you have such a design, it begs to be reused.
Code reuse is one of the greatest advantages that object-oriented programming
languages provide.
The simplest way to reuse a class is to just use an
object of that class directly, but you can also place an object of that class
inside a new class. We call this creating a member object. Your new class can
be made up of any number and type of other objects, in any combination that you
need to achieve the functionality desired in your new class. Because you are
composing a new class from existing classes, this concept is called composition (if
the composition happens dynamically, its usually called aggregation). Composition is often referred to as a
has-a relationship, as in a car has an engine.
(This UML diagram indicates composition with the filled diamond, which states
there is one car. I will typically use a simpler form: just a line, without the
diamond, to indicate an association.[6])
Composition comes with a great deal of flexibility. The member objects of
your new class are typically private, making them inaccessible to the client
programmers who are using the class. This allows you to change those members
without disturbing existing client code. You can also change the member objects
at run time, to dynamically change the behavior of your program. Inheritance,
which is described next, does not have this flexibility since the compiler must
place compile-time restrictions on classes created with inheritance.
Because inheritance is so important in object-oriented programming it is
often highly emphasized, and the new programmer can get the idea that
inheritance should be used everywhere. This can result in awkward and overly
complicated designs. Instead, you should first look to composition when creating
new classes, since it is simpler and more flexible. If you take this approach,
your designs will be cleaner. Once youve had some experience, it will be
reasonably obvious when you need inheritance.
By itself, the idea of an object is a convenient tool. It allows you to
package data and functionality together by concept, so you can represent
an appropriate problem-space idea rather than being forced to use the idioms of
the underlying machine. These concepts are expressed as fundamental units in the
programming language by using the class keyword.
It seems a pity, however, to go to all the trouble to create a class and then
be forced to create a brand new one that might have similar functionality. Its
nicer if we can take the existing class, clone it, and then make additions and
modifications to the clone. This is effectively what you get with inheritance, with the exception that if the original
class (called the base class or superclass or parent
class) is changed, the modified clone (called the derived class
or inherited class or subclass or child
class) also reflects those changes.
(The arrow in this UML diagram points from the derived class to the base
class. As you will see, there is commonly more than one derived class.)
A type does more than describe the constraints on a set of objects; it also
has a relationship with other types. Two types can have characteristics and
behaviors in common, but one type may contain more characteristics than another
and may also handle more messages (or handle them differently). Inheritance
expresses this similarity between types by using the concept of base types and
derived types. A base type contains all of the characteristics and behaviors
that are shared among the types derived from it. You create a base type to
represent the core of your ideas about some objects in your system. From the
base type, you derive other types to express the different ways that this core
can be realized.
For example, a trash-recycling machine sorts pieces of trash.
The base type is trash, and each piece of trash has a weight, a value, and so
on, and can be shredded, melted, or decomposed. From this, more specific types
of trash are derived that may have additional characteristics (a bottle has a
color) or behaviors (an aluminum can may be crushed, a steel can is magnetic).
In addition, some behaviors may be different (the value of paper depends on its
type and condition). Using inheritance, you can build a type hierarchy that
expresses the problem youre trying to solve in terms of its types.
A second example is the classic shape example, perhaps used in a
computer-aided design system or game simulation. The base type is shape, and
each shape has a size, a color, a position, and so on. Each shape can be drawn,
erased, moved, colored, etc. From this, specific types of shapes are derived
(inherited)circle, square, triangle, and so on each of which may have
additional characteristics and behaviors. Certain shapes can be flipped, for
example. Some behaviors may be different, such as when you want to calculate the
area of a shape. The type hierarchy embodies both the similarities and
differences between the shapes.
Casting the solution in the same terms as the problem is tremendously
beneficial because you dont need a lot of intermediate models to get from a
description of the problem to a description of the solution. With objects, the
type hierarchy is the primary model, so you go directly from the description of
the system in the real world to the description of the system in code. Indeed,
one of the difficulties people have with object-oriented design is that its too
simple to get from the beginning to the end. A mind trained to look for complex
solutions can initially be stumped by this simplicity.
When you inherit from an existing type, you create a new type. This new type
contains not only all the members of the existing type (although the
private ones are hidden away and inaccessible), but more importantly it
duplicates the interface of the base class. That is, all the messages you can
send to objects of the base class you can also send to objects of the derived
class. Since we know the type of a class by the messages we can send to it, this
means that the derived class is the same type as the base class. In the
previous example, a circle is a shape. This type equivalence via inheritance
is one of the fundamental gateways in understanding the meaning of
object-oriented programming.
Since both the base class and derived class have the same fundamental
interface, there must be some implementation to go along with that interface.
That is, there must be some code to execute when an object receives a particular
message. If you simply inherit a class and dont do anything else, the methods
from the base-class interface come right along into the derived class. That
means objects of the derived class have not only the same type, they also have
the same behavior, which isnt particularly interesting.
You have two ways to differentiate your new derived class from the original
base class. The first is quite straightforward: You simply add brand new methods
to the derived class. These new methods are not part of the base class
interface. This means that the base class simply didnt do as much as you wanted
it to, so you added more methods. This simple and primitive use for inheritance
is, at times, the perfect solution to your problem. However, you should look
closely for the possibility that your base class might also need these
additional methods. This process of discovery and iteration of your design
happens regularly in object-oriented programming.
Although inheritance may sometimes imply (especially in Java, where the
keyword for inheritance is extends) that you are going to add new
methods to the interface, thats not necessarily true. The second and more
important way to differentiate your new class is to change the behavior
of an existing base-class method. This is referred to as overriding that method.
To override a method, you simply create a new definition for the method in
the derived class. Youre saying, Im using the same interface method here, but
I want it to do something different for my new type.
Theres a certain debate that can occur about
inheritance: Should inheritance override only base-class methods (and not
add new methods that arent in the base class)? This would mean that the derived
type is exactly the same type as the base class since it has exactly the
same interface. As a result, you can exactly substitute an object of the derived
class for an object of the base class. This can be thought of as pure
substitution, and its often referred to as the substitution principle. In a sense, this is the ideal
way to treat inheritance. We often refer to the relationship between the base
class and derived classes in this case as an is-a relationship, because
you can say a circle is a shape. A test for inheritance is to determine
whether you can state the is-a relationship about the classes and have it make
sense.
There are times when you must add new interface elements to a derived type,
thus extending the interface and creating a new type. The new type can still be
substituted for the base type, but the substitution isnt perfect because your
new methods are not accessible from the base type. This can be described as an
is-like-a relationship (my term). The new type has the interface of the
old type but it also contains other methods, so you cant really say its
exactly the same. For example, consider an air conditioner. Suppose your house
is wired with all the controls for cooling; that is, it has an interface that
allows you to control cooling. Imagine that the air conditioner breaks down and
you replace it with a heat pump, which can both heat and cool. The heat pump
is-like-an air conditioner, but it can do more. Because the control
system of your house is designed only to control cooling, it is restricted to
communication with the cooling part of the new object. The interface of the new
object has been extended, and the existing system doesnt know about anything
except the original interface.
Of course, once you see this design it becomes clear that the base class
cooling system is not general enough, and should be renamed to temperature
control system so that it can also include heatingat which point the
substitution principle will work. However, this diagram is an example of what
can happen with design in the real world.
When you see the substitution principle its easy to feel like this approach
(pure substitution) is the only way to do things, and in fact it is nice
if your design works out that way. But youll find that there are times when
its equally clear that you must add new methods to the interface of a derived
class. With inspection both cases should be reasonably obvious.
When dealing with type hierarchies, you often want to
treat an object not as the specific type that it is, but instead as its base
type. This allows you to write code that doesnt depend on specific types. In
the shape example, methods manipulate generic shapes without respect to whether
theyre circles, squares, triangles, or some shape that hasnt even been defined
yet. All shapes can be drawn, erased, and moved, so these methods simply send a
message to a shape object; they dont worry about how the object copes with the
message.
Such code is unaffected by the addition of new types, and adding new types is
the most common way to extend an object-oriented program to handle new
situations. For example, you can derive a new subtype of shape called
pentagon without modifying the methods that deal only with generic
shapes. This ability to easily extend a design by deriving new subtypes is one
of the essential ways to encapsulate change. This greatly improves designs while
reducing the cost of software maintenance.
Theres a problem, however, with attempting to treat derived-type objects as
their generic base types (circles as shapes, bicycles as vehicles, cormorants as
birds, etc.). If a method is going to tell a generic shape to draw itself, or a
generic vehicle to steer, or a generic bird to move, the compiler cannot know at
compile time precisely what piece of code will be executed. Thats the whole
pointwhen the message is sent, the programmer doesnt want to know what
piece of code will be executed; the draw method can be applied equally to a
circle, a square, or a triangle, and the object will execute the proper code
depending on its specific type. If you dont have to know what piece of code
will be executed, then when you add a new subtype, the code it executes can be
different without requiring changes to the method call. Therefore, the compiler
cannot know precisely what piece of code is executed, so what does it do? For
example, in the following diagram the BirdController object just works
with generic Bird objects and does not know what exact type they are.
This is convenient from BirdControllers perspective because it doesnt
have to write special code to determine the exact type of Bird its
working with or that Birds behavior. So how does it happen that, when
move( ) is called while ignoring the specific type of Bird,
the right behavior will occur (a Goose runs, flies, or swims, and a
Penguin runs or swims)?
The answer is the primary twist in object-oriented programming: the compiler
cannot make a function call in the traditional sense. The function call
generated by a non-OOP compiler causes what is called early binding, a term you may not have heard before
because youve never thought about it any other way. It means the compiler
generates a call to a specific function name and the linker resolves this call
to the absolute address of the code to be executed. In OOP, the program cannot
determine the address of the code until run time, so some other scheme is
necessary when a message is sent to a generic object.
To solve the problem, object-oriented languages use the concept of late binding. When you send a
message to an object, the code being called isnt determined until run time. The
compiler does ensure that the method exists and performs type checking on the
arguments and return value (a language in which this isnt true is called weakly typed), but it doesnt know
the exact code to execute.
To perform late binding, Java uses a special bit of code in lieu of the
absolute call. This code calculates the address of the method body, using
information stored in the object (this process is covered in great detail in
Chapter 7). Thus, each object can behave differently according to the contents
of that special bit of code. When you send a message to an object, the object
actually does figure out what to do with that message.
In some languages you must explicitly state that you want a method to have
the flexibility of late-binding properties (C++ uses the virtual keyword
to do this). In these languages, by default, methods are not dynamically
bound. In Java, dynamic binding is the default behavior and you dont need to
remember to add any extra keywords in order to get polymorphism.
Consider the shape example. The family of classes (all based on the same
uniform interface) was diagrammed earlier in this chapter. To demonstrate
polymorphism, we want to write a single piece of code that ignores the specific
details of type and talks only to the base class. That code is decoupled from type-specific information and thus is
simpler to write and easier to understand. And, if a new typea Hexagon,
for exampleis added through inheritance, the code you write will work just as
well for the new type of Shape as it did on the existing types. Thus, the
program is extensible.
If you write a method in Java (as you will soon learn how to do):
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}
This method speaks to any Shape, so it is independent of the specific
type of object that its drawing and erasing. If some other part of the program
uses the doStuff( ) method:
Circle c = new Circle(); Triangle t = new Triangle(); Line l = new Line(); doStuff(c); doStuff(t); doStuff(l);
the calls to doStuff( ) automatically work correctly, regardless
of the exact type of the object.
This is a rather amazing trick. Consider the line:
doStuff(c);
Whats happening here is that a Circle is being passed into a method
thats expecting a Shape. Since a Circle is a Shape
it can be treated as one by doStuff( ). That is, any message that
doStuff( ) can send to a Shape, a Circle can accept.
So it is a completely safe and logical thing to do.
We call this process of treating a derived type as though it were its base
type upcasting. The name cast is used in the sense of casting into a mold and
the up comes from the way the inheritance diagram is typically arranged,
with the base type at the top and the derived classes fanning out downward.
Thus, casting to a base type is moving up the inheritance diagram: upcasting.
An object-oriented program contains some upcasting somewhere, because thats
how you decouple yourself from knowing about the exact type youre working with.
Look at the code in doStuff( ):
s.erase(); // ... s.draw();
Notice that it doesnt say If youre a Circle, do this, if youre a
Square, do that, etc. If you write that kind of code, which checks for
all the possible types that a Shape can actually be, its messy and you
need to change it every time you add a new kind of Shape. Here, you just
say Youre a shape, I know you can erase( ) and draw( )
yourself, do it, and take care of the details correctly.
Whats impressive about the code in doStuff( ) is that, somehow,
the right thing happens. Calling draw( ) for Circle causes
different code to be executed than when calling draw( ) for a
Square or a Line, but when the draw( ) message is sent
to an anonymous Shape, the correct behavior occurs based on the actual
type of the Shape. This is amazing because, as mentioned earlier, when
the Java compiler is compiling the code for doStuff( ), it cannot
know exactly what types it is dealing with. So ordinarily, youd expect it to
end up calling the version of erase( ) and draw( ) for
the base class Shape, and not for the specific Circle,
Square, or Line. And yet the right thing happens because of
polymorphism. The compiler and run-time system handle the details; all you need
to know right now is that it does happen, and more importantly, how to design
with it. When you send a message to an object, the object will do the right
thing, even when upcasting is involved.
Often in a design, you want the base class to present only an
interface for its derived classes. That is, you dont want anyone to actually
create an object of the base class, only to upcast to it so that its interface
can be used. This is accomplished by making that class abstract by using
the abstract keyword. If anyone tries to make an object of an
abstract class, the compiler prevents it. This is a tool to enforce a
particular design.
You can also use the abstract keyword to describe a method that hasnt
been implemented yetas a stub indicating here is an interface method for all
types inherited from this class, but at this point I dont have any
implementation for it. An abstract method may be created only inside an
abstract class. When the class is inherited, that method must be
implemented, or the inheriting class becomes abstract as well. Creating
an abstract method allows you to put a method in an interface without
being forced to provide a possibly meaningless body of code for that method.
The interface keyword takes the concept of an abstract class
one step further by preventing any method definitions at all. The
interface is a very handy and commonly used tool, as it provides the
perfect separation of interface and implementation. In addition, you can combine
many interfaces together, if you wish, whereas inheriting from multiple regular
classes or abstract classes is not possible.
Technically, OOP is just about abstract data typing, inheritance, and
polymorphism, but other issues can be at least as important. This section will
cover these issues.
One of the most important factors of objects is the way they are created and
destroyed. Where is the data for an object and how is the lifetime of the object
controlled? There are different philosophies at work here. C++ takes the
approach that control of efficiency is the most important issue, so it gives the
programmer a choice. For maximum run-time speed, the storage and lifetime can be
determined while the program is being written, by placing the objects on the
stack (these are sometimes called automatic or scoped variables)
or in the static storage area. This places a priority on the speed of storage
allocation and release, and control of these can be very valuable in some
situations. However, you sacrifice flexibility because you must know the exact
quantity, lifetime, and type of objects while you're writing the program. If you
are trying to solve a more general problem such as computer-aided design,
warehouse management, or air-traffic control, this is too restrictive.
The second approach is to create objects dynamically in a pool of memory
called the heap. In this approach, you don't know until run time how many
objects you need, what their lifetime is, or what their exact type is. Those are
determined at the spur of the moment while the program is running. If you need a
new object, you simply make it on the heap at the point that you need it.
Because the storage is managed dynamically, at run time, the amount of time
required to allocate storage on the heap can be noticeably longer than the time
to create storage on the stack. (Creating storage on the stack is often a single
assembly instruction to move the stack pointer down and another to move it back
up. The time to create heap storage depends on the design of the storage
mechanism.) The dynamic approach makes the generally logical assumption that
objects tend to be complicated, so the extra overhead of finding storage and
releasing that storage will not have an important impact on the creation of an
object. In addition, the greater flexibility is essential to solve the general
programming problem.
Java uses the second approach, exclusively.[7] Every time you want
to create an object, you use the new keyword to build a dynamic instance
of that object.
There's another issue, however, and that's the lifetime of an object. With
languages that allow objects to be created on the stack, the compiler determines
how long the object lasts and can automatically destroy it. However, if you
create it on the heap the compiler has no knowledge of its lifetime. In a
language like C++, you must determine programmatically when to destroy the
object, which can lead to memory leaks if you dont do it correctly (and this is
a common problem in C++ programs). Java provides a feature called a garbage
collector that automatically discovers when an object is no longer in use
and destroys it. A garbage collector is much more convenient because it reduces
the number of issues that you must track and the code you must write. More
important, the garbage collector provides a much higher level of insurance
against the insidious problem of memory leaks (which has brought many a C++
project to its knees).
If you dont know how many objects youre going to need to solve a particular
problem, or how long they will last, you also dont know how to store those
objects. How can you know how much space to create for those objects? You cant,
since that information isnt known until run time.
The solution to most problems in object-oriented design seems flippant: You
create another type of object. The new type of object that solves this
particular problem holds references to other objects. Of course, you can do the
same thing with an array, which is available in most languages. But this new
object, generally called a container (also called a collection,
but the Java library uses that term in a different sense so this book will use
container), will expand itself whenever necessary to accommodate everything
you place inside it. So you dont need to know how many objects youre going to
hold in a container. Just create a container object and let it take care of the
details.
Fortunately, a good OOP language comes with a set of containers as part of
the package. In C++, its part of the Standard C++ Library and is sometimes
called the Standard Template Library (STL). Object Pascal has containers in its
Visual Component Library (VCL). Smalltalk has a very complete set of containers.
Java also has containers in its standard library. In some libraries, a generic
container is considered good enough for all needs, and in others (Java, for
example) the library has different types of containers for different needs:
several different kinds of List classes (to hold sequences), Map
classes (also known as associative arrays, to associate objects with
other objects), and Set classes (to hold one of each type of object).
Container libraries may also include queues, trees, stacks, etc.
All containers have some way to put things in and get things out; there are
usually methods to add elements to a container, and others to fetch those
elements back out. But fetching elements can be more problematic, because a
single-selection method is restrictive. What if you want to manipulate or
compare a set of elements in the container instead of just one?
The solution is an iterator, which is an object whose job is to select
the elements within a container and present them to the user of the iterator. As
a class, it also provides a level of abstraction. This abstraction can be used
to separate the details of the container from the code thats accessing that
container. The container, via the iterator, is abstracted to be simply a
sequence. The iterator allows you to traverse that sequence without worrying
about the underlying structurethat is, whether its an ArrayList, a
LinkedList, a Stack, or something else. This gives you the
flexibility to easily change the underlying data structure without disturbing
the code in your program. Java began (in version 1.0 and 1.1) with a standard
iterator, called Enumeration, for all of its container classes. Java 2
added a much more complete container library that contains an iterator called
Iterator that does more than the older Enumeration.
From a design standpoint, all you really want is a sequence that can be
manipulated to solve your problem. If a single type of sequence satisfied all of
your needs, thered be no reason to have different kinds. There are two reasons
that you need a choice of containers. First, containers provide different types
of interfaces and external behavior. A stack has a different interface and
behavior than that of a queue, which is different from that of a set or a list.
One of these might provide a more flexible solution to your problem than the
other. Second, different containers have different efficiencies for certain
operations. The best example compares two types of List: an
ArrayList and a LinkedList. Both are simple sequences that can
have identical interfaces and external behaviors. But certain operations can
have radically different costs. Randomly accessing elements in an
ArrayList is a constant-time operation; it takes the same amount of time
regardless of the element you select. However, in a LinkedList it is
expensive to move through the list to randomly select an element, and it takes
longer to find an element that is farther down the list. On the other hand, if
you want to insert an element in the middle of a sequence, its cheaper in a
LinkedList than in an ArrayList. These and other operations have
different efficiencies depending on the underlying structure of the sequence. In
the design phase, you might start with a LinkedList and, when tuning for
performance, change to an ArrayList. Because of the abstraction via the
base class List and via iterators, you can change from one to the other
with minimal impact on your code.
One of the issues in OOP that has become especially prominent since the
introduction of C++ is whether all classes should ultimately be inherited from a
single base class. In Java (as with virtually all other OOP languages
except for C++) the answer is yes, and the name of this ultimate base
class is simply Object. It turns out that the benefits of the singly
rooted hierarchy are many.
All objects in a singly rooted hierarchy have an interface in common, so they
are all ultimately the same fundamental type. The alternative (provided by C++)
is that you dont know that everything is the same basic type. From a
backward-compatibility standpoint this fits the model of C better and can be
thought of as less restrictive, but when you want to do full-on object-oriented
programming you must then build your own hierarchy to provide the same
convenience thats built into other OOP languages. And in any new class library
you acquire, some other incompatible interface will be used. It requires effort
(and possibly multiple inheritance) to work the new interface into your design.
Is the extra flexibility of C++ worth it? If you need itif you have a large
investment in Cits quite valuable. If youre starting from scratch, other
alternatives such as Java can often be more productive.
All objects in a singly rooted hierarchy (such as Java provides) can be
guaranteed to have certain functionality. You know you can perform certain basic
operations on every object in your system. A singly rooted hierarchy, along with
creating all objects on the heap, greatly simplifies argument passing (one of
the more complex topics in C++).
A singly rooted hierarchy makes it much easier to implement a garbage
collector (which is conveniently built into Java). The necessary support can be
installed in the base class, and the garbage collector can thus send the
appropriate messages to every object in the system. Without a singly rooted
hierarchy and a system to manipulate an object via a reference, it is difficult
to implement a garbage collector.
Since run time type information is guaranteed to be in all objects, youll
never end up with an object whose type you cannot determine. This is especially
important with system-level operations, such as exception handling, and to allow
greater flexibility in programming.
To make these containers reusable, they hold the one universal type in Java:
Object. The singly rooted hierarchy means that everything is an
Object, so a container that holds Objects can hold
anything.[8]
This makes containers easy to reuse.
To use such a container, you simply add object references to it and later ask
for them back. But, since the container holds only Objects, when you add
your object reference into the container it is upcast to Object, thus
losing its identity. When you fetch it back, you get an Object reference,
and not a reference to the type that you put in. So how do you turn it back into
something that has the useful interface of the object that you put into the
container?
Here, the cast is used again, but this time youre not casting up the
inheritance hierarchy to a more general type. Instead, you cast down the
hierarchy to a more specific type. This manner of casting is called
downcasting. With upcasting, you know, for example, that a Circle
is a type of Shape so its safe to upcast, but you dont know that an
Object is necessarily a Circle or a Shape so its hardly
safe to downcast unless you know exactly what youre dealing with.
Its not completely dangerous, however, because if you downcast to the wrong
thing youll get a run-time error called an exception, which will be
described shortly. When you fetch object references from a container, though,
you must have some way to remember exactly what they are so you can perform a
proper downcast.
Downcasting and the run-time checks require extra time for the running
program and extra effort from the programmer. Wouldnt it make sense to somehow
create the container so that it knows the types that it holds, eliminating the
need for the downcast and a possible mistake? The solution is called a
parameterized type mechanism. A parameterized type is a class that the
compiler can automatically customize to work with particular types. For example,
with a parameterized container, the compiler could customize that container so
that it would accept only Shapes and fetch only Shapes.
Parameterized types are an important part of C++, partly because C++ has no
singly rooted hierarchy. In C++, the keyword that implements parameterized types
is template. Java currently has no parameterized types since it is possible
for it to get byhowever awkwardlyusing the singly rooted hierarchy. However, a
current proposal for parameterized types uses a syntax that is strikingly
similar to C++ templates, and we can expect to see parameterized types (which
will be called generics) in the next version of Java.
Each object requires resources, most notably memory, in order to exist. When
an object is no longer needed it must be cleaned up so that these resources are
released for reuse. In simple programming situations the question of how an
object is cleaned up doesnt seem too challenging: you create the object, use it
for as long as its needed, and then it should be destroyed. However, its not
hard to encounter situations that are more complex.
Suppose, for example, you are designing a system to manage air traffic for an
airport. (The same model might also work for managing crates in a warehouse, or
a video rental system, or a kennel for boarding pets.) At first it seems simple:
Make a container to hold airplanes, then create a new airplane and place it in
the container for each airplane that enters the air-traffic-control zone. For
cleanup, simply delete the appropriate airplane object when a plane leaves the
zone.
But perhaps you have some other system to record data about the planes;
perhaps data that doesnt require such immediate attention as the main
controller function. Maybe its a record of the flight plans of all the small
planes that leave the airport. So you have a second container of small planes,
and whenever you create a plane object you also put it in this second container
if its a small plane. Then some background process performs operations on the
objects in this container during idle moments.
Now the problem is more difficult: How can you possibly know when to destroy
the objects? When youre done with the object, some other part of the system
might not be. This same problem can arise in a number of other situations, and
in programming systems (such as C++) in which you must explicitly delete an
object when youre done with it this can become quite complex.
With Java, the garbage collector is designed to take care of the problem of
releasing the memory (although this doesnt include other aspects of cleaning up
an object). The garbage collector knows when an object is no longer in use,
and it then automatically releases the memory for that object. This (combined
with the fact that all objects are inherited from the single root class
Object and that you can create objects only one wayon the heap) makes
the process of programming in Java much simpler than programming in C++. You
have far fewer decisions to make and hurdles to overcome.
If all this is such a good idea, why didnt they do the same thing in C++?
Well of course theres a price you pay for all this programming convenience, and
that price is run time overhead. As mentioned before, in C++ you can create
objects on the stack, and in this case theyre automatically cleaned up (but you
dont have the flexibility of creating as many as you want at run time).
Creating objects on the stack is the most efficient way to allocate storage for
objects and to free that storage. Creating objects on the heap can be much more
expensive. Always inheriting from a base class and making all method calls
polymorphic also exacts a small toll. But the garbage collector is a particular
problem because you never quite know when its going to start up or how long it
will take. This means that theres an inconsistency in the rate of execution of
a Java program, so you cant use it in certain situations, such as when the rate
of execution of a program is uniformly critical. (These are generally called
real time programs, although not all real time programming problems are this
stringent.)
The designers of the C++ language, trying to woo C programmers (and most
successfully, at that), did not want to add any features to the language that
would impact the speed or the use of C++ in any situation where programmers
might otherwise choose C. This goal was realized, but at the price of greater
complexity when programming in C++. Java is simpler than C++, but the trade-off
is in efficiency and sometimes applicability. For a significant portion of
programming problems, however, Java is the superior choice.
Ever since the beginning of programming languages, error handling has been
one of the most difficult issues. Because its so hard to design a good error
handling scheme, many languages simply ignore the issue, passing the problem on
to library designers who come up with halfway measures that work in many
situations but that can easily be circumvented, generally by just ignoring them.
A major problem with most error handling schemes is that they rely on programmer
vigilance in following an agreed-upon convention that is not enforced by the
language. If the programmer is not vigilantoften the case if they are in a
hurrythese schemes can easily be forgotten.
Exception handling wires error handling directly into the programming
language and sometimes even the operating system. An exception is an object that
is thrown from the site of the error and can be caught by an appropriate
exception handler designed to handle that particular type of error. Its as if
exception handling is a different, parallel path of execution that can be taken
when things go wrong. And because it uses a separate execution path, it doesnt
need to interfere with your normally executing code. This makes that code
simpler to write because you arent constantly forced to check for errors. In
addition, a thrown exception is unlike an error value thats returned from a
method or a flag thats set by a method in order to indicate an error
conditionthese can be ignored. An exception cannot be ignored, so its
guaranteed to be dealt with at some point. Finally, exceptions provide a way to
reliably recover from a bad situation. Instead of just exiting the program, you
are often able to set things right and restore execution, which produces much
more robust programs.
Javas exception handling stands out among programming languages, because in
Java, exception handling was wired in from the beginning and youre forced to
use it. If you dont write your code to properly handle exceptions, youll get a
compile-time error message. This guaranteed consistency can sometimes make error
handling much easier.
Its worth noting that exception handling isnt an object-oriented feature,
although in object-oriented languages the exception is normally represented by
an object. Exception handling existed before object-oriented languages.
A fundamental concept in computer programming is the idea of handling more
than one task at a time. Many programming problems require that the program be
able to stop what its doing, deal with some other problem, and then return to
the main process. The solution has been approached in many ways. Initially,
programmers with low-level knowledge of the machine wrote interrupt service
routines, and the suspension of the main process was initiated through a
hardware interrupt. Although this worked well, it was difficult and nonportable,
so it made moving a program to a new type of machine slow and expensive.
Sometimes, interrupts are necessary for handling time-critical tasks, but
theres a large class of problems in which youre simply trying to partition the
problem into separately running pieces so that the whole program can be more
responsive. Within a program, these separately running pieces are called
threads, and the general concept is called concurrency or
multithreading. A common example of multithreading is the user interface.
By using threads, a user can press a button and get a quick response rather than
being forced to wait until the program finishes its current task.
Ordinarily, threads are just a way to allocate the time of a single
processor. But if the operating system supports multiple processors, each thread
can be assigned to a different processor, and they can truly run in parallel.
One of the convenient features of multithreading at the language level is that
the programmer doesnt need to worry about whether there are many processors or
just one. The program is logically divided into threads and if the machine has
more than one processor, then the program runs faster, without any special
adjustments.
All this makes threading sound pretty simple. There is a catch: shared
resources. If you have more than one thread running thats expecting to access
the same resource, you have a problem. For example, two processes cant
simultaneously send information to a printer. To solve the problem, resources
that can be shared, such as the printer, must be locked while they are being
used. So a thread locks a resource, completes its task, and then releases the
lock so that someone else can use the resource.
Javas threading is built into the language, which makes a complicated
subject much simpler. The threading is supported on an object level, so one
thread of execution is represented by one object. Java also provides limited
resource locking. It can lock the memory of any object (which is, after all, one
kind of shared resource) so that only one thread can use it at a time. This is
accomplished with the synchronized keyword. Other
types of resources must be locked explicitly by the programmer, typically by
creating an object to represent the lock that all threads must check before
accessing that resource.
When you create an object, it exists for as long as you need it, but under no
circumstances does it exist when the program terminates. While this makes sense
at first, there are situations in which it would be incredibly useful if an
object could exist and hold its information even while the program wasnt
running. Then the next time you started the program, the object would be there
and it would have the same information it had the previous time the program was
running. Of course, you can get a similar effect by writing the information to a
file or to a database, but in the spirit of making everything an object, it
would be quite convenient to be able to declare an object persistent and have
all the details taken care of for you.
Java provides support for lightweight persistence, which means that you can
easily store objects on disk and later retrieve them. The reason its
lightweight is that youre still forced to make explicit calls to do the
storage and retrieval. Lightweight persistence can be implemented both through
object serialization (shown in Chapter 12) and Java Data Objects
(JDO, shown in Thinking in Enterprise Java).
If Java is, in fact, yet another computer programming language, you may
question why it is so important and why it is being promoted as a revolutionary
step in computer programming. The answer isnt immediately obvious if youre
coming from a traditional programming perspective. Although Java is very useful
for solving traditional standalone programming problems, it is also important
because it will solve programming problems on the World Wide Web.
The Web can seem a bit of a mystery at first, with all this talk of
surfing, presence, and home pages. Its helpful to step back and see what
it really is, but to do this you must understand client/server systems, another
aspect of computing thats full of confusing issues.
The primary idea of a client/server system is that you have a central
repository of informationsome kind of data, often in a databasethat you want
to distribute on demand to some set of people or machines. A key to the
client/server concept is that the repository of information is centrally located
so that it can be changed and so that those changes will propagate out to the
information consumers. Taken together, the information repository, the software
that distributes the information, and the machine(s) where the information and
software reside is called the server. The software that resides on the remote
machine, communicates with the server, fetches the information, processes it,
and then displays it on the remote machine is called the client.
The basic concept of client/server computing, then, is not so complicated.
The problems arise because you have a single server trying to serve many clients
at once. Generally, a database management system is involved, so the designer
balances the layout of data into tables for optimal use. In addition, systems
often allow a client to insert new information into a server. This means you
must ensure that one clients new data doesnt walk over another clients new
data, or that data isnt lost in the process of adding it to the database (this
is called transaction processing). As client software changes, it must be built,
debugged, and installed on the client machines, which turns out to be more
complicated and expensive than you might think. Its especially problematic to
support multiple types of computers and operating systems. Finally, theres the
all-important performance issue: You might have hundreds of clients making
requests of your server at any one time, so any small delay is crucial. To
minimize latency, programmers work hard to offload processing tasks, often to
the client machine, but sometimes to other machines at the server site, using
so-called middleware. (Middleware is also used to improve
maintainability.)
The simple idea of distributing information has so many layers of complexity
that the whole problem can seem hopelessly enigmatic. And yet its crucial:
Client/server computing accounts for roughly half of all programming activities.
Its responsible for everything from taking orders and credit-card transactions
to the distribution of any kind of datastock market, scientific, government,
you name it. What weve come up with in the past is individual solutions to
individual problems, inventing a new solution each time. These were hard to
create and hard to use, and the user had to learn a new interface for each one.
The entire client/server problem needs to be solved in a big way.
The Web is actually one giant client/server system. Its a bit worse than
that, since you have all the servers and clients coexisting on a single network
at once. You dont need to know that, because all you care about is connecting
to and interacting with one server at a time (even though you might be hopping
around the world in your search for the correct server).
Initially it was a simple one-way process. You made a request of a server and
it handed you a file, which your machines browser software (i.e., the client)
would interpret by formatting onto your local machine. But in short order people
began wanting to do more than just deliver pages from a server. They wanted full
client/server capability so that the client could feed information back to the
server, for example, to do database lookups on the server, to add new
information to the server, or to place an order (which required more security
than the original systems offered). These are the changes weve been seeing in
the development of the Web.
The Web browser was a big step forward: the concept that one piece of
information could be displayed on any type of computer without change. However,
browsers were still rather primitive and rapidly bogged down by the demands
placed on them. They werent particularly interactive, and tended to clog up
both the server and the Internet because any time you needed to do something
that required programming you had to send information back to the server to be
processed. It could take many seconds or minutes to find out you had misspelled
something in your request. Since the browser was just a viewer it couldnt
perform even the simplest computing tasks. (On the other hand, it was safe,
because it couldnt execute any programs on your local machine that might
contain bugs or viruses.)
To solve this problem, different approaches have been taken. To begin with,
graphics standards have been enhanced to allow better animation and video within
browsers. The remainder of the problem can be solved only by incorporating the
ability to run programs on the client end, under the browser. This is called
client-side programming.
The Webs initial server-browser design provided for interactive content, but
the interactivity was completely provided by the server. The server produced
static pages for the client browser, which would simply interpret and display
them. Basic HyperText Markup Language (HTML) contains simple mechanisms
for data gathering: text-entry boxes, check boxes, radio boxes, lists and
drop-down lists, as well as a button that can only be programmed to reset the
data on the form or submit the data on the form back to the server. This
submission passes through the Common Gateway Interface (CGI) provided on
all Web servers. The text within the submission tells CGI what to do with it.
The most common action is to run a program located on the server in a directory
thats typically called cgi-bin. (If you watch the address window at the top
of your browser when you push a button on a Web page, you can sometimes see
cgi-bin within all the gobbledygook there.) These programs can be written in
most languages. Perl has been a common choice because it is designed for text
manipulation and is interpreted, so it can be installed on any server regardless
of processor or operating system. However, Python (my favoritesee
www.Python.org) has been making inroads because of its greater power and
simplicity.
Many powerful Web sites today are built strictly on CGI, and you can in fact
do nearly anything with CGI. However, Web sites built on CGI programs can
rapidly become overly complicated to maintain, and there is also the problem of
response time. The response of a CGI program depends on how much data must be
sent, as well as the load on both the server and the Internet. (On top of this,
starting a CGI program tends to be slow.) The initial designers of the Web did
not foresee how rapidly this bandwidth would be exhausted for the kinds of
applications people developed. For example, any sort of dynamic graphing is
nearly impossible to perform with consistency because a Graphics Interchange
Format (GIF) file must be created and moved from the server to the client
for each version of the graph. And youve no doubt had direct experience with
something as simple as validating the data on an input form. You press the
submit button on a page; the data is shipped back to the server; the server
starts a CGI program that discovers an error, formats an HTML page informing you
of the error, and then sends the page back to you; you must then back up a page
and try again. Not only is this slow, its inelegant.
The solution is client-side programming. Most machines that run Web browsers
are powerful engines capable of doing vast work, and with the original static
HTML approach they are sitting there, just idly waiting for the server to dish
up the next page. Client-side programming means that the Web browser is
harnessed to do whatever work it can, and the result for the user is a much
speedier and more interactive experience at your Web site.
The problem with discussions of client-side programming is that they arent
very different from discussions of programming in general. The parameters are
almost the same, but the platform is different; a Web browser is like a limited
operating system. In the end, you must still program, and this accounts for the
dizzying array of problems and solutions produced by client-side programming.
The rest of this section provides an overview of the issues and approaches in
client-side programming.
One of the most significant steps forward in client-side programming is the
development of the plug-in. This is a way for a programmer to add new
functionality to the browser by downloading a piece of code that plugs itself
into the appropriate spot in the browser. It tells the browser from now on you
can perform this new activity. (You need to download the plug-in only once.)
Some fast and powerful behavior is added to browsers via plug-ins, but writing a
plug-in is not a trivial task, and isnt something youd want to do as part of
the process of building a particular site. The value of the plug-in for
client-side programming is that it allows an expert programmer to develop a new
language and add that language to a browser without the permission of the
browser manufacturer. Thus, plug-ins provide a back door that allows the
creation of new client-side programming languages (although not all languages
are implemented as plug-ins).
Plug-ins resulted in an explosion of scripting languages. With a scripting
language, you embed the source code for your client-side program directly into
the HTML page, and the plug-in that interprets that language is automatically
activated while the HTML page is being displayed. Scripting languages tend to be
reasonably easy to understand and, because they are simply text that is part of
an HTML page, they load very quickly as part of the single server hit required
to procure that page. The trade-off is that your code is exposed for everyone to
see (and steal). Generally, however, you arent doing amazingly sophisticated
things with scripting languages, so this is not too much of a hardship.
This points out that the scripting languages used inside Web browsers are
really intended to solve specific types of problems, primarily the creation of
richer and more interactive graphical user interfaces (GUIs). However, a
scripting language might solve 80 percent of the problems encountered in
client-side programming. Your problems might very well fit completely within
that 80 percent, and since scripting languages can allow easier and faster
development, you should probably consider a scripting language before looking at
a more involved solution such as Java or ActiveX programming.
The most commonly discussed browser scripting languages are JavaScript (which
has nothing to do with Java; its named that way just to grab some of Javas
marketing momentum), VBScript (which looks like Visual BASIC), and Tcl/Tk, which
comes from the popular cross-platform GUI-building language. There are others
out there, and no doubt more in development.
JavaScript is probably the most commonly supported. It comes built into both
Netscape Navigator and the Microsoft Internet Explorer (IE). Unfortunately, the
flavor of JavaScript on the two browsers can vary widely (the Mozilla browser,
freely downloadable from www.Mozilla.org, supports the ECMAScript standard,
which may one day become universally supported). In addition, there are probably
more JavaScript books available than there are for the other browser languages,
and some tools automatically create pages using JavaScript. However, if youre
already fluent in Visual BASIC or Tcl/Tk, youll be more productive using those
scripting languages rather than learning a new one. (Youll have your hands full
dealing with the Web issues already.)
If a scripting language can solve 80 percent of the client-side programming
problems, what about the other 20 percentthe really hard stuff? Java is a
popular solution for this. Not only is it a powerful programming language built
to be secure, cross-platform, and international, but Java is being continually
extended to provide language features and libraries that elegantly handle
problems that are difficult in traditional programming languages, such as
multithreading, database access, network programming, and distributed computing.
Java allows client-side programming via the applet and with Java Web
Start.
An applet is a mini-program that will run only under a Web browser. The
applet is downloaded automatically as part of a Web page (just as, for example,
a graphic is automatically downloaded). When the applet is activated, it
executes a program. This is part of its beautyit provides you with a way to
automatically distribute the client software from the server at the time the
user needs the client software, and no sooner. The user gets the latest version
of the client software without fail and without difficult reinstallation.
Because of the way Java is designed, the programmer needs to create only a
single program, and that program automatically works with all computers that
have browsers with built-in Java interpreters. (This safely includes the vast
majority of machines.) Since Java is a full-fledged programming language, you
can do as much work as possible on the client before and after making requests
of the server. For example, you wont need to send a request form across the
Internet to discover that youve gotten a date or some other parameter wrong,
and your client computer can quickly do the work of plotting data instead of
waiting for the server to make a plot and ship a graphic image back to you. Not
only do you get the immediate win of speed and responsiveness, but the general
network traffic and load on servers can be reduced, preventing the entire
Internet from slowing down.
One advantage a Java applet has over a scripted program is that its in
compiled form, so the source code isnt available to the client. On the other
hand, a Java applet can be decompiled without too much trouble, but hiding your
code is often not an important issue. Two other factors can be important. As you
will see later in this book, a compiled Java applet can require extra time to
download, if it is large. A scripted program will just be integrated into the
Web page as part of its text (and will generally be smaller and reduce server
hits). This could be important to the responsiveness of your Web site. Another
factor is the all-important learning curve. Regardless of what youve heard,
Java is not a trivial language to learn. If youre a VISUAL BASIC programmer,
moving to VBScript will be your fastest solution (assuming you can constrain
your customers to Windows platforms), and since it will probably solve most
typical client/server problems, you might be hard pressed to justify learning
Java. If youre experienced with a scripting language you will certainly benefit
from looking at JavaScript or VBScript before committing to Java, because they
might fit your needs handily and youll be more productive sooner.
For awhile, the main competitor to Java applets was Microsofts ActiveX,
although it required that the client be running Windows. Since then, Microsoft
has produced a full competitor to Java in the form of the .NET platform and the C#
programming language. The .NET platform is roughly the same as the
Java virtual machine and Java libraries, and C# bears unmistakable
similarities to Java. This is certainly the best work that Microsoft has done in
the arena of programming languages and programming environments. Of course, they
had the considerable advantage of being able to see what worked well and what
didnt work so well in Java, and build upon that, but build they have. This is
the first time since its inception that Java has had any real competition, and
if all goes well, the result will be that the Java designers at Sun will take a
hard look at C# and why programmers might want to move to it, and will respond
by making fundamental improvements to Java.
Currently, the main vulnerability and important question concerning
.NET is whether Microsoft will allow it to be completely ported to
other platforms. They claim theres no problem doing this, and the Mono project
(www.go-mono.com) has a partial implementation of .NET working on Linux, but until the implementation is
complete and Microsoft has not decided to squash any part of it, .NET as
a cross-platform solution is still a risky bet.
To learn more about .NET and C#, see Thinking in C# by Larry OBrien and Bruce Eckel,
Prentice Hall 2003.
Automatically downloading and running programs across the Internet can sound
like a virus-builders dream. If you click on a Web site, you might
automatically download any number of things along with the HTML page: GIF files,
script code, compiled Java code, and ActiveX components. Some of these are
benign; GIF files cant do any harm, and scripting languages are generally
limited in what they can do. Java was also designed to run its applets within a
sandbox of safety, which prevents it from writing to disk or accessing memory
outside the sandbox.
Microsofts ActiveX is at the opposite end of the spectrum. Programming with
ActiveX is like programming Windowsyou can do anything you want. So if you
click on a page that downloads an ActiveX component, that component might cause
damage to the files on your disk. Of course, programs that you load onto your
computer that are not restricted to running inside a Web browser can do the same
thing. Viruses downloaded from Bulletin-Board Systems (BBSs) have long been a
problem, but the speed of the Internet amplifies the difficulty.
The solution seems to be digital signatures, whereby code is verified to
show who the author is. This is based on the idea that a virus works because its
creator can be anonymous, so if you remove the anonymity, individuals will be
forced to be responsible for their actions. This seems like a good plan because
it allows programs to be much more functional, and I suspect it will eliminate
malicious mischief. If, however, a program has an unintentional destructive bug,
it will still cause problems.
The Java approach is to prevent these problems from occurring, via the
sandbox. The Java interpreter that lives on your local Web browser examines the
applet for any untoward instructions as the applet is being loaded. In
particular, the applet cannot write files to disk or erase files (one of the
mainstays of viruses). Applets are generally considered to be safe, and since
this is essential for reliable client/server systems, any bugs in the Java
language that allow viruses are rapidly repaired. (Its worth noting that the
browser software actually enforces these security restrictions, and some
browsers allow you to select different security levels to provide varying
degrees of access to your system.)
You might be skeptical of this rather draconian restriction against writing
files to your local disk. For example, you may want to build a local database or
save data for later use offline. The initial vision seemed to be that eventually
everyone would get online to do anything important, but that was soon seen to be
impractical (although low-cost Internet appliances might someday satisfy the
needs of a significant segment of users). The solution is the signed applet
that uses public-key encryption to verify that an applet does indeed come from
where it claims it does. A signed applet can still trash your disk, but the
theory is that since you can now hold the applet creators accountable, they
wont do vicious things. Java provides a framework for digital signatures so
that you will eventually be able to allow an applet to step outside the sandbox
if necessary. Chapter 14 contains an example of how to sign an applet.
In addition, Java Web Start is a relatively new way to easily distribute
standalone programs that dont need a web browser in which to run. This
technology has the potential of solving many client side problems associated
with running programs inside a browser. Web Start programs can either be signed,
or they can ask the client for permission every time they are doing something
potentially dangerous on the local system. Chapter 14 has a simple example and
explanation of Java Web Start.
Digital signatures have missed an important issue, which is the speed that
people move around on the Internet. If you download a buggy program and it does
something untoward, how long will it be before you discover the damage? It could
be days or even weeks. By then, how will you track down the program thats done
it? And what good will it do you at that point?
The Web is the most general solution to the client/server problem, so it
makes sense to use the same technology to solve a subset of the problem, in
particular the classic client/server problem within a company. With
traditional client/server approaches you have the problem of multiple types of
client computers, as well as the difficulty of installing new client software,
both of which are handily solved with Web browsers and client-side programming.
When Web technology is used for an information network that is restricted to a
particular company, it is referred to as an intranet. Intranets provide much
greater security than the Internet, since you can physically control access to
the servers within your company. In terms of training, it seems that once people
understand the general concept of a browser its much easier for them to deal
with differences in the way pages and applets look, so the learning curve for
new kinds of systems seems to be reduced.
The security problem brings us to one of the divisions that seems to be
automatically forming in the world of client-side programming. If your program
is running on the Internet, you dont know what platform it will be working
under, and you want to be extra careful that you dont disseminate buggy code.
You need something cross-platform and secure, like a scripting language or Java.
If youre running on an intranet, you might have a different set of
constraints. Its not uncommon that your machines could all be Intel/Windows
platforms. On an intranet, youre responsible for the quality of your own code
and can repair bugs when theyre discovered. In addition, you might already have
a body of legacy code that youve been using in a more traditional client/server
approach, whereby you must physically install client programs every time you do
an upgrade. The time wasted in installing upgrades is the most compelling reason
to move to browsers, because upgrades are invisible and automatic (Java Web
Start is also a solution to this problem). If you are involved in such an
intranet, the most sensible approach to take is the shortest path that allows
you to use your existing code base, rather than trying to recode your programs
in a new language.
When faced with this bewildering array of solutions to the client-side
programming problem, the best plan of attack is a cost-benefit analysis.
Consider the constraints of your problem and what would be the shortest path to
your solution. Since client-side programming is still programming, its always a
good idea to take the fastest development approach for your particular
situation. This is an aggressive stance to prepare for inevitable encounters
with the problems of program development.
This whole discussion has ignored the issue of server-side programming. What
happens when you make a request of a server? Most of the time the request is
simply send me this file. Your browser then interprets the file in some
appropriate fashion: as an HTML page, a graphic image, a Java applet, a script
program, etc. A more complicated request to a server generally involves a
database transaction. A common scenario involves a request for a complex
database search, which the server then formats into an HTML page and sends to
you as the result. (Of course, if the client has more intelligence via Java or a
scripting language, the raw data can be sent and formatted at the client end,
which will be faster and less load on the server.) Or you might want to register
your name in a database when you join a group or place an order, which will
involve changes to that database. These database requests must be processed via
some code on the server side, which is generally referred to as server-side
programming. Traditionally, server-side programming has been performed using
Perl, Python, C++, or some other language, to create CGI programs, but more
sophisticated systems have been appearing. These include Java-based Web servers
that allow you to perform all your server-side programming in Java by writing
what are called servlets. Servlets and their offspring, JSPs, are two of the
most compelling reasons that companies who develop Web sites are moving to Java,
especially because they eliminate the problems of dealing with differently-abled
browsers (these topics are covered in Thinking in Enterprise Java).
Much of the brouhaha over Java has been over applets. Java is actually a
general-purpose programming language that can solve the kinds of problems you
can solve with other languagesat least in theory. And as pointed out
previously, there might be more effective ways to solve most client/server
problems. When you move out of the applet arena (and simultaneously release the
restrictions, such as the one against writing to disk) you enter the world of
general-purpose applications that run standalone, without a Web browser, just
like any ordinary program does. Here, Javas strength is not only in its
portability, but also its programmability. As youll see throughout this book,
Java has many features that allow you to create robust programs in a shorter
period than with previous programming languages.
Be aware that this is a mixed blessing. You pay for the improvements through
slower execution speed (although there is significant work going on in this
areain particular, the so-called hotspot performance improvements in recent
versions of Java). Like any language, Java has built-in limitations that might
make it inappropriate to solve certain types of programming problems. Java is a
rapidly evolving language, however, and as each new release comes out it becomes
more and more attractive for solving larger sets of problems.
The reason Java has been so successful is that the goal was to solve many of
the problems facing developers today. A fundamental goal of Java is improved
productivity. This productivity comes in many ways, but the language is designed
to be a significant improvement over its predecessors, and to provide important
benefits to the programmer.
Classes designed to fit the problem tend to express it better. This means
that when you write the code, youre describing your solution in the terms of
the problem space (Put the grommet in the bin) rather than the terms of the
computer, which is the solution space (Set the bit in the chip that means that
the relay will close). You deal with higher-level concepts and can do much more
with a single line of code.
The other benefit of this ease of expression is maintenance, which (if
reports can be believed) is a huge portion of the cost over a programs
lifetime. If a program is easier to understand, then its easier to maintain.
This can also reduce the cost of creating and maintaining the documentation.
The fastest way to create a program is to use code thats already written: a
library. A major goal in Java is to make library use easier. This is
accomplished by casting libraries into new data types (classes), so that
bringing in a library means adding new types to the language. Because the Java
compiler takes care of how the library is usedguaranteeing proper
initialization and cleanup, and ensuring that methods are called properlyyou
can focus on what you want the library to do, not how you have to do it.
Error handling in C is a notorious problem, and one that is often
ignoredfinger-crossing is usually involved. If youre building a large, complex
program, theres nothing worse than having an error buried somewhere with no
clue as to where it came from. Java exception handling is a way to
guarantee that an error is noticed, and that something happens as a result.
Many traditional languages have built-in limitations to program size and
complexity. BASIC, for example, can be great for pulling together quick
solutions for certain classes of problems, but if the program gets more than a
few pages long, or ventures out of the normal problem domain of that language,
its like trying to swim through an ever-more viscous fluid. Theres no clear
line that tells you when your language is failing you, and even if there were,
youd ignore it. You dont say, My BASIC program just got too big; Ill have to
rewrite it in C! Instead, you try to shoehorn a few more lines in to add that
one new feature. So the extra costs come creeping up on you.
Java is designed to aid programming in the largethat is, to erase those
creeping-complexity boundaries between a small program and a large one. You
certainly dont need to use OOP when youre writing a hello, world style
utility program, but the features are there when you need them. And the compiler
is aggressive about ferreting out bug-producing errors for small and large
programs alike.
Java looks a lot like C++, so naturally it would seem that C++ will be
replaced by Java. But Im starting to question this logic. For one thing, C++
still has some features that Java doesnt, and although there have been a lot of
promises about Java someday being as fast or faster than C++, weve seen steady
improvements but no dramatic breakthroughs. Also, there seems to be a continuing
interest in C++, so I dont think that language is going away any time soon.
Languages seem to hang around.
Im beginning to think that the strength of Java lies in a slightly different
arena than that of C++, which is a language that doesnt try to fit a mold.
Certainly it has been adapted in a number of ways to solve particular problems.
Some C++ tools combine libraries, component models, and code-generation tools to
solve the problem of developing windowed end-user applications (for Microsoft
Windows). And yet, what do the vast majority of Windows developers use?
Microsofts Visual BASIC (VB). This despite the fact that VB produces the kind
of code that becomes unmanageable when the program is only a few pages long (and
syntax that can be positively mystifying). As successful and popular as VB is,
its not a very good example of language design. It would be nice to have the
ease and power of VB without the resulting unmanageable code. And thats where I
think Java will shine: as the next VB.[9] You may or may not
shudder to hear this, but think about it: so much of Java is intended to make it
easy for the programmer to solve application-level problems like networking and
cross-platform UI, and yet it has a language design that allows the creation of
very large and flexible bodies of code. Add to this the fact that Javas type
checking and error handling is a big improvement over most languages and you
have the makings of a significant leap forward in programming productivity.
If youre developing all your code primarily from scratch, then the
simplicity of Java over C++ will significantly shorten your development timethe
anecdotal evidence (stories from C++ teams that Ive talked to who have switched
to Java) suggests a doubling of development speed over C++. If Java performance
doesnt matter or you can somehow compensate for it, sheer time-to-market issues
make it difficult to choose C++ over Java.
The biggest issue is performance. Interpreted Java has been slow, even 20 to
50 times slower than C in the original Java interpreters. This has improved
greatly over time (especially with more recent versions of Java), but it will
still remain an important number. Computers are about speed; if it wasnt
significantly faster to do something on a computer then youd do it by hand.
(Ive even heard it suggested that you start with Java, to gain the short
development time, then use a tool and support libraries to translate your code
to C++, if you need faster execution speed.)
The key to making Java feasible for many development projects is the
appearance of speed improvements like so-called just-in-time (JIT) compilers,
Suns own hotspot technology, and even native code compilers. Of course,
native code compilers will eliminate the touted cross-platform execution of the
compiled programs, but they will also bring the speed of the executable closer
to that of C and C++. And cross-compiling a program in Java should be a lot
easier than doing so in C or C++. (In theory, you just recompile, but that
promise has been made before for other languages.)
This chapter attempts to give you a feel for the broad issues of
object-oriented programming and Java, including why OOP is different, and why
Java in particular is different.
OOP and Java may not be for everyone. Its important to evaluate your own
needs and decide whether Java will optimally satisfy those needs, or if you
might be better off with another programming system (including the one youre
currently using). If you know that your needs will be very specialized for the
foreseeable future and if you have specific constraints that may not be
satisfied by Java, then you owe it to yourself to investigate the alternatives
(In particular, I recommend looking at Python; see www.Python.org). Even if you
eventually choose Java as your language, youll at least understand what the
options were and have a clear vision of why you took that direction.
You know what a procedural program looks like: data
definitions and function calls. To find the meaning of such a program, you have
to work a little, looking through the function calls and low-level concepts to
create a model in your mind. This is the reason we need intermediate
representations when designing procedural programsby themselves, these programs
tend to be confusing because the terms of expression are oriented more toward
the computer than to the problem youre solving.
Because Java adds many new concepts on top of what you find in a procedural
language, your natural assumption may be that the main( ) in a Java
program will be far more complicated than for the equivalent C program. Here,
youll be pleasantly surprised: A well-written Java program is generally far
simpler and much easier to understand than the equivalent C program. What youll
see are the definitions of the objects that represent concepts in your problem
space (rather than the issues of the computer representation) and messages sent
to those objects to represent the activities in that space. One of the delights
of object-oriented programming is that, with a well-designed program, its easy
to understand the code by reading it. Usually, theres a lot less code as well,
because many of your problems will be solved by reusing existing library code.
[2] Some language designers have decided that object-oriented
programming by itself is not adequate to easily solve all programming problems,
and advocate the combination of various approaches into multiparadigm
programming languages. See Multiparadigm Programming in Leda by Timothy Budd
(Addison-Wesley 1995).
[3] This is actually a bit restrictive, since objects can
conceivably exist in different machines and address spaces, and they can also be
stored on disk. In these cases, the identity of the object must be determined by
something other than memory address.
[4] Some people make a distinction, stating that type
determines the interface while class is a particular implementation of that
interface.
[5] Im indebted to my friend Scott
Meyers for this term.
[6] This is usually enough detail for most diagrams, and you
dont need to get specific about whether youre using aggregation or
composition.
[7] Primitive types, which youll learn about later, are a
special case.
[8] Except, unfortunately, for primitives. This is discussed
in detail later in the book.
[9] Microsoft is effectively saying not so fast with C# and
.NET. Numerous people have raised the question of whether VB programmers want to
change to anything else, whether that be Java, C#, or even
VB.NET.
|
|
|