Thinking in Java - 4th Edition

effort from the programmer. Wouldn’t 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.
One of the big changes in Java SE5 is the addition of parameterized types, called generics in
Java. You’ll recognize the use of generics by the angle brackets with types inside; for example,
an ArrayList that holds Shape can be created like this:
ArrayList<Shape> shapes = new ArrayList<Shape>();
There have also been changes to many of the standard library components in order to take
advantage of generics. As you will see, generics have an impact on much of the code in this
book.
Object creation & lifetime
One critical issue when working with objects is the way they are created and destroyed. 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 doesn’t seem too
challenging: You create the object, use it for as long as it’s needed, and then it should be
destroyed. However, it’s 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 clean up 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
doesn’t require such immediate attention as the main controller function. Maybe it’s 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 it’s 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 you’re 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 you’re done with it this can become
quite complex.
30 Thinking in Java Bruce Eckel
Introduction to Objects 31
Where is the data for an object and how is the lifetime of the object controlled? C++ takes the
approach that control of efficiency is the most important issue, so it gives the programmer a
choice. For maximum runtime 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 this control 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 dynamic memory allocation, exclusively.7 Every time you want to create an object,
you use the new operator 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 don’t 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 importantly, 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.
With Java, the garbage collector is designed to take care of the problem of releasing the
memory (although this doesn’t 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 way—on 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.
Exception handling: dealing with errors
Ever since the beginning of programming languages, error handling has been a particularly
difficult issue. Because it’s 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
7 Primitive types, which you’ll learn about later, are a special case.
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 vigilant—often the case if they are in a hurry—these
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. It’s 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
doesn’t