The Artima Developer Community
Sponsored Link

Objects and Java by Bill Venners
Chapter 2:
Classes and Objects

Advertisement

[bv: need an intro]

Abstraction

When you set out to design a Java program, you have to create abstractions. You are faced with a problem domain and (with luck) a specification, and you have to architect [bv: is architect a verb?] a solution. (The problem domain is the subject area of a particular programming effort, such as "accounting," "elevator control," or "word processing.") Given that Java is an object-oriented language, you will likely want to perform an object-oriented design. In the process, you will end up with abstractions in the form of objects, types, attributes, and behaviors.

The object-oriented design process involves the following three tasks:

These tasks are not listed in any particular order. Most likely, you will perform these tasks iteratively throughout the design process.

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.

Designing a Virtual Café

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.

Identifying Types

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.

Defining Attributes

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.

Attributes Versus Types

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.

Defining Behavior

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.

Encapsulation

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.

Defining Classes

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().

Creating Objects

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();

Sending Messages to Objects

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.

Separation of Interface and Implementation

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.


Figure 2-1. A CoffeeCup object and its message receptors

Access Levels

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.

Object Lifetimes

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.

Creating a Java Application

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.

Comments

Java has three kinds of comments:

  1. a single //,
  2. a matching pair of /* and */, and
  3. a matching pair of /** 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.

Primitive Types

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

Reference Types

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.]

Arrays

Strings

Class Variables and Class Methods

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.

Named Constants

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 and Local Variables

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.
        //...
    }
}

Method Overloading

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.

Packages and Importing

The main() Method

The EchoServer 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();
    }
}

Compiling and Running 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.

Design Corner

UML Diagram of Class CoffeeCup

Java's Object Model

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.

Recommended Naming Conventions

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:

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.

Objects as Bundles of Services

Make Members as Private as Possible

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.

Behind the Scenes

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.

The relationship between the language, class file, and virtual machine

Compile Time Resolution of Constants? Or is it too soon?

Quick Overview of the JVM's Internal Architecture

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.

Example Programs

Hello World

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!

Another Echo

[bv:show the no object way to do Echo and talk about prefering objects over classes.]


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use