Building Your Own Classes

   

Core Java™ 2: Volume I - Fundamentals
By Cay S. Horstmann, Gary Cornell
Table of Contents
Chapter 4.  Objects and Classes


In Chapter 3, you started writing simple classes. However, all those classes had just a single main method. Now the time has come to show you how to write the kind of "workhorse classes" that are needed for more sophisticated applications. These classes typically do not have a main method. Instead, they have their own instance fields and methods. To build a complete program, you combine several classes, one of which has a main method.

An Employee Class

The simplest form for a class definition in Java is:

 class NameOfClass {    constructor1    constructor2    . . .    method1    method2    . . .    field1    field2    . . . } 

graphics/notes_icon.gif

We adopt the style that the methods for the class come first and the fields come at the end. Perhaps this, in a small way, encourages the notion of looking at the interface first and paying less attention to the implementation.

Consider the following, very simplified version of an Employee class that might be used by a business in writing a payroll system.

 class Employee {    // constructor    public Employee(String n, double s,       int year, int month, int day)    {       name = n;       salary = s;       GregorianCalendar calendar =          new GregorianCalendar(year, month - 1, day);       hireDay = calendar.getTime();    }    // method 1    public String getName()    {       return name;    }    // more methods    . . .    // instance fields    private String name;    private double salary;    private Date hireDay; } 

We will break down the implementation of this class in some detail in the sections that follow. First, though, Example 4-2 shows a program code that shows the Employee class in action.

In the program, we construct an Employee array and fill it with three employee objects:

 Employee[] staff = new Employee[3]; staff[0] = new Employee("Carl Cracker", . . .); staff[1] = new Employee("Harry Hacker", . . .); staff[2] = new Employee("Tony Tester", . . .); 

Next, we use the raiseSalary method of the Employee class to raise every employee's salary by 5%:

 for (i = 0; i < staff.length; i++)    staff[i].raiseSalary(5); 

Finally, we print out information about each employee, by calling the getName, getSalary and getHireDay methods:

 for (int i = 0; i < staff.length; i++) {    Employee e = staff[i];    System.out.println("name=" + e.getName()       + ",salary=" + e.getSalary()       + ",hireDay=" + e.getHireDay()); } 

Note that the example program consists of two classes: the Employee class and a class EmployeeTest with the public access specifier. The main method with the instructions that we just described is contained in the EmployeeTest class.

The name of the source file is EmployeeTest.java since the name of the file must match the name of the public class. You can only have one public class in a source file, but you can have any number of non-public classes.

Next, when you compile this source code, the compiler creates two class files in the directory: EmployeeTest.class and Employee.class.

You start the program by giving the bytecode interpreter the name of the class that contains the main method of your program:

 java EmployeeTest 

The bytecode interpreter starts running the code in the main method in the EmployeeTest class. This code in turn constructs three new Employee objects and shows you their state.

Example 4-2 EmployeeTest.java
  1. import java.util.*;  2.  3. public class EmployeeTest  4. {  5.    public static void main(String[] args)  6.    {  7.       // fill the staff array with three Employee objects  8.       Employee[] staff = new Employee[3];  9. 10.       staff[0] = new Employee("Carl Cracker", 75000, 11.          1987, 12, 15); 12.       staff[1] = new Employee("Harry Hacker", 50000, 13.          1989, 10, 1); 14.       staff[2] = new Employee("Tony Tester", 40000, 15.          1990, 3, 15); 16. 17.       // raise everyone's salary by 5% 18.       for (int i = 0; i < staff.length; i++) 19.          staff[i].raiseSalary(5); 20. 21.       // print out information about all Employee objects 22.       for (int i = 0; i < staff.length; i++) 23.       { 24.          Employee e = staff[i]; 25.          System.out.println("name=" + e.getName() 26.             + ",salary=" + e.getSalary() 27.             + ",hireDay=" + e.getHireDay()); 28.       } 29.    } 30. } 31. 32. class Employee 33. { 34.    public Employee(String n, double s, 35.       int year, int month, int day) 36.    { 37.       name = n; 38.       salary = s; 39.       GregorianCalendar calendar 40.          = new GregorianCalendar(year, month - 1, day); 41.          // GregorianCalendar uses 0 for January 42.       hireDay = calendar.getTime(); 43.    } 44. 45.    public String getName() 46.    { 47.       return name; 48.    } 49. 50.    public double getSalary() 51.    { 52.       return salary; 53.    } 54. 55.    public Date getHireDay() 56.    { 57.       return hireDay; 58.    } 59. 60.    public void raiseSalary(double byPercent) 61.    { 62.       double raise = salary * byPercent / 100; 63.       salary += raise; 64.    } 65. 66.    private String name; 67.    private double salary; 68.    private Date hireDay; 69. } 

Using Multiple Source Files

The program in Example 4-2 has two classes in a single source file. Many programmers prefer to put each class into its own source file. For example, you can place the class Employee into a file Employee.java and EmployeeTest into EmployeeTest.java.

If you like this arrangement, then you have two choices for compiling the program. You can invoke the Java compiler with a wildcard:

 javac Employee*.java 

Then, all source files matching the wildcard will be compiled into class files. Or, you can simply type:

 javac EmployeeTest.java 

You may find it surprising that the second choice works since the Employee.java file is never explicitly compiled. However, when the Java compiler sees the Employee class being used inside EmployeeTest.java, it will look for a Employee.class file. If it does not find that file, it automatically searches for Employee.java and then compiles it. Even more is true: if the time stamp of the version of Employee.java that it finds is newer than that of the existing Employee.class file, the Java compiler will automatically recompile the file.

graphics/notes_icon.gif

If you are familiar with the "make" facility of UNIX (or one of its Windows cousins such as "nmake"), then you can think of the Java compiler as having the "make" functionality already built in.

Analyzing the Employee Class

In the sections that follow, we want to dissect the Employee class. Let's start with the methods in this class. As you can see by examining the source code, this class has one constructor and four methods:

 public Employee(String n, double s, int year, int month, int day) public String getName() public double getSalary() public Date getHireDay() public void raiseSalary(double byPercent) 

All methods of this class are all tagged as public. The keyword public means that any method in any class can call the method. (There are four possible access levels; they are covered in this and the next chapter.)

Next, notice that there are three instance fields that will hold the data we will manipulate inside an instance of the Employee class.

 private String name; private double salary; private Date hireDay; 

The private keyword makes sure that the only methods that can access these instance fields are the methods of the Employee class itself. No outside method can read or write to these fields.

graphics/notes_icon.gif

It is possible to use the public keyword with your instance fields, but it would be a very bad idea. Having public data fields would allow any part of the program to read and modify the instance fields. That completely ruins encapsulation. We strongly recommend that you always make your instance fields private.

Finally, notice that two of the instance fields are themselves objects: The name and hireDay fields are references to String and Date objects. This is quite usual: classes will often contain instance fields of class type.

First Steps with Constructors

Let's look at the constructor listed in our Employee class.

 public Employee(String n, double s, int year, int month, int day) {    name = n;    salary = s;    GregorianCalendar calendar       = new GregorianCalendar(year, month - 1, day);    hireDay = calendar.getTime(); } 

As you can see, the name of the constructor is the same as the name of the class. This constructor runs when you construct objects of the Employee class giving the instance fields the initial state you want them to have.

For example, when you create an instance of the Employee class with code like this:

 new Employee("James Bond", 100000, 1950, 1, 1); 

you have set the instance fields as follows:

 name = "James Bond"; salary = 100000; hireDay = January 1, 1950; 

There is an important difference between constructors and other methods: A constructor can only be called in conjunction with the new operator. You can't apply a constructor to an existing object to reset the instance fields. For example,

 james.Employee("James Bond", 250000, 1950, 1, 1); // ERROR 

is a compile-time error.

We will have more to say about constructors later in this chapter. For now, keep the following in mind:

  • A constructor has the same name as the class.

  • A class can have more than one constructor.

  • A constructor may take zero, one, or more parameters.

  • A constructor has no return value.

  • A constructor is always called with the new operator.

graphics/cplus_icon.gif

Constructors work the same way in Java as they do in C++. But keep in mind that all Java objects are constructed on the heap and that a constructor must be combined with new. It is a common C++ programmer error to forget the new operator:

 Employee number007("James Bond", 100000, 1950, 1, 1);    // C++, not Java 

That works in C++ but does not work in Java.

graphics/caution_icon.gif

Be careful not to introduce local variables with the same names as the instance fields. For example, the following constructor will not set the salary.

    public Employee(String n, double s, . . .) {    String name = n; // ERROR    double salary = s; // ERROR    . . . } 

The constructor declares local variables name and salary. These variables are only accessible inside the constructor. They shadow the instance fields with the same name. Some programmers such as the authors of this book write this kind of code when they type faster than they think, because their fingers are used to adding the data type. This is a nasty error that can be hard to track down. You just have to be careful in all of your methods that you don't use variable names that equal the names of instance fields.

The Methods of the Employee Class

The methods in our Employee class are quite simple. Notice that all of these methods can access the private instance fields by name. This is a key point: instance fields are always accessible by the methods of their own class.

For example,

 public void raiseSalary(double byPercent) {    double raise = salary * byPercent / 100;    salary += raise; } 

sets a new value for the salary instance field in the object that executes this method. (This particular method does not return a value.) For example, the call

 number007.raiseSalary(5); 

raises number007's salary by increasing the number007.salary variable by 5%. More specifically, the call executes the following instructions:

 double raise = number007.salary * 5 / 100; number007.salary += raise; 

The raiseSalary method has two parameters. The first parameter, called the implicit parameter, is the object of type Employee that appears before the method name. The second parameter, the number inside the parentheses after the method name, is an explicit parameter.

As you can see, the explicit parameters are explicitly listed in the method declaration, for example, double byPercent. The implicit parameter does not appear in the method declaration.

In every method, the keyword this refers to the implicit parameter. If you like, you can write the raiseSalary method as follows:

 public void raiseSalary(double byPercent) {    double raise = this.salary * byPercent / 100;    this.salary += raise; } 

Some programmers prefer that style because it clearly distinguishes between instance fields and local variables.

graphics/cplus_icon.gif

In C++, you generally define methods outside the class:

    void Employee::raiseSalary(double byPercent) // C++, not Java    {    . . . } 

If you define a method inside a class, then it is automatically an inline method.

 class Employee {    . . .    int getName() { return name; } // inline in C++ } 

In the Java programming language, all methods are defined inside the class itself. This does not make them inline.

Finally, let's look more closely at the rather simple getName, getSalary, and getHireDay methods.

 public String getName() {    return name; } public double getSalary() {    return salary; } public Date getHireDay() {    return hireDay; } 

These are obvious examples of accessor methods. Because they simply return the values of instance fields, they are sometimes called field accessors.

Wouldn't it be easier to simply make the name, salary, and hireDay fields public, instead of having separate accessor methods?

The point is that the name field is a "read only" field. Once you set it in the constructor, there is no method to change it. Thus, we have a guarantee that the name field will never be corrupted.

The salary field is not read-only, but it can only be changed by the raiseSalary method. In particular, should the value ever be wrong, only that method needs to be debugged. Had the salary field been public, the culprit for messing up the value could have been anywhere.

Sometimes, it happens that you want to get and set the value of an instance field. Then you need to supply three items:

  • A private data field;

  • A public field accessor method;

  • A public field mutator method.

This is a lot more tedious than supplying a single public data field, but there are considerable benefits:

  1. The internal implementation can be changed without affecting any code other than the methods of the class.

    For example, if the storage of the name is changed to

     String firstName; String lastName; 

    then the getName method can be changed to return

     firstName + " " + lastName 

    This change is completely invisible to the remainder of the program.

Of course, the accessor and mutator methods may need to do a lot of work and convert between the old and the new data representation. But that leads us to our second benefit.

  1. Mutator methods can perform error-checking, whereas code that simply assigns to a field may not go through the trouble.

    For example, a setSalary method might check that the salary is never less than 0.

graphics/caution_icon.gif

Be careful not to write accessor methods that return references to mutable objects. We violated that rule in our Employee class in which the getHireDay method returns an object of class Date:

    class Employee {    . . .    public Date getHireDay()    {       return hireDay;    }    . . .    private Date hireDay; } 

This breaks the encapsulation! Consider the following rogue code:

 Employee harry = . . .; Date d = harry.getHireDay(); double tenYearsInMilliSeconds     = 10 * 365.25 * 24 * 60 * 60 * 1000; d.setTime(d.getTime() - (long)tenYearsInMilliSeconds); // let's give Harry ten years added seniority 

The reason is subtle. Both d and harry.hireDay refer to the same object (see Figure 4-5). Applying mutator methods to d automatically changes the private state of the employee object!

Figure 4-5. Returning a reference to a mutable data field

graphics/04fig05.gif

If you need to return a reference to a mutable object, you should clone it first. A clone is an exact copy of an object that is stored in a new location. We will discuss cloning in detail in Chapter 6. Here is the corrected code:

 class Employee {    . . .    public Date getHireDay()    {       return (Date)hireDay.clone();    }    . . . } 

As a rule of thumb, always use clone whenever you need to return a copy of a mutable data field.

Method Access to Private Data

You know that a method can access the private data of the object on which it is invoked. What many people find surprising is that a method can access the private data of all objects of its class. For example, consider a method equals that compares two employees.

 class Employee {    . . .    boolean equals(Employee other)    {       return name.equals(other.name);    } } 

A typical call is

 if (harry.equals(boss)) . . . 

This method accesses the private fields of harry, which is not surprising. It also accesses the private fields of boss. This is legal because boss is an object of type Employee, and a method of the Employee class is permitted to access the private fields of any object of type Employee.

graphics/cplus_icon.gif

C++ has the same rule. A method can access the private features of any object of its class, not just of the implicit parameter.

Private Methods

When implementing a class, we make all data fields private, since public data are dangerous. But what about the methods? While most methods are public, private methods occur quite frequently. These methods can be called only from other methods of the same class. The reason is simple: to implement certain methods, you may wish to break up the code into many separate methods. Some of these internal methods may not be particularly useful to the public. (For example, they may be too close to the current implementation or require a special protocol or calling order.) Such methods are best implemented as private.

To implement a private method in Java, simply change the public keyword to private.

By making a method private, you are under no obligation to keep it available if you change to another implementation. The method may well be harder to implement or unnecessary if the data representation changes: this is irrelevant. The point is that as long as the method is private, the designers of the class can be assured that it is never used outside the other class operations and can simply drop it. If a method is public, you cannot simply drop it because other code might rely on it.

In sum, choose private methods:

  • For those methods that are of no concern to the class user ;

  • For those methods that could not easily be supported if the class implementation were to change.

Final Instance Fields

You can define an instance field as final. Such a field must be initialized when the object is constructed. That is, it must be guaranteed that the field value is set after the end of every constructor. Afterwards, the field may not be modified again. For example, the name field of the Employee class may be declared as final since it never changes after the object is constructed there is no setName method.

 class Employee {    . . .    private final String name; } 

graphics/exclamatory_icon.gif

It is a good idea to tag fields that don't change during an object's lifetime as final. If all fields of a class are final, then the class is immutable its objects never change after they are constructed. For example, the String class is immutable. Immutable classes have one important advantage you don't have to worry about sharing references.


       
    Top
     



    Core Java 2(c) Volume I - Fundamentals
    Building on Your AIX Investment: Moving Forward with IBM eServer pSeries in an On Demand World (MaxFacts Guidebook series)
    ISBN: 193164408X
    EAN: 2147483647
    Year: 2003
    Pages: 110
    Authors: Jim Hoskins

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net