Полезная информация

Java Language Reference

Previous Chapter 5
Declarations
Next
 

5.3 Object-Orientation Java Style

Before considering class and interface declarations in Java, it is essential that you understand the object-oriented model used by the language. No useful programs can be written in Java without using objects. Java deliberately omits certain C++ features that promote a less object-oriented style of programming. Thus, all executable code in a Java program must be part of an object (or a class to be more precise).

The two main characteristics of objects in Java are:

Classes

An object is a collection of variables, associated methods, and other associated classes. Objects in Java are described by classes; a particular object is an instance of a particular class. A class describes the data an object can contain by defining variables to contain the data in each instance of the class. A class describes the behavior of an object by defining methods for the class and possibly other auxiliary classes. Methods are named pieces of executable code; they are similar to what other programming languages call functions or procedures. Collectively, the variables, methods, and auxiliary classes of a class are called its members.

A class can define multiple methods with the same name if the number or type of parameters for each method is different. Multiple methods with the same name are called overloaded methods. Like C++, Java supports overloaded methods, but unlike C++, Java does not support overloaded operators. Overloaded methods are useful when you want to describe similar operations on different types of data. For example, Java provides a class called java.io.OutputStream that is used to write data. The OutputStream class defines three different write() methods: one to write a single byte of data, another to write some of the bytes in an array, and another to write all of the bytes in an array.

References Class Declarations

Encapsulation

Encapsulation is the technique of hiding the details of the implementation of an object, while making its functionality available to other objects. When encapsulation is used properly, you can change an object's implementation without worrying that any other object can see, and therefore depend on, the implementation details.

The portion of an object that is accessible to other types of objects is called the object's interface.[1] For example, consider a class called Square. The interface for this class might consist of:

[1] The notion of an object's interface is a commonly accepted concept in the object-oriented community. Later in this chapter, a Java construct called an interface is described. A Java interface is not the same thing as the interface of an object, so there is some potential for confusion. Outside of this section, the term "interface" is only used to mean the Java interface construct.

The implementation of this Square class would include executable code that implements the various methods, as well as an internal variable that an object would use to remember its size. Variables that an object uses to remember things about itself are called state variables.

The point of the distinction between the interface and the implementation of a class is that it makes programs easier to maintain. The implementation of a class may change, but as long as the interface remains the same, these changes do not require changes to any other classes that may use the class.

In Java, encapsulation is implemented using the public, protected, and private access modifiers. If a field of a class is part of the interface for the class, the field should be declared with the public modifier or with no access modifier. The private and protected modifiers limit the accessibility of a field, so these modifiers should be used for state variables and other implementation-specific functionality.

Here's a partial definition of a Square class that has the interface just described:

class Square {
    private int sideLength;
    public void setSideLength(int len) {
        sideLength = len;
    }
    public int getSideLength() {
        return sideLength;
    }
    public void draw(int x, int y) {
        // code to draw the square
        ...
    }
}

References Method modifiers; Inner class modifiers; Variable modifiers

Object Creation

An object is typically created using an allocation expression. The newInstance() methods of the Class or java.lang.reflect.Contructor class can also be used to create an instance of a class. In either case, the storage needed for the object is allocated by the system.

When a class is instantiated, a special kind of method called a constructor is invoked. A constructor for a class does not have its own name; instead it has the same name as the class of which it is a part. Constructors can have parameters, just like regular methods, and they can be overloaded, so a class can have multiple constructors. A constructor does not have a return type. The main purpose of a constructor is to do any initialization that is necessary for an object.

If a class declaration does not define any constructors, Java supplies a default public constructor that takes no parameters. You can prevent a class from being instantiated by methods in other classes by defining at least one private constructor for the class without defining any public constructors.

References Class; Constructors; Object Allocation Expressions

Object Destruction

Java does not provide any way to explicitly destroy an object. Instead, an object is automatically destroyed when the garbage collector detects that it is safe to do so. The idea behind garbage collection is that if it is possible to prove that a piece of storage will never be accessed again, that piece of storage can be freed for reuse. This is a more reliable way of managing storage than having a program explicitly deallocate its own storage. Explicit memory allocation and deallocation is the single largest source of programming errors in C/C++. Java eliminates this source of errors by handling the deallocation of memory for you.

Java's garbage collector runs continuously in a low priority thread. You can cause the garbage collector to take a single pass through allocated storage by calling System.gc().

Garbage collection will never free storage before it is safe to do so. However, garbage collection usually does not free storage as soon as it would be freed using explicit deallocation. The logic of a program can sometimes help the garbage collector recognize that it is safe to free some storage sooner rather than later. Consider the following code:

class G {
    byte[] buf;
    String readIt(FileInputStream f) throws IOException {
        buf = new byte[20000];
        int length = f.read(buf);
        return new String(buf, 0, 0, length);
    }
}

The first time readIt() is called, it allocates an array that is referenced by the instance variable buf. The variable buf continues to refer to the array until the next time that readIt() is called, when buf is set to a new array. Since there is no longer any reference to the old array, the garbage collector will free the storage on its next pass. This situation is less than optimal. It would be better if the garbage collector could recognize that the array is no longer needed once a call to readIt() returns. Defining the variable buf as a local variable in readIt() solves this problem:

class G {
    String readIt(FileInputStream f) throws IOException {
        byte[] buf;
        buf = new byte[20000];
        int length = f.read(buf);
        return new String(buf, 0, 0, length);
    }
}

Now the reference to the array is in a local variable that disappears when readIt() returns. After readIt() returns, there is no longer any reference to the array, so the garbage collector will free the storage on its next pass.

Just as a constructor is called when an object is created, there is a special method that is called before an object is destroyed by the garbage collector. This method is called a finalizer ; it has the name finalize(). A finalize() method is similar to a destructor in C++. The finalize() method for a class must be declared with no parameters, the void return type, and no modifiers. A finalizer can be used to clean up after a class, by doing such things as closing files and terminating network connections.

If an object has a finalize() method, it is normally called by the garbage collector before the object is destroyed. A program can also explicitly call an object's finalize() method, but in this case, the garbage collector does not call the method during the object destruction process. If the garbage collector does call an object's finalize() method, the garbage collector does not immediately destroy the object because the finalize() method might do something that causes a variable to refer to the object again.[2] Thus the garbage collector waits to destroy the object until it can again prove it is safe to do so. The next time the garbage collector decides it is safe to destroy the object, it does so without calling the finalizer again. In any case, a finalize() method is never called more than once for a particular object.

[2] A finalize() method should not normally do something that results in a reference to the object being destroyed, but Java does not do anything to prevent this situation from happening.

The garbage collector guarantees that the thread it uses to call a finalize() method will not be holding any programmer-visible synchronization locks when the method is called. This means that a finalize() method never has to wait for the garbage collector to release a lock. If the garbage collector calls a finalize() method and the finalize() method throws any kind of exception, the garbage collector catches and ignores the exception.

References System; The finalize method

Inheritance

One of the most important benefits of object-oriented programming is that it promotes the reuse of code, particularly by means of inheritance. Inheritance is a way of organizing related classes so that they can share common code and state information. Given an existing class declaration, you can create a similar class by having it inherit all of the fields in the existing definition. Then you can add any fields that are needed in the new class. In addition, you can replace any methods that need to behave differently in the new class.

To illustrate the way that inheritance works, let's start with the following class definition:

class RegularPolygon {
    private int numberOfSides;
    private int sideLength;
    RegularPolygon(int n, int len) {
        numberOfSides = n;
        sideLength = len;
    }
    public void setSideLength(int len) {
        sideLength = len;
    }
    public int getSideLength() {
        return sideLength;
    }
    public void draw(int x, int y) {
        // code to draw the regular polygon
        ...
    }
}

The RegularPolygon class defines a constructor, methods to set and get the side length of the regular polygon, and a method to draw the regular polygon. Suppose that after writing this class you realize that you have been using it to draw a lot of squares. You can use inheritance to build a more specific Square class from the existing RegularPolygon class as follows:

class Square extends   RegularPolygon {
    Square(int len) {
        super(4,len);
    }
}

The extends clause indicates that the Square class is a subclass of the RegularPolygon class, or looked at another way, RegularPolygon is a superclass of Square. When one class is a subclass of another class, the subclass inherits all of the fields of its superclass that are not private. Thus Square inherits setSideLength(), getSideLength(), and draw() methods from RegularPolygon. These methods work fine without any modification, which is why the definition of Square is so short. All the Square class needs to do is define a constructor, since constructors are not inherited.

There is no limit to the depth to which you can carry subclassing. For example, you could choose to write a class called ColoredSquare that is a subclass of the Square class. The ColoredSquare class would inherit the public methods from both Square and RegularPolygon. However, ColoredSquare would need to override the draw() method with an implementation that handles drawing in color.

Having defined the three classes RegularPolygon, Square, and ColoredSquare, it is correct to say that RegularPolygon and Square are superclasses of ColoredSquare and ColoredSquare and Square are subclasses of RegularPolygon. To describe a relationship between classes that extends through exactly one level of inheritance, you can use the terms immediate superclass and immediate subclass. For example, Square is an immediate subclass of RegularPolygon, while ColoredSquare is an immediate subclass of Square. By the same token, RegularPolygon is the immediate superclass of Square, while Square is the immediate superclass of ColoredSquare.

A class can have any number of subclasses or superclasses. However, a class can only have one immediate superclass. This constraint is enforced by the syntax of the extends clause; it can only specify the name of one superclass. This style of inheritance is called single inheritance ; it is different from the multiple inheritance scheme that is used in C++.

Every class in Java (except Object) has the class Object as its ultimate superclass. The class Object has no superclass. The subclass relationships between all of the Java classes can be drawn as a tree that has the Object class as its root. Another important difference between Java and C++ is that C++ does not have a class that is the ultimate superclass of all of its classes.

References Class Inheritance; Interfaces; Object

Abstract classes

If a class is declared with the abstract modifier, the class cannot be instantiated. This is different than C++, which has no way of explicitly specifying that a class cannot be instantiated. An abstract class is typically used to declare a common set of methods for a group of classes when there are no reasonable or useful implementations of the methods at that level of abstraction.

For example, the java.lang package includes classes called Byte, Short, Integer, Long, Float, and Double. These classes are subclasses of the abstract class Number, which declares the following methods: byteValue(), shortValue(), intValue(), longValue(), floatValue(), and doubleValue(). The purpose of these methods is to return the value of an object converted to the type implied by the method's name. Every subclass of Number implements all of these methods. The advantage of the abstraction is that it allows you to write code to extract whatever type of value you need from a Number object, without knowing the actual type of the underlying object.

Methods defined in an abstract class can be declared abstract. An abstract method is declared without any implementation; it must be overridden in a subclass to provide an implementation.

References Class Modifiers; Inner class modifiers; Local class modifiers; Method modifiers; Number

Final classes

If a class is declared with the final modifier, the class cannot be subclassed. Declaring a class final is useful if you need to ensure the exact properties and behavior of that class. Many of the classes in the java.lang package are declared final for that reason.

Methods defined in a non-abstract class can be declared final. A final method cannot be overridden by any subclasses of the class in which it appears.

References Class Modifiers; Inner class modifiers; Local class modifiers; Method modifiers

Interfaces

Java provides a construct called an interface to support certain multiple inheritance features that are desirable in an object-oriented language. An interface is similar to a class, in that an interface declaration can define both variables and methods. But unlike a class, an interface cannot provide implementations for its methods.

A class declaration can include an implements clause that specifies the name of an interface. When a class declaration specifies that it implements an interface, the class inherits all of the variables and methods declared in that interface. The class declaration must then provide implementations for all of the methods declared in the interface, unless the class is declared as an abstract class. Unlike the extends clause, which can only specify one class, the implements clause can specify any number of interfaces. Thus a class can implement an unlimited number of interfaces.

Interfaces are most useful for declaring that an otherwise unrelated set of classes have a common set of methods, without needing to provide a common implementation. For example, if you want to store a variety of objects in a database, you might want all of the those objects to have a common set of methods for storing and fetching. Since the fetch and store methods for each object need to be different, it is appropriate to declare these methods in an interface. Then any class that needs fetch and store methods can implement the interface.

Here is a simplistic example that illustrates such an interface:

public interface Db {
    void dbStore(Database d, Object key);
    Object dbFetch(Database d, Object key);
}

The Db interface declaration contains two methods, dbStore() and dbFetch(). Here is a partial class definition for a class that implements the Db interface:

class DbSquare extends Square implements Db {
    public void dbStore(Database d, Object key) {
        // Perform database operation to store Square
        ...
    }
    public Square dbFetch(Database d, Object key) {
        // Perform database operation to fetch Square
        ...
    }
    ...
}

The DbSquare class defines implementations for both of the methods declared in the Db interface. The point of this interface is that it provides a uniform way for unrelated objects to arrange to be stored in a database. The following code shows part of a class that encapsulates database operations:

class Database {
    ...
    public void store(Object o, Object key) {
        if (o instanceof Db)
          ((Db)o).dbStore(this, key);
    }
    ...
}

When the database is asked to store an object, it does so only if the object implements the Db interface, in which case it can call the dbStore() of the object.

References Interface Declarations

Inner Classes

Java 1.1 provides a new feature that allows programmers to encapsulate even more functionality within objects. With the addition of inner classes to the Java language, classes can be defined as members of other classes, just like variables and methods. Classes can also be defined within blocks of Java code, just like local variables. The ability to declare a class inside of another class allows you to encapsulate auxiliary classes inside of a class, thereby limiting access to the auxiliary classes. A class that is declared inside of another class may have access to the instance variables of the enclosing class; a class declared within a block may have access to the local variable and/or formal parameters of that block.

Nested top-level classes and interfaces

A nested top-level class or interface is declared as a static member of an enclosing top-level class or interface. The declaration of a nested top-level class uses the static modifier, so you may also see these classes called static classes. A nested interface is implicitly static, but you can declare it to be static to make it explicit. Nested top-level classes and interfaces are typically used to group related classes in a convenient way.

A nested top-level class or interface functions like a normal top-level class or interface, except that the name of the nested entity includes the name of the class in which it is defined. For example, consider the following declaration:

public class Queue {
    ...
    public static class EmptyQueueException extends Exception {
    } 
    ...
}

Code that calls a method in Queue that throws an EmptyQueueException can catch that exception with a try statement like this:

try {
    ...
} catch (Queue.EmptyQueueException e) {
    ...
}

A nested top-level class cannot access the instance variables of its enclosing class. It also cannot call any non-static methods of the enclosing class without an explicit reference to an instance of that class. However, a nested top-level class can use any of the static variables and methods of its enclosing class without qualification.

Only top-level classes in Java can contain nested top-level classes. In other words, a static class can only be declared as a direct member of a class that is declared at the top level, directly as a member of a package. In addition, a nested top-level class cannot declare any static variables, static methods, or static initializers.

References Class Declarations; Methods; Nested Top-Level and Member Classes; Variables

Member classes

A member class is an inner class that is declared within an enclosing class without the static modifier. Member classes are analogous to the other members of a class, namely the instance variables and methods. The code within a member class can refer to any of the variables and methods of its enclosing class, including private variables and methods.

Here is a partial definition of a Queue class that uses a member class:

public class Queue {
    private QueueNode queue;
    ...
    public Enumeration elements() {
        return new QueueEnumerator();
    } 
    ...
    private class QueueEnumerator implements Enumeration {
        private QueueNode start, end;
        QueueEnumerator() {
            synchronized (Queue.this) {
                if (queue != null) {
                    start = queue.next;
                    end = queue;
                } 
            } 
        } 
        public boolean hasMoreElements() {
            return start != null;
        } 
        public synchronized Object nextElement() {
            ...
        } 
    } 
    private static class QueueNode {
        private Object obj;
        QueueNode next;
        QueueNode(Object obj) {
            this.obj = obj;
        }
        Object getObject() {
            return obj;
        }
    } 
}

The QueueEnumerator class is a private member class that implements the java.util.Enumeration interface. The advantage of this approach is that the QueueEnumerator class can access the private instance variable queue of the enclosing Queue class. If QueueEnumerator were declared outside of the Queue class, this queue variable would need to be public, which would compromise the encapsulation of the Queue class. Using a member class that implements the Enumeration interface provides a means to offer controlled access to the data in a Queue without exposing the internal data structure of the class.

An instance of a member class has access to the instance variables of exactly one instance of its enclosing class. That instance of the enclosing class is called the enclosing instance. Thus, every QueueEnumerator object has exactly one Queue object that is its enclosing instance. To access an enclosing instance, you use the construct ClassName.this. The QueueEnumerator class uses this construct in the synchronized statement in its constructor to synchronize on its enclosing instance. This synchronization is necessary to ensure that the newly created QueueEnumerator object has exclusive access to the internal data of the Queue object.

The Queue class also contains a nested top-level, or static, class, QueueNode. However, this class is also declared private, so it is not accessible outside of Queue. The main difference between QueueEnumerator and QueueNode is that QueueNode does not need access to any instance data of Queue.

A member class cannot declare any static variables, static methods, static classes, or static initializers.

Although member classes are often declared private, they can also be public or protected or have the default accessibility. To refer to a class declared inside of another class from outside of that class, you prefix the class name with the names of the enclosing classes, separated by dots. For example, consider the following declaration:

public class A {
    public class B {
        public class C {
            ...
        } 
        ...
    } 
    ...
} 

Outside of the class named A, you can refer to the class named C as A.B.C.

References Class Declarations; Field Expressions; Methods; Nested Top-Level and Member Classes; Variables

Local classes

A local class is an inner class that is declared inside of a block of Java code. A local class is only visible within the block in which it is declared, so it is analogous to a local variable. However, a local class can access the variables and methods of any enclosing classes. In addition, a local class can access any final local variables or method parameters that are in the scope of the block that declares the class.

Local classes are most often used for adapter classes. An adapter class is a class that implements a particular interface, so that another class can call a particular method in the adapter class when a certain event occurs. In other words, an adapter class is Java's way of implementing a "callback" mechanism. Adapter classes are commonly used with the new event-handling model required by the Java 1.1 AWT and by the JavaBeans API.

Here is an example of a local class functioning as an adapter class:

public class Z extends Applet {
    public void init() {
        final Button b = new Button("Press Me");
        add(b);
        class ButtonNotifier implements ActionListener {
            public void actionPerformed(ActionEvent e) {
                b.setLabel("Press Me Again");
                doIt();
            } 
        } 
        b.addActionListener(new ButtonNotifier());
    } 
    ...
} 

The above example is from an applet that has a Button in its user interface. To tell a Button object that you want to be notified when it is pressed, you pass an instance of an adapter class that implements the ActionListener interface to its addActionListener() method. A class that implements the ActionListener interface is required to implement the actionPerformed() method. When the Button is pressed, it calls the adapter object's actionPerformed() method. The main advantage of declaring the ButtonNotifier class in the method that creates the Button is that it puts all of the code related to creating and setting up the Button in one place.

As the preceding example shows, a local class can access local variables of the block in which it is declared. However, any local variables that are accessed by a local class must be declared final. A local class can also access method parameters and the exception parameter of a catch statement that are accessible within the scope of its block, as long as the parameter is declared final. The Java compiler complains if a local class uses a non-final local variable or parameter. The lifetime of a parameter or local variable is extended indefinitely, as long as there is an instance of a local class that refers to it.

References Blocks; Class Declarations; Local Classes; Local Variables; Method formal parameters; Methods; The try Statement; Variables

Anonymous classes

An anonymous class is a kind of local class that does not have a name and is declared inside of an allocation expression. As such, an anonymous class is a more concise declaration of a local class that combines the declaration of the class with its instantiation.

Here is how you can rewrite the previous adapter class example to use an anonymous class instead of a local class:

public class Z extends Applet {
    public void init() {
        final Button b = new Button("Press Me");
        add(b);
        b.addActionListener(new ActionListener () {
            public void actionPerformed(ActionEvent e) {
                b.setLabel("Press Me Again");
            } 
        } );
    } 
    ...
} 

As you can see, an anonymous class is declared as part of an allocation expression. If the name after new is the name of an interface, as is the case in the preceding example, the anonymous class is an immediate subclass of Object that implements the given interface. If the name after new is the name of a class, the anonymous class is an immediate subclass of the named class.

Obviously, an anonymous class doesn't have a name. The other restriction on an anonymous class is it can't have any constructors other than the default constructor. Any constructor-like initialization must be done using an instance initializer. Other than these differences, anonymous classes function just like local classes.

References Allocation Expressions; Class Declarations; Instance Initializers; Object

Implementation of inner classes

It is possible to use inner classes without knowing anything about how they are implemented. However, a high-level understanding can help you comprehend the filenames that the compiler produces, and also some of the restrictions associated with inner classes. The implementation of inner classes is less than transparent in a number of ways, primarily because the Java virtual machine does not know about inner classes. Instead, the Java compiler implements inner classes by rewriting them in a form that does not use inner classes. The advantage of this approach is that the Java virtual machine does not require any new features to be able to run programs that use inner classes.

Since a class declared inside another class is rewritten by the compiler as an external class, the compiler must give it a name unique outside of the class in which it is declared. The unique name is formed by prefixing the name of the inner class with the name of the class in which it is declared and a dollar sign ($). Thus, when the Queue class is compiled, the Java compiler produces four .class files:

  • Queue.class

  • Queue$EmptyQueueException.class

  • Queue$QueueEnumerator.class

  • Queue$QueueNode.class

Because anonymous classes do not have names, the Java compiler gives each anonymous class a number for a name; the numbers start at 1. When the version of the Z applet that uses an anonymous class is compiled, the Java compiler produces two .class files:

  • Z.class

  • Z$1.class

In order to give an inner class access to the variables of its enclosing instance, the compiler adds a private variable to the inner class that references the enclosing instance. The compiler also inserts a formal parameter into each constructor of the inner class and passes the reference to the enclosing instance using this parameter. Therefore, the QueueEnumerator class is rewritten as follows:

class Queue$QueueEnumerator implements Enumeration {
    private Queue this$0;
    private QueueNode start, end;
    QueueEnumerator(Queue this$0) {
        this.this$0 = this$0;
        synchronized (this$0) {
            if (queue != null) {
                start = queue.next;
                end = queue;
            } 
        } 
    }
    ...
}

As you can see, the compiler rewrites all references to the enclosing instance as this$0. One implication of this implementation is that you cannot pass the enclosing instance as an argument to its superclass's constructor because this$0 is not available until after the superclass's constructor returns.


Previous Home Next
Lexical Scope of Declarations Book Index Class Declarations

Java in a Nutshell Java Language Reference Java AWT Java Fundamental Classes Exploring Java