Object-oriented programming has been around since the introduction of the language, Simula '67, in 1967. However, it really came to the forefront of programming paradigms in the mid 1980's.
Object-oriented programming mainly differs from traditional structured programming because it places the data and the operations that pertain to the data within a single data structure. In structured programming, the data and the operations on the data are separate, and this methodology requires sending data structures to procedures and functions to operate on them. Object-oriented programming solves many of the problems inherent in this design because the attributes and operations are part of the same entity. This more closely models the real world, in which all objects have both attributes and activities associated with them.
Java is a pure object-oriented language, meaning that the outermost level of data structure in Java is the object. There are no stand-alone constants, variables, or functions in Java - everything is accessed through classes and objects. This is one of the nicest features of Java as compared to other hybrid object-oriented languages, which have aspects of structured languages in addition to object extensions. For example, C++ and Object Pascal are object-oriented languages, but you can still write structured programming constructs, which dilutes the effectiveness of the object-oriented extensions. Such an option in Java is not available!
First, a distinction must be made between classes and objects. A class is a type definition, whereas an object is a variable declaration. Once you create a class, you can create as many objects based on that class as you want. The same relationship exists between classes and objects as cherry pie recipes and cherry pies-you can make as many cherry pies as you want from a single recipe.
The process of creating an object from a class is referred to as instantiating an object, or creating an instance of a class.
A class in Java can be very simple. Here is a class definition for an empty class:
class MyClass { }
Obviously, this class is not yet useful, but it is legal in Java. A slightly better class would contain some data members and methods, which will be added shortly. First, however, the syntax for instantiating a class must be covered. To create an instance of this class, the new operator is used in conjunction with the class name. An instance variable must be declared for the object.
MyClass myObject;
This, however, does not allocate memory and other resources for the object. This creates a reference called, myObject, but does not instantiate the object. The new operator performs this task.
myObject = new MyClass();
Notice that the name of the class is used as if it were a method. This is not coincidental (as you will see in an upcoming section). Once this line of code has executed, the member variables and methods of the class (which do not yet exist) can be accessed using the "." operator.
Once you have created the object, you never have to worry about destroying it. Objects in Java are automatically garbage collected. As soon as the object reference (i.e., the variable) goes out of scope, the virtual machine will automatically deallocate any resources allocated by the new operator.
As stated above, a class in Java can contain both data members and methods. Here is a class that contains just data members:
public class DogClass { String name,eyeColor; int age; boolean hasTail; }
In this example, we have created a class called, DogClass, which contains member variables for name, eyeColor, age, and a flag called hasTail. You can include any data type as the member variable of a class (primitive, composite, and object types). To access a data member, you must first create an instance of the class, then access the data using the "." operator.
You can also include methods in classes. In fact, there are no stand-alone functions or procedures in Java - all subroutines are defined as methods of classes. Here is an example of the DogClass above with a speak() method added:
public class DogClass { String name,eyeColor; int age; boolean hasTail; public void speak() { Message msgSpeak = new Message(); msgSpeak.setMessage( "arf, arf" ); msgSpeak.setFrame( new Frame() ); msgSpeak.show(); } }
Notice that when you define methods, the implementation for the method appears directly below the declaration. This is unlike some other object-oriented languages where the class is defined in one location and the implementation code appears somewhere else. Also notice that you must specify a return type and any parameters received by the method.
To call the method, you would access it just like you would access the member variables. For example,
DogClass dog = new DogClass(); dog.age = 4; dog.speak();
Every class has a special purpose method called a constructor. The constructor always has the same name as the class and cannot specify a return value. The constructor takes care of allocating all the resources needed by the object and returning an instance of the object. When you use the new operator, you are actually calling the constructor. The reason that you do not need to specify a return type for the constructor is that the instance of the object is always the return type!
Most object-oriented languages have a corresponding method called a destructor, which is called to deallocate all the resources that the constructor allocated. However, as stated above, Java takes care of deallocating all the resources for you, thus, there is no destructor mechanism in Java.
However, there are situations that require you to perform some special cleanup that the garbage collector cannot handle, as the class goes away. For example, you may have opened some files in the life of the object and you want to make sure the files are closed properly when the object is destroyed. There is another special purpose method that can be defined for a class called a finalizer. This method (if present) is called by the garbage collector immediately before the object is destroyed. Thus, if there is any special cleanup that needs to be performed, the finalizer can handle it for you. However, the garbage collector runs as a low priority thread in the virtual machine, so you can never predict when it will actually destroy your object. So, you should not put any time-sensitive code in the finalizer because you cannot predict when it will be called.
In this section, we will see a simple example of defining classes and instantiating objects. We will develop an application that creates two objects (a dog and a man) and show their attributes on a form.
Here is a screenshot of the running application.
Figure 8.1 - OOP1 form showing two instantiated objects
Here is a listing of the file DogClass.java:
package Oop1; public class DogClass { String name,eyeColor; int age; boolean hasTail; public DogClass() { name = "Snoopy"; eyeColor = "Black"; age = 2; hasTail = true; } }
As you can see, we are defining DogClass and supplying some member variables. There is also a constructor to handle instantiating DogClass objects.
Here is the source for the ManClass (found in ManClass.java):
package Oop1; public class ManClass { String name,eyeColor; int age; boolean isMarried; public ManClass() { name = "Bob"; eyeColor = "Blue"; age = 1; isMarried = true; } }
There are obviously some distinct similarities (which we will take advantage of in an upcoming section).
Within the Frame class, we declare two instance variables as references to our objects. Here is the source listing of the Frame1 variable declarations:
public class Frame1 extends DecoratedFrame { // Create a reference for the objects DogClass dog; ManClass man; XYLayout xYLayout2 = new XYLayout(); BevelPanel bevelPanel1 = new BevelPanel(); . . .
Here is the event handler code for the Create Dog button where we instantiate the object and fill in some form text fields:
void button1_actionPerformed(ActionEvent e) { dog = new DogClass(); txtfldDogName.setText( dog.name ); txtfldDogEyeColor.setText( dog.eyeColor ); txtfldDogAge.setText( Integer.toString( dog.age ) ); chkbxDog.setState( dog.hasTail); }
As shown, we call the constructor for the dog object and then access its member variables.
Here is the similar source code for the Create Man button's event handler:
void button2_actionPerformed(ActionEvent e) { man = new ManClass(); txtfldManName.setText( man.name ); txtfldManEyeColor.setText( man.eyeColor ); txtfldManAge.setText( Integer.toString( man.age ) ); chkbxMan.setState( man.isMarried ); }
The dog and man classes we created have a lot of similarities. One of the benefits of object-oriented programming is the ability to handle similarities like this within a hierarchy. This ability is referred to as inheritance. When a class inherits from another class, the child class automatically inherits all the characteristics (member variables) and behavior (methods) from the parent class. Notice that inheritance is always additive - there is no way to inherit from a class and get less than what the parent class has.
Inheritance in Java is handled through the keyword, extends. When one class inherits from another class, the child class extends the parent class.
public class DogClass extends MammalClass { . . . }
The items that men and dogs have in common could be said to be common to all mammals, thus we can create a MammalClass to handle these similarities. We can then remove the declarations of the common items from DogClass and ManClass, declare them in MammalClass instead, and then subclass DogClass and ManClass from MammalClass. The following is the declaration of MammalClass:
package Oop2; public class MammalClass { String name,eyeColor; int age; public MammalClass() { name = "The Name"; eyeColor = "Black"; age = 0; } }
Notice that the MammalClass has the common characteristics from both the DogClass and the ManClass. Now, we can rewrite the DogClass and ManClass to take advantage of inheritance.
package Oop2; public class DogClass extends MammalClass { boolean hasTail; public DogClass() { // implied Super() name = "Snoopy"; age = 2; hasTail = true; } }
package Oop2; public class ManClass extends MammalClass{ boolean isMarried; public ManClass() { name = "Bob"; eyeColor = "Blue"; age = 1; isMarried = true; } }
Notice that as soon as DogClass extends MammalClass, DogClass has all the member variables and methods that the MammalClass has. In fact, even MammalClass is inherited from another class. All classes in Java ultimately extend the Object class; so if a class is declared that does not extend another class, it implicitly extends the Object class.
Classes in Java can only inherit from one class at a time (single inheritance). Some languages (such as C++) allow a class to inherit from several classes at once (multiple inheritance) but this is not the case in Java. A class can only extend one class at a time. Although, there is no restriction on how many times you can use inheritance to extend the hierarchy, you must do so one extension at a time. Multiple inheritance is a nice feature, but it leads to very complex object hierarchies. Java has a mechanism that provides many of the same benefits without so much complexity, called interfaces (covered in an upcoming section).
The MammalClass has a constructor, which sets very practical and convenient default values, and it would be very beneficial if the sub-classes could access this constructor.
In fact, they can. There are two ways to go about this in Java. If you do not explicitly call the parent's constructor, Java automatically calls it for you as the first line of the child constructor. The only way to prevent this behavior is to call one of the parent's constructors, yourself, as the first line of the child class' constructor. The constructor calls are always chained like this, and this mechanism cannot be defeated. This is a very nice feature of the Java language, because in other object-oriented languages it is a common bug not to call the parent's constructor. Java will always do this for you if you do not. That is the meaning of the comment in the first line of the DogClass constructor (implied Super()). The MammalClass constructor is called at that point, automatically. This mechanism relies on the existence of a super class constructor, which takes no parameters. If the constructor does not exist and you do not call one of the other constructors as the first line of the child constructor, the class will not compile.
Because you frequently want to call the super class constructor explicitly, there is a keyword in Java that makes this easy. Super() will call the parent's constructor which has the appropriate supplied parameters. It is also possible to have more than one constructor for a class (which will be discussed in the "Overloading" section). If you are creating more than one constructor, you typically do not want to duplicate the common code. So, you can call a same class constructor using the this() keyword, sending it any required parameters.
For the example application, the change in the hierarchy is the only difference between the first two versions of the sample. The instantiation of the objects and the main form have not changed at all. However, the design of the application is more effective, because now if we have to modify any of the mammal characteristics, we can do so in the MammalClass and just recompile the child classes. Those changes will automatically flow to the child classes.
Another aspect of classes in Java is the accessibility of the members (both variables and methods) in the class. There are several options in Java to allow you to closely tailor how accessible you want these members to be.
As a general rule of thumb, you want to limit the scope of program elements (and this includes class members) as much as possible-the fewer places something is accessible, the fewer places it can be accessed incorrectly.
There are four different access modifiers for class members in Java: private, protected, public, and default (or the absence of any modifier). This is slightly complicated by the fact that classes within the same package have different access than classes outside the package. Thus, here are two charts which show both the accessibility and inheritability of classes and member variables from within the same package and from outside the package (packages are discussed in a later section).
Access Modifier |
Inherited |
Accessible |
Default (no modifier) |
C |
C |
Public |
C |
C |
Protected |
C |
C |
Private |
D |
D |
This table shows how class members are accessed and inherited from, with respect to other members in the same package. For example, a member that is declared to be private cannot be accessed by, or inherited from, other members of the same package. On the other hand, members declared using the other modifiers, could be accessed by and inherited from all other members of that package.
Access Modifier |
Inherited |
Accessible |
default (no modifier) |
D |
D |
Public |
C |
C |
Protected |
C |
D |
Private |
D |
D |
For example, this table shows that a protected member could be inherited from, but not accessed, by classes outside its package.
The main item to note in the above charts is that public members are available to anyone who wants to access them, (notice that constructors are always public) whereas, private members are never accessible nor inheritable outside the class. So, any member variable or method that is to be kept strictly internal to the class, should be private.
It is common in object-oriented languages to support the idea of information hiding within the class by making all of the member variables of the class private and accessing them through methods that are in a specific format called, Accessor methods.
Accessor methods are methods that provide the outward public interface to the class while keeping the actual data storage private to the class. This is a good idea because you can, at any time in the future, change the internal representation of the data in the class without touching the methods that actually set those internal values.
If you do not change the public interface to the class, you do not break any code that relies on that class and its public methods. You are free to change the internal workings without breaking any code that relies on this class.
Accessor methods in Java typically come in pairs: one to get the internal value, and another to set the internal value. By convention, the Get method uses the internal private name with "get" as a prefix and the Set method does the same with "set". Notice that a read-only property would only have a "get" method. Typically, Boolean Get methods use "is" or "has" as the prefix instead of "get". Accessor methods also make it easy to validate the data that is assigned to a particular member variable.
Here is an example. For our DogClass, we have made all of the internal member variables private and added accessor methods to access the internal values. Notice that the DogClass only creates one new member variable, hasTail.
package Oop3; import borland.jbcl.control.*; import java.awt.*; public class DogClass extends MammalClass{ // accessor methods for properties // Tail public boolean hasTail() { return tail; } public void setTail( boolean value ) { tail = value; } public DogClass() { setName( "Snoopy" ); setSound( "Arf, arf!" ); setAge( 2 ); setTail( true ); } public void Speed() { Message speedMessage = new Message(); speedMessage.setMessage("30 mph"); speedMessage.setFrame(new Frame()); speedMessage.show(); } private boolean tail; }
Notice that tail has been moved to the bottom of the class and now is declared as private. The location of the definition is not important, but it is common in Java to place the private members of the class at the bottom of the class definition (after all, you can't get to them outside the class; therefore, if you are reading the code, you are interested in the public aspects, first). We also created public methods to both, get the value (hasTail()), and set the value (setTail()). Notice that the MammalClass also has Accessor methods for its member variables, so the DogClass constructor uses the "set" methods to assign those values.
It is possible to declare a method in a class as abstract, meaning that there will be no implementation for the method within this class, but all classes that extend this class must provide an implementation. Once you have an abstract method in a class, the entire class must also be declared as abstract. This indicates that a class which includes at least one abstract method (and is therefore, an abstract class) cannot be instantiated.
Here is an example: We want all mammals to have the ability to report their top running speed. However, different mammals will report this differently. Thus, in the mammal class we have created an abstract method called Speed(). This will force all subclasses to implement a method that demonstrates speed. This class also demonstrates the complete use of accessor methods (The example also demonstrates interfaces-to be discussed in the next section).
package Oop3; import Oop3.SoundInterface; import java.awt.*; import borland.jbcl.control.*; abstract public class MammalClass implements SoundInterface { // accessor methods for properties // name public String getName() { return name; } public void setName( String value ) { name = value; } // eyecolor public String getEyeColor() { return eyeColor; } public void setEyeColor( String value ) { eyeColor = value; } // sound public String getSound() { return sound; } public void setSound( String value ) { sound = value; } // age public int getAge() { return age; } public void setAge( int value ) { if ( value > 0 ) { age = value; } else age = 0; } public MammalClass() { name = "The Name"; eyeColor = "Black"; age = 0; } public void Speak() { Message soundMessage = new Message(); soundMessage.setMessage( this.sound ); soundMessage.setFrame( new Frame() ); soundMessage.show(); } abstract public void Speed(); private String name, eyeColor, sound; private int age; }
Notice the declaration of the abstract method Speed() and also the declaration of the class as abstract.
Polymorphism is the ability for two separate, yet related, classes to receive the same message but act on it in their own way. In other words, two different (but related) classes can have the same method name but implement it in different ways.
Thus, you can have a method in a class, which is also implemented in a child class, and access the code from the parent's class (similar to the automatic constructor chaining discussed earlier). Just as in the constructor example, you can use the keyword super to access any of the superclass' methods or member variables.
Here is a simple example. We have two classes (Parent and Child).
class Parent { int x = 1; int someMethod(){ return x; } } class Child extends Parent { int x; // this x is part of this class int someMethod() { // this overrides parent's method x = super.x + 1; // access parent's x with super return super.someMethod() + x; } }
It is possible in Java to create several methods of a class which have the same name but have different parameters and/or return value. This is referred to as method overloading. Java takes care of deciding which method to call by looking at the return value and the parameters.
An interface is functionally like an abstract class but with one important difference: an interface cannot include any code. The interface mechanism in Java is meant to replace multiple inheritance.
An interface is a specialized class declaration that can declare constants and method declarations, but not implementation-code can never be placed in an interface.
Here is an example interface declaration.
package Oop3; interface SoundInterface { public void Speak(); }
Note that the interface keyword is used instead of class. All methods declared in an interface are public by default, so there is no need to specify accessibility. A class can implement an interface by using the implements keyword. Also, a class can only extend one other class, but a class can implement as many interfaces as it wants. This is how situations, which are normally handled by multiple inheritance, are handled by interfaces in Java. In many situations, you can treat the interface as if it were a class. In other words, you can treat objects that implement an interface as subclasses of the interface for convenience. However, notice that you can only access the methods defined by that interface if you are casting an object that implements the interface.
The following is an example of both polymorphism and interfaces: The MammalClass definition above implements the SoundInterface shown above. Remember that MammalClass also contains an abstract method called Speed(). Here are the new DogClass and ManClass source files.
package Oop3; import borland.jbcl.control.*; import java.awt.*; public class DogClass extends MammalClass{ // accessor methods for properties // Tail public boolean hasTail() { return tail; } public void setTail( boolean value ) { tail = value; } public DogClass() { setName( "Snoopy" ); setSound( "Arf, arf!" ); setAge( 2 ); setTail( true ); } public void Speed() { Message speedMessage = new Message(); speedMessage.setMessage("30 mph"); speedMessage.setFrame(new Frame()); speedMessage.show(); } private boolean tail; } package Oop3; import borland.jbcl.control.*; import java.awt.*; public class ManClass extends MammalClass { // accessor methods for properties // married public boolean isMarried() { return married; } public void setMarried( boolean value ) { married = value; } public ManClass() { setName( "Bob" ); setEyeColor( "Blue" ); setSound( "Hello there!" ); setAge( 1 ); setMarried( true ); } public void Speed() { Message speedMessage = new Message(); speedMessage.setMessage( "22 mph" ); speedMessage.setFrame( new Frame() ); speedMessage.show(); } private boolean married; }
Because both these classes extend MammalClass, they must both provide an implementation of the Speed() method. Notice, too, that they also implement the SoundInterface interface because it is implemented by the MammalClass. In fact, the Speak() method is defined in MammalClass, but the sound that each mammal will make is specified in the constructor.
There has also been some changes to the main form of the application. We have added two buttons, Speed and Speak.
Figure 8.2 - New version of the sample application with Speed and Speak buttons added
We have also added a couple of declarations to the form's class.
// Create a reference for the objects DogClass dog; ManClass man; //Create an Array of SoundInterface SoundInterface soundList[] = new SoundInterface[2]; //Create an Array of Mammal MammalClass mammalList[] = new MammalClass[2];
In addition to creating references for dog and man, we also have created a couple of arrays in terms of both Mammals and SoundInterfaces. Then, when we create the dog and man, we add references to them in both lists.
void button1_actionPerformed(ActionEvent e) { dog = new DogClass(); txtfldDogName.setText( dog.getName() ); txtfldDogEyeColor.setText( dog.getEyeColor() ); txtfldDogAge.setText( Integer.toString( dog.getAge()) ); chkbxDog.setState( dog.hasTail() ); mammalList[0] = dog; soundList[0] = dog; } void button2_actionPerformed(ActionEvent e) { man = new ManClass(); txtfldManName.setText( man.getName() ); txtfldManEyeColor.setText( man.getEyeColor() ); txtfldManAge.setText( Integer.toString( man.getAge()) ); chkbxMan.setState( man.isMarried() ); mammalList[1] = man; soundList[1] = man; }
Notice that we can add both objects to both lists without any casting. This is because they can both be thought of as mammals and objects that can speak because of their lineage.
The code under the Speed button loops through the list and tells each object to display its speed.
void button4_actionPerformed(ActionEvent e) { for (int i = 0; i <= 1; i++) { mammalList[i].Speed(); } }
Notice again that we do not have to cast either object to access the Speed() method. The first time through the list, the dog displays speed, the second time through the list, the man displays speed. This is polymorphism in action - two separate but related objects receiving the same message and reacting to it in their own way.
The code under the Speak button is similar.
void button3_actionPerformed(ActionEvent e) { for (int i = 0; i <= 1; i++) { soundList[i].Speak(); } }
You'll see that we can treat the SoundInterface as a class when it is convenient. Again, we do not have to do any casting to execute this method against these objects. However, because this is an interface, we cannot access the Speed() method from this reference without type casting. But notice that the interface gives us some of the benefits of multiple inheritance without the added complexity.
In order to facilitate code reuse, Java allows you to group several class definitions together in a logical grouping called a package. If, for instance, you create a group of business rules that model the work processes of your organization, you might want to place them together in a package. This makes it easier to reuse code that you have previously created.
The Java language comes with many predefined packages. For instance, the java.applet package contains classes for working with Java applets. In the previous example, we were declaring an applet subclass with the following line of code:
public class Hello extends java.applet.Applet {
In this code, we were really referring to the class called Applet in the Java package java.applet. You can imagine that it might get quite tedious to have to repeat the entire full class name java.applet.Applet every time we referred to this class. Instead, Java offers an alternative. You can choose to import a package you will use frequently:
import java.applet.*;
This tells the compiler "if you see a class name you do not recognize, look in the java.applet package for it." Now, when we declare our new class, we can say,
public class Hello extends Applet {
which is a little less verbose. However, this does present a problem if you have two classes by the same name defined in two different packages that are imported. In this case, you must use the fully qualified name.
Creating your own packages is almost as easy as using them. For instance, if you want to create a package called, mypackage, you would simply use a package statement at the beginning of your file:
package mypackage; public class Hello extends java.applet.Applet { public void init() { add(new java.awt.Label("Hello World Wide Web!")); } } // end class
Now, any other program can access the classes declared in mypackage with the statement:
import mypackage.*;
Remember, this file should be in a subdirectory called mypackage. This allows your Java compiler to easily locate your package. JBuilder's Project Wizard will automatically set the directory to match the project name. Also, keep in mind that the base directory of any package you import must be listed in the Source Path of the JBuilder IDE or the Source Path of your project. This is good to remember if you decide to relocate a package to a different base directory.
"The Java Language Specification" section in the help files has more information about packages, including a naming convention.
In the Paths section of the Project Properties dialog, you can set the following package-related properties:
When a piece of Java code references an external package (e.g. you use one of the components in the java.awt package), the Java environment and compiler will look through the Source and Class paths to find a subdirectory with the same name as the package.
What was covered in this chapter: