Sponsored Link •
|
Advertisement
|
[bv: need an intro]
The object-oriented design process involves the following three tasks:
In an object-oriented design, you identify the fundamental objects of the problem domain, the "things" involved. You then classify the objects into types by identifying groups of objects that have common characteristics and behaviors. The types of objects you identify in the problem domain become "types" in your solution. The program you write will create and manipulate objects of these types. By naming the types in your solution after the types in the problem, you build a vocabulary for expressing the solution out of the language you would use to describe the problem.
In addition to types that correspond to elements in the problem, the "problem domain types," your solution will likely have types that don't correspond to anything in the problem domain. For example, most programs will require types that deal with data management and user interface. An example of a data-management type is a hash table. You might use a hash table object in your program to speed lookup of a set of objects, even though there is no hash table object in the problem domain. The objects you are looking up in the hash table, however, might represent objects that exist in the problem domain. Some examples of user interface types might be button, window, and dialog.
The fundamental task of abstraction in an object-oriented design is to identify objects in the problem domain and then to classify the objects into types. As you divide the problem domain into types, you will to some degree model the relationships between the types as well. Objects can have three kinds of relationships:
The has-a relationship means that one type of object contains another or is composed of another. Some examples are: a car has-an engine, a bicycle has-a wheel, and a coffee cup has coffee. The has-a relationship is modeled with composition, which is discussed in Chapter 6.
The is-a relationship means that one type of object is a more specific version of a general type. Some examples are: a car is-a vehicle, a bicycle is-a vehicle, and a coffee cup is-a cup. The is-a relationship is modeled with inheritance, which is also discussed in Chapter 6.
The uses-a relationship means that during some activity, one type of object uses another type of object. Some examples are: a car uses-a squeegee (during a window-washing activity), a bicycle uses-a pump (during a tire-pumping activity), and a coffee cup uses-a stirrer (during a stirring activity). The uses-a relationship will be discussed further in Chapter ?? [bv: which chapter?].
Along with dividing the problem domain into types and modeling their relationships, you must define attributes and behaviors that will characterize each type in the solution.
The attributes of a type define the nature of the state of objects of that type. An object's state is composed of values for all the attributes of the type. For example, two possible attributes for a bicycle type are speed and direction. An object of type bicycle would therefore have a state that is composed of values for speed and direction. Note that an object's state (the values of its attributes) can change over the lifetime of the object. A bicycle object, for example, could have a state of 15 mph and north at one point in time. Later, that same object could have state 10 mph and south.
In object-oriented thinking, interaction between objects is modeled as messages sent between objects and the action that objects take as a result. When you model the behavior of a type you define a set of messages that objects of that type will accept, and the actions that objects of that type will take upon receipt of those messages. The set of accepted messages and the resulting actions constitute services that are offered by the object.
As the designer of a type, you decide what an object of that type will do when it receives a message. Messages contain information, and an object can use the information contained in a received message along with the information represented by its own current state, to decide what to do. It may do nothing. It may send messages to other objects. It may change its own state. It may return some information to the message sender. Or it may do all of these things.
In computer science circles, the term "message" is often associated with asynchronous messaging, in which received messages can queue up and be processed by the recipient at some later time. In this object-oriented context, however, a message is simply a request coupled with some information that is passed to an object. In general, an object begins to process a message immediately upon receipt and potentially returns a reply to the sender. A message can have an effect that is delayed (similar to asynchronous messaging), but creating such a delayed effect is an option of the message recipient.
These design activities are processes of abstraction because out of all the elements of the problem domain, you are selecting only those that are important. As a result, in any one design you will likely ignore many elements of the problem domain. In your solution, you won't model every type of object you can possibly identify in the problem domain, only those that matter to your solution. Likewise, you won't model every attribute and every behavior of the types of objects you have chosen to represent in your solution, just those attributes and behaviors that are important to your solution. In a different problem domain, you might model different attributes and behaviors of the same types of objects. Thus, you are abstracting: pulling out what you feel is important about the problem domain, and using only those elements in your solution.
As an example of an object-oriented design with the Java programming language, imagine you are designing a virtual café, a place in cyberspace where guests can sit at small tables, sipping virtual cups of coffee, and chatting with one another. The primary function of the café is that of a chat room: a place where people separated by (potentially vast) physical distances, but connected to the same network, can get together and converse. To make your chat room more inviting, you want it to look like a café. You want each participant to see graphical representations ("avatars") of the other people in the café. And to make the participant's experience more real, you want the people to be able to interact with certain items in the café, such as tables, chairs, and cups of coffee.
To start your design process, visualize your virtual café on an average busy day. What objects do you see? Perhaps you see guests, tables and chairs, coffee cups, coffee, pitchers of milk, packets of sugar, coffee stirrers, and the café itself. These are the types of objects in your problem domain. By describing them in a human language, you are already classifying them, grouping related objects together. You may have 100 different coffee cups in your café--all distinct objects--but you place all 100 of them into the "coffee cup" category when you say, "I see 100 coffee cups." This is the first step in an object- oriented design: identifying objects in the problem domain, and grouping them by type.
As the designer, you must decide which objects to group into which categories. Just as describing a scene in human language is not an exact science--you have to pull out what's important and focus your description on that--neither is object-oriented design an exact science. There are many ways to slice up a given problem domain into objects and types. Because of this, you must focus on the types of objects you think will be most important in your solution. These will be the types that have the greatest interaction. For example, in the case of your virtual café, you may have artsy drawings hanging on the walls, but if those drawings do nothing but hang there, then perhaps they shouldn't be awarded with their own type. They can just be a characteristic of your café type.
So what types of objects are the most important in the virtual café? Ask yourself what kind of objects are involved in the activities of your café. Guests sit on chairs at tables and chat with their neighbors. They buy cups of coffee, add milk or sugar to them, swirl the result with a stirrer, and drink. If these are the primary activities that go on in your problem domain, then the types of objects involved should be what you most concern yourself with in your solution: guests, tables, chairs, coffee cups, coffee, milk, sugar, stirrers, and the café itself.
Each of these objects interacts with other objects. Chairs may host a guest or be empty. Tables can have different numbers of chairs. Both tables and chairs can be moved around the café. Tables can be moved together to accommodate large groups of guests. Coffee cups accept coffee, milk, sugar, and a swirling stirrer. They also release their contents sip by sip to the drinker, or can spill their contents all at once onto the table, onto the floor of the café, or more alarmingly, onto a guest.
Although the objects you group together will share characteristics and behaviors, they will usually not be identical copies of each other. There are many objects in your everyday experience that you would call coffee cups--some are made of styrofoam, some of ceramic; some are big, some small--but you still recognize them as coffee cups because of the characteristics and behaviors they have in common. Mainly, they hold coffee for you to drink. They allow you to add extras, such as sugar or milk, and to insert a stirrer to swirl the components into the perfect drink to suit your palette. If you knock them over, they'll spill their contents. The first step of object-oriented design, therefore, is creating abstract categories, such as "coffee cup", and placing different objects, each of which may have their own unique qualities, into the same category.
The first part of the process of abstraction usually involves not simply deciding upon lone types, but upon inheritance hierarchies of types. An inheritance hierarchy is a diagram showing the inheritance (is-a) relationships between types. Thus, as you determine types you will usually model is-a relationships with inheritance. To start with, however, this discussion will keep it simple and just focus on lone types. Inheritance will be discussed in Chapter 6.
Once you have identified the main types of objects you will use in your solution, you must next define the internal nature of each type: you must model attributes and behaviors. Here you focus once again on what's important to your particular solution. There are many ways to describe a coffee cup. You can, for instance, talk about its color, its volume, its shape, the amount of coffee it currently contains, whether or not it has a handle, its material, its current orientation with respect to the planet Venus, its place of manufacture, or the name of the last three people who drank out of it. Not all of a coffee cup's characteristics will be important in your solution. You must, as the designer, choose the attributes of a type of object that are most important, and model only those. You may, for example, decide that color, current amount of coffee, and position in the café are the only attributes of coffee cups that are important in your solution.
As mentioned previously, an object has state and behavior, and the nature of an object's state is defined by its attributes. For example, the nature of a coffee cup object's state could be its color, the amount of coffee it contains, and its position in the café. Different coffee cup objects can have different colors, be filled with different amounts of coffee, and be located in different places in the café. Thus, "coffee cup" is a type; "the red coffee cup that is currently filled with 38 milliliters of coffee and is sitting on the table in the corner of the café" is an object. In this case, "color", "amount of coffee", and "position" are attributes of the coffee cup type. "Red", "38 ml", and "sitting on the corner table" comprise the state of a particular coffee cup object.
Even though the objects you classify as a type will share certain characteristics and behaviors, they will usually differ in some ways too. Attributes model the variation that is allowed among different objects of the same type. An object maintains a value for each attribute defined in its type. All the values taken together comprise the object's state. In the previous example, the coffee cup object's value for the color attribute was red. Its value for the amount of coffee attribute was 38 ml. Its value for the position attribute was "sitting on the corner table." The object's state, which is composed of all its attribute values, was: red, 38ml, and "sitting on the corner table." Later, the same coffee cup could have a changed state: red, empty, and "sitting on the corner table." A different coffee cup object could have a different state, such as: yellow, 200 ml, and "hovering in midair."
During the lifetime of an object, an attribute value may either fluctuate or remain constant. An example of an attribute value that may fluctuate during the object's lifetime is the amount of coffee contained in a coffee cup object. You may add and remove coffee many times during the lifetime of a coffee cup object, resulting in an "amount of coffee" attribute value that changes over time. Another example of an attribute value that may fluctuate is position. When a coffee cup object is created, its position attribute would be initialized to the cup's starting position. As the cup moves around the café during its lifetime, its position attribute would change to reflect its changed positions in the café.
An example of an attribute value that could remain constant over the lifetime of an object is the color attribute of a coffee cup object. You may decide that, although different coffee cups can have different colors, each individual coffee cup has a single color for its entire lifetime. Thus, the value of the color attribute would be established when a coffee cup object is created, and never changed after that. Another example of an attribute value that would likely remain constant for each individual object, but vary among different objects, is size. You may decide to serve three sizes of coffee product in your café, and have a separate size coffee cup for each: short (8 ounces), tall (12 ounces), and grande (16 ounces). The size of a coffee cup object would be established when the object is created. The size would then remain constant throughout the remainder of the cup's life. Although size is a constant attribute value for any one coffee cup object, it is not a constant for the type. Different coffee cup objects can have different sizes.
When you partition a problem domain into types, you will encounter choices between modeling difference between objects as a set of distinct types or as attributes of a common type. For example, instead of making color an attribute of the "coffee cup" type, you could have created one type for each color: "yellow coffee cup," "blue coffee cup," and "red coffee cup." These types wouldn't have a color attribute, because their color is inherent in their type. Likewise, instead of adding a size attribute to a coffee cup type, you could have defined three types: "short coffee cup," "tall coffee cup," and "grande coffee cup." These types wouldn't include a size attribute because their size is inherent in their type. So when you look at the problem domain, you have to decide which differences in characteristics and behavior that you will model as different types and which you will model as attributes of the same type.
The value of each attribute you decide upon, whether it fluctuates or remains constant during an object's lifetime, should in some way affect the behavior of the object. If the value of an attribute doesn't affect an object's behavior in any way, then there is no sense modeling the attribute as part of the type. The value of the color attribute, for example, could determine how a coffee cup object draws itself in the user interface of the café. The value of the size attribute could affect how a coffee cup object reacts to being filled with coffee. If an empty 16 ounce cup gets filled with 16 ounces of coffee, all is well. But if an empty 12 ounce cup gets filled with 16 ounces of coffee, 4 ounces of coffee have to go onto the table (or somewhere). Thus, the object's behavior differs depending upon the value of its attributes.
To model behavior, you must use yet another process of abstraction. You will not model all the possible behaviors a coffee cup can exhibit, only those that matter to your particular solution. A coffee cup can do many things. It can, for example, accept coffee, release one sip of coffee, spill, move from one part of the café to another, shatter into pieces, hold pencils and pens, or serve as a template for drawing nice circles with crayons. (Note that in each of these behaviors, the coffee cup object is interacting with other kinds of objects.) If you decide that accepting coffee, releasing coffee one sip at a time, spilling, and moving to another part of the café are the only behaviors that matter in your solution, your design should model only those behaviors.
A coffee cup's behavior is rooted in the ways it interacts with other objects. When a coffee cup accepts coffee, it is interacting with the coffee pot from which the coffee is poured. When it releases one sip of coffee, it is interacting with the guest who did the sipping. When it moves from one part of the café to another, it is interacting with the café. To model the coffee cup's behavior, you must decide upon a set of messages it will accept from other objects. In the case of a coffee cup, you may decide to accept four kinds of messages from other objects:
For each message, you need to decide what the object will do when it receives that message.
A fundamental object-oriented concept is encapsulation, the bundling of data that represents the state of an object together with the code responsible for manipulating that data. In a Java program, the state of an object is represented by the value of its instance variables: data fields that represent the attributes of a type of object. You interact with a Java object (you send it messages) by invoking the object's instance methods: executable code that manipulates the object's instance variables.
To create an object in Java, you need a class. A class encapsulates the instance variables and methods that define an object. The act of creating an object is sometimes called instantiation, and objects themselves are sometimes called class instances. A class serves as a blueprint from which you can instantiate objects that contain the instance variables and methods defined by the class.
As part of the software design of your virtual café, for example, you probably want a coffee cup class with which you can instantiate coffee cup objects. To do so you must declare a new class, using the class keyword, and give your new class a name:
// On CD-ROM in file encap/ex1/CoffeeCup.java /** * Models all coffee cups in the virtual cafe;. */ public class CoffeeCup { /** * The amount of coffee contained in the cup. * Units are in milliliters of coffee. */ private int innerCoffee = 0; // a field /** * Adds coffee to the current inner amount. */ public void addCoffee(int amount) { // a method innerCoffee += amount; } /** * Releases one sip of coffee to the caller. * If current inner amount (innerCoffee) is less than a sip, * then returns entire remaining amount of coffee. * Always decrements innerCoffee by amount returned. */ public int releaseOneSip(int sipSize) { int sip = sipSize; if (innerCoffee < sipSize) { sip = innerCoffee; } innerCoffee -= sip; return sip; } /** * Releases entire store of coffee to the caller. * Sets innerCoffee to zero. */ public int spillEntireContents() { int all = innerCoffee; innerCoffee = 0; return all; } }
In the above Java code, the class
keyword indicates you want to define a new type of object, in this case
a type named CoffeeCup
. In between the curly braces of class CoffeeCup
is one instance variable,
innerCoffee
, and three instance methods, addCoffee()
, releaseOneSip()
, and
spillEntireContents()
.
The CoffeeCup
defines a new type for your program, from which you can instantiate objects.
Once you create a CoffeeCup
object, you will be able to add coffee to it via the addCoffee()
method,
remove coffee one sip at a time via the releaseOneSip()
method, or remove coffee in one big deluge
via the spillEntireContents()
method. (You will be able to send it these three kinds of messages.)
To create a CoffeeCup
object, you use the new
operator, which
returns a reference to a new object. A reference is a kind of pointer
to an object, which you can use to invoke methods on the object.
In other words, to send an object a message (by invoking a method on
the object), you have to have a reference to that object.
To keep track of references, you can declare variables in which you can store
the references.
Here's an example in which a
variable named cup
is declared
to be of type CoffeeCup
and
assigned a reference to a new CoffeeCup
object created by
the new
operator:
CoffeeCup cup = new CoffeeCup();
Once you have created a CoffeeCup
object in a Java program, you will likely wish to send it a
message. In Java, you send a message to an object by invoking a method on that object. Here is an
example of a method interacting with a CoffeeCup
object by
sending it messages via its methods:
CoffeeCup cup = new CoffeeCup(); cup.addCoffee(150); // 150 ml of coffee cup.releaseOneSip(20); // 20 ml sip cup.spillEntireContents();
Here you sent an "add 150 ml of coffee" message to a CoffeeCup
object. Then you sent a
"release one sip" message, indicating that the size of the sip is 20 ml, to the same CoffeeCup
object.
Lastly, you sent a "spill entire contents" message to the CoffeeCup
.
One of the key ideas in object-oriented programming is the separation of an object's external interface from its internal implementation. An object's interface is the messages it will accept from other objects. An object's implementation is its attributes and its behavior in response to received messages. In the object-oriented world, an object exposes its interface to other objects, but keeps its implementation private. Thus, the implementation is separate from the interface. From the outside, the only way to interact with an object is by sending it a message by way of its interface.
Keeping interface separate from implementation enables objects to have responsibility for managing their own state. Other objects cannot directly manipulate an object's internal state, but must send the object a message. The object receives the message and decides what to do. It can, at its option, disregard the message. An object does not control when it will be sent messages, but it maintains control of its response to any messages it receives, including whether or not to change state.
A fundamental tenet of object-oriented programming is that every object
of a particular class can
receive the same messages. This tenet, which holds true in Java programs, also means that the external
interface of an object depends only upon its class. If an object doesn't expose a CoffeeCup
class's
interface, for example, you can know for certain that the object is not an
instance of class CoffeeCup
.
One way to think of objects is that each object displays a set of message
receptors to the world. To send an object a message, you must go
through one of its receptors. In Figure 2-1, a CoffeeCup
object is shown floating through space with
three message receptors exposed to the universe. Each receptor forms a landing place for a message sent
from another object. There is one receptor for each kind of message that this object is prepared to accept
from the outside world. One receptor, addCoffee()
, allows an external object to request that this coffee
cup accept some incoming coffee. Another receptor, releaseOneSip()
, allows an external object to
request that the cup relinquish one sip of its coffee contents. A third receptor,
spillEntireContents()
, allows an external object to request that this coffee cup release its entire
store of coffee.
CoffeeCup
object and its message receptors
Java allows you to separate the interface and implementation of objects by providing you with access levels to attach to the instance variables and methods of a class. The access level of an instance variable or method determines what other classes of object, if any, can access that instance variable or method.
The keywords
public
and private
are access specifiers.
If an instance variable or method is declared private, as is
CoffeeCup
's innerCoffee
variable, only code
defined in the same class can access it. If an instance variable or
method is declared public, as are the methods of class
CoffeeCup
, code of other classes can access it. (Besides
public and private, Java has two other access levels, package and
protected, which will be
discussed in Chapter 9.)
The customary way to design an object is to declare as public only those
instance methods that other objects need to invoke. Instance variables
are normally declared private, so that the object's instance methods will
have sole responsibility for maintaining the object's state. Class
CoffeeCup
is an example of this kind of design. In such
an object, the public instance methods are the objects only external
interface.
Objects have lifetimes. Some objects are short-lived. They are created, used for a short period of time, and then no longer needed. Other objects are used for longer periods of time, but eventually become unneeded as well. Other objects may remain in use from the time they are created until the application in which they live terminates.
Because objects use up memory resources, the memory occupied by unneeded objects may need to be reclaimed to make room for more objects, or to simply keep the memory image of the application small.
Java does not have any explicit way to "free" or "delete" an object once it is created. Instead, JVMs normally use a garbage collector to free memory occupied by objects. A garbage collector is a subsystem of the virtual machine that tracks the usage of objects and reclaims the memory occupied by objects that are no longer needed by the program. To indicate to the garbage collector that you no longer need an object, you simply drop all references to that object. Once an object is no longer referenced by the program, that object can't affect the program's future course of computation, and it is therefore available for garbage collection. At any point in the future, the garbage collector may reclaim the memory occupied by that object.
Now that you've been exposed to the intellectually stimulating
object-oriented meanings of the
terms abstraction and encapsulation, it is time to come back down
to Earth and create your first
Java application. This section will cover various topics that you need
to understand to create that first Java application. The application itself,
named EchoServer
, is given at the end of this section.
Java has three kinds of comments:
//
,
/*
and */
, and
/**
and */
.
//
indicates the rest of the line is a comment.
/*
and */
indicate that all characters between the
initial /*
and the terminating */
should be ignored
by the compiler.
/**
and */
also comment out
anything between them; however, the comments between /**
and
*/
are picked up a special documentation tool called
javadoc
.
The javadoc
tool parses Java source files and builds
HTML files that document the source code. Comments starting with
/**
are called "documentation comments,"
or simply "doc comments," because javadoc
includes these
comments in the HTML files it generates.
More information on javadoc
and doc
comments is given in Appendix A.
Java programs can contain three kinds of variables: instance variables, class variables, and local variables. (Class and local variables will be described in later in this chapter.) Each variable in a Java program has a type, which precedes the variable name when the variable is declared in the source file.
In Java, variables
have either a primitive type or a reference type. Primitive types
are built-in types, such as int
or float
, that are
not objects. The primitive types offered
by the Java programming language are shown in Table 2-1. Note that unlike
C++, the primitive types in Java have defined ranges. Variables that have a
reference type may hold a reference (similar to a pointer in C++) to an
object. This kind of variable will be discussed in the next section.
Type | Range |
---|---|
boolean |
either true or false |
byte |
8-bit signed two's complement integer (-27 to 27 - 1, inclusive) |
short |
16-bit signed two's complement integer (-215 to 215 - 1, inclusive) |
int |
32-bit signed two's complement integer (-231 to 231 - 1, inclusive) |
long |
64-bit signed two's complement integer (-263 to 263 - 1, inclusive) |
char |
16-bit unsigned Unicode character (0 to 216 - 1, inclusive) |
float |
32-bit IEEE 754 single-precision float |
double |
64-bit IEEE 754 double-precision float |
Table 2-1. The primitive types of the Java programming language
When you define a Java class, you not only create a new blueprint from which you can instantiate objects, you also create a new reference type with which you can declare variables. Although the primitive types are built into the Java language and virtual machine, reference types are defined by Java code.
Each class you write defines a new type.
For example, once you define class CoffeeCup
,
CoffeeCup
becomes a new type you can use in your program. You
can then declare a variable of type CoffeeCup
, and that
variable can hold a reference to a CoffeeCup
object.
Variables declared to have reference types are called object
variables, because they hold references to objects.
More than one object variable can hold a reference to the same object. Here is an example of a
CoffeeCup
object reference being shared by two variables:
CoffeeCup cup = new CoffeeCup(); CoffeeCup sameCup = cup;
The type of an object variable determines the class of object to which
it can hold a reference. For example, an object variable of type
CoffeeCup
can hold references to objects of class
CoffeeCup
, but wouldn't necessarily be able to hold
references to objects of class Girraffe
. (Through
polymorphism, which will be described in Chapter 7, an object
variable of type CoffeeCup
can hold a reference to
objects of other classes, so long as those classes are subclasses
of CoffeeCup
).
[bv: somewhere note that just declaring a variable gets you only a reference, but no object. You need to use new to get an object.]
In addition to instance variables and methods, classes can have class
variables and class methods, which are
denoted by the static
keyword. Class variables exist for the lifetime of a class and can be accessed even
if there is no instance of the class. A class variable can be used to share a value among many instances of
the class at the same time, or it can be used to store information between subsequent instances spread out
over time. Class methods, which can be called in the absence of an instance of the class, can manipulate class variables defined in the class. Often, however,
class methods serve as general purpose
utility methods,
methods that operate only on the data explicitly passed to them as parameters.
A class's instance and class variables are also called its
fields.
A class's fields and methods are its
members.
Class variables that are declared final (with the final
keyword) are
named constants.
A named constant has a value that
doesn't change over its lifetime. It must be initialized to its constant value with a literal constant, such as
355
, 1.0
, or "I'm a string literal."
.
For example, if you wanted to have convenient constants
for the maximum amount of milliliters of coffee in each of your cup sizes, you could declare three named
constants:
// On CD-ROM in file encap/ex2/CoffeeCup.java class CoffeeCup { public static final int MAX_SHORT_ML = 237; public static final int MAX_TALL_ML = 355; public static final int MAX_GRANDE_ML = 473; // ... }
Methods may have a return type, which can be any of the primitive types
or an object type. Those that don't return anything must be declared to return
void
. Methods accept a set of zero or more parameters, each of
which can have either a primitive or object type. To pass an object to a
method, you pass a reference to it. Although primitive types are passed by
value, objects are always passed by reference. [bv: actually, object
references are passed by value. Objects are never passed. Be precise.]
Unlike C++, which allows you
to define a global function, there is no way in Java to have a method that
isn't part of a class.
In addition to being assigned a reference to an object of the appropriate
type, object references can
also be set to null
. You may want to do this to release an object for garbage
collection. In Java, there is
no equivalent to the delete operator of C++. You can't explicitly free memory.
All objects instantiated
by new are placed on the garbage-collected heap of the Java Virtual Machine.
The garbage collector will
from time to time reclaim the memory occupied by objects that are no longer
referenced by the program.
So long as there is at least one variable that holds a reference to an
object, the garbage collector will leave
that object on the heap. Here's an example of an object becoming
available for garbage collection:
//On CD-ROM in file encap/ex1/Example1c.java class Example1c { public static void main(String[] args) { // Create a new coffee cup object, assign it to ref1 CoffeeCup ref1 = new CoffeeCup(); // Assign reference to same CoffeeCup to ref2 CoffeeCup ref2 = ref1; // Null out ref1 ref1 = null; // Create a new object for ref2 ref2 = new CoffeeCup(); // The original CoffeeCup object is no longer referenced, so // it is now available for garbage collection. // Assume this program continues and does other work. //... } }
In Java, you send a message to an object by invoking one of the object's methods. Every method of a particular class has a signature unique to that class. A method's signature consists of its name and the number and types of its parameters. (Note that a method signature does not include the method's return type.) The signature and return type of each public instance method in a class form part (or all) of the interface of instances of that class. They define the kinds of messages instances of that class will accept.
Every method in a class must have a unique signature, but not necessarily a unique name. You can put multiple methods of the same name into the same class, so long as each method differs in either the number, the types, or the order of their parameters. This is called method overloading. If you try to put two methods with the same signature, but different return types, into the same class, that class won't compile. In Java, you can't overload methods just by returning a different type. For example, you could have these three separate methods for adding coffee:
// On CD-ROM in file fieldmethod/ex14/CoffeeCup.java class CoffeeCup { public void add(int amount) { System.out.println("Adding int amount " + amount); //... } public void add(float amount) { System.out.println("Adding float amount " + amount); //... } // Pour coffee into this cup from another cup. public void add(CoffeeCup cup) { System.out.println("Pouring from another cup."); //... } //... }
The compiler determines which add()
method you are invoking by the type
of the parameter you pass:
// On CD-ROM in file fieldmethod/ex14/Example14.java class Example14 { public static void main(String[] args) { CoffeeCup blueCup = new CoffeeCup(); CoffeeCup redCup = new CoffeeCup(); blueCup.add(50); // Use void add(int amount) blueCup.add(50.0f); // Use void add(float amount) redCup.add(blueCup); // Use void add(CoffeeCup cup) } }
You can't overload methods by varying only the return type, because the
compiler wouldn't know which method you wanted if you invoke it and ignore
the return value. For example, were you able to define two add()
methods
like this:
// On CD-ROM in file fieldmethod/ex15/CoffeeCup.java // THESE TWO METHODS WON'T COMPILE TOGETHER, BUT IMAGINE // FOR A MOMENT THAT IT WERE POSSIBLE. class CoffeeCup { public void add(int amount) { //... } public boolean add(int amount) { //... } //... }And somewhere else in your program you invoked an
add()
method
but ignored the return type:
// On CD-ROM in file fieldmethod/ex15/Example15.java // THIS WON'T COMPILE EITHER, BECAUSE THE CoffeeCup CLASS IT // DEPENDS ON DOESN'T COMPILE. class Example15 { public static void main(String[] args) { CoffeeCup cup = new CoffeeCup(); cup.add(50); } }The compiler wouldn't know which
add(int amount)
you were
referring to, the one that returns an int
or the one that doesn't
return anything. This is why a method's signature is composed of the
method's name and the number and types of its arguments, but not its return
type. Every method in a class, whether explicitly declared in the class
or inherited by it, must have a unique signature.
main()
MethodEchoServer
Application// On CD-ROM in file encap/ex?/Echo.java public class EchoServer { public void run() { while ((int c = System.in.read()) != -1) { System.out.write(c); } } public static void main(String[] args) { Echo echo = new Echo(); echo.run(); } }
EchoServer
Note that unlike C++, Java makes no distinction between a
"declaration" and a "definition." In Java, all items--classes, fields, and
methods--are declared in one place only, in a .java
file. There
are no separate header files and implementation files, as in C++. Also,
other classes in other .java
files can use CoffeeCup
directly, without needing to declare it as a class at the top of the file,
as C++ requires. The Java compiler automatically attempts to find any class,
field, or method that is used in a .java
file by looking in
other .java
files or in libraries of class files. The compiler
also looks ahead in the same source file, so to see if the referenced item
occurs later in the file. If the Java compiler is unable to find some
class, field, or method referred to by a .java
file, that
.java
file fails compilation with an error message indicating
the missing item.
The Java compiler takes your source files and, as is the habit of
compilers, checks them for correctness. When the compiler is pleased
with your work, it translates the source files into class files--the binary
format for Java programs. (In Java, source files take the extension
".java
"; class files take the extension ".class
".)
The compiler transforms the Java
language instructions expressed in your source files to a form called
bytecodes, instructions for the Java Virtual Machine, which the compiler
places into the class files. You get one class file for each class and
one stream of bytecodes for each method in your program's source. The
EchoServer
program compiles to only one class file,
named "EchoServer.class
".
Once your program is in class file form, it is ready to run. A Java Virtual
Machine runs a program by loading its class files and executing the bytecodes
they contain. To run the EchoServer
program, for example, a
Java Virtual Machine would first load Greeting.class
and then
execute the bytecodes of the main()
method it
contains. The virtual machine would also load class files from the Java
API, to execute any of their bytecodes that are needed by the
EchoServer
program.
CoffeeCup
The object model described here is that which exists inside a single virtual machine instance. Somewhere, perhaps in chapter 1?, say that the object model is different across VMs.
Java allows you to name classes, fields, and methods any way you want,
however, Java does offer some suggested naming conventions. To classes you
should give names that are nouns or noun phrases. They should be descriptive
and not overly long. Class names are mixed case: the first character of each
word in the name is upper case; the rest are lower case. Hence, the "coffee
cup" type of object in the problem domain becomes the CoffeeCup
type in the solution. Using noun names for classes makes sense
because classes represent the "things" in your problem domain--and in human
language, the names for things are nouns. If you have a class with a verb
name, such as FillCupWithCoffee
, it is likely a sign of a poorly
designed class. Filling a cup with coffee is an activity, not a object.
Therefore, this activity should probably be modeled as a method of some
class, not as a class itself.
You should give fields names that are nouns, noun phrases, or
abbreviations for nouns. Field names are mixed case. The first character of
a field name should be lower case. Each subsequent word in the field name,
however, should begin with an upper case character. (One exception to this
naming convention for fields is constant fields, which should be all upper
case. This is discussed later in this chapter.) The attribute modeled in
the CoffeeCup class is the amount of coffee contained inside the cup. So a
potential field name for this attribute would be
amountOfCoffeeContainedInsideTheCup
. This long name was
abbreviated to innerCoffee
. Using noun names for fields makes
sense, because fields model attributes of objects in the problem domain,
and attributes are generally described with nouns.
The constants above illustrate the suggested naming convention for compile-time constants: all capital letters with each word separated by an underscore. The name can be any part of speech. Although attributes are modeled by fields, not all fields are necessarily attributes.
You should give methods names that are verbs or verb phrases. Their
capitalization follows that of fields: the first character is lower case
and the first character of each subsequent word is upper case. The names
of the methods in the example above follow the suggested Java naming
convention for methods: addCoffee()
, releaseOneSip()
,
and spillEntireContents()
.Giving methods verb names is sensible,
because methods represent activities or actions in your problem domain, and
in human language, action words are verbs.
Another way to make your Java code more readable is by following the recommended naming conventions proposed by the Java Language Specification. A few conventions for naming methods that were not described in Chapter 5 are:
get
" and "set
" as prefixes that assign
and return the value of an
attribute. For example, consider an object which keeps track of its current
temperature. You would name a method that assigns a new value to the object's
temperature attribute "setTemperature()
", and name a method that
returns the object's current temperature "getTemperature()
".
This is also the convention used by JavaBeans for getting and setting
bean properties.
length()
".
is
". For example, to test whether an object is too
hot, too cold, or just right, you would expect to call isTooHot()
,
isTooCold()
, or isJustRight()
.
to
". An
example of this convention is the toString()
method, which
every class inherits from class Object
. This method returns a
string that is a textual representation of the object.
Class names should be nouns. Methods names should be verbs.
As with other types of variables, the Java Language Specification provides suggested naming conventions for local variables. In general, you should give local variables names that are lower case, short, and meaningful. Local variable names are often not whole words, but are instead acronyms, abbreviations, or mnemonics. You may wish to give single- character names to looping or temporary local variables. The suggested single-character names for variables of different types are:
byte b; char c; double d; Exception e; // For any exception float f; int i, j, k; long l; Object o; // For any object String s; // And v, for an arbitrary value of any type
If you do use local variable names that include words, in an
effor to maximize the
readability of your code, begin with a lower case character and start each subsequent word with an upper
case character, as in
thisIsALongButDescriptiveNameForALocalVariable
.
[bv: Also, say long for rarely used names; short for oft used names.]
You can use method overloading to make your code more readable. If methods are really doing the same thing, but on different types of data, you should give those methods the same name.
In a Java class, the implementation is any hidden internal data of a class, any hidden methods of the class, and the code of the methods exposed as the interface of the class. The primary step you must take to separate the interface and implementation is to hide the internal data of the class. To hide a field, you declare it private.
The four access levels are private, public, protected, and package. As a designer of a class, you generally hide internal data by declaring it private. Any methods that are invoked only by other methods of the class can be declared private also. Members declared private can only be accessed by code belonging to the same class. If you are creating a library, you'll want to declare any methods you want to expose to the world public. Public members of public classes can be accessed by any method of any class.[bv: so long as the class itself is declared public] [bv: need a short advice on using protected and package? Need to describe public classes.]
[bv: mention classes can be public or package. Package access classes will be described in Chapter 5. All classes up to that point will be public. (Need to make them all public.)]
If another object is allowed access to a member, that member is part of the object's external interface and represents one way the object can receive a message. A class's members include all the variables and methods declared in the class. (As you will see in Chapters 5 and 9, a class can also include as members other classes, which are called nested or inner classes.)
Separation of interface and implementation is not intended to keep objects out of trouble; rather, it is intended to keep programmers out of trouble. To understand why, it helps to classify programmers into two groups. When you design a class, you are the designer of that class. All the programmers who use your class in their programs are the clients of the class. The designer's goal is to build a useful library of types, whereas the client's goal is to hook together types from various libraries to build a program that solves a particular problem. In your career as a Java programmer you will undoubtedly play both roles. Sometimes you'll create new classes; sometimes you'll use libraries of classes created by others.
One of the main ideas of the object-oriented paradigm is that designers should keep hidden from clients the actual implementation of a class. The designer of a class writes and maintains the implementation, and exposes as the interface only what is needed by the client. Clients of the class have access only to the interface.
There are two main benefits of separating the interface and implementation of a class. First, this approach makes it easier for designers to make changes to their classes. A designer can change the underlying implementation of a class while maintaining the same external interface. This in turn enables clients to use the new version of the class without requiring any changes to existing client code (so long as the designer doesn't change the class's behavior). [bv: talk about contract here?]
A second benefit of this approach is that it allows a designer to keep control of the internal data of an object. If the internal data of an object is hidden, the only way a client can manipulate the internal data is by invoking the methods exposed as the interface. Because the designer writes all the methods of the class, the designer has exclusive control over how the internal data is manipulated.
One other reason to make data private is because you synchronize access to data by multiple threads through methods. This justification for keeping data private will be discussed in Chapter 13.
As a design guideline, you should always make non-constant fields private, unless you have an explicit reason not to. When you expose fields outside the class, you lose the ability to control changes to the value of that field as your program runs. You also have less flexibility in the future to change the implementation of the class.
In the example above, all
SugarHolder
objects will share the same
sugarPacketsInCafeCount
, but each SugarHolder
object
will be awarded its own copy of sugarPacketsCount
. In the Java Virtual
Machine, instance variables are associated with objects. When a new object is created by a running
program, the Java Virtual Machine allocates enough memory to hold all the object's instance variables.
However, only one copy of each class variable is needed during the entire lifetime of the class, because
class variables are shared among all instances of a class. Therefore, in the Java Virtual Machine, class
variables are associated with the class itself, exist throughout the lifetime of the class, and must be created
and initialized before a class is used.
When
the CoffeeCup
object is create by new, two things happen. First, the Java Virtual Machine allocates
space on the heap for the object. Once memory has been allocated for the object, the Java Virtual Machine
invokes a constructor for the object to initialize the object's fields. The constructor invoked in the example
above is the default constructor, which takes no parameters. In this case, because the class itself did not
declare a constructor, the Java compiler generated a default one. (If you declare at least one constructor in
a class, the compiler won't generate one for you.) The parentheses to the right of the class name is how
you pass parameters to the constructor. In this case, because it is a default constructor, there are no
parameters and the parentheses are empty. Constructors will be discussed in detail in Chapter 4.
Here's an example of a simple Java program. This one prints out a friendly greeting:
// On CD-ROM in file encap/ex?/Greeting.java public class Greeting { public static void main(String[] args) { System.out.println("Wake up and smell the coffee!"); } }
As the Java Virtual Machine executed the bytecodes of the Greeting program, you would see this friendly greeting at the standard output:
Wake up and smell the coffee!
[bv:show the no object way to do Echo and talk about prefering objects over classes.]
Sponsored Links
|