Core Java(TM) 2, Volume I--Fundamentals (7th Edition) (Core Series) (Core Series)

   

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 ClassName{

   constructor1

   constructor2

   . . .   method1   method2

   . . .

   field1   field2

   . . .

}

NOTE

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(); } // a method public String getName() { return name; } // more methods . . . // instance fields private String name; private double salary; private Date hireDay; }

We 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 each employee's salary by 5%:

for (Employee e : staff) e.raiseSalary(5);

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

for (Employee e : staff) 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 because the name of the file must match the name of the public class. You can have only one public class in a source file, but you can have any number of nonpublic 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, 1987, 12, 15); 11. staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1); 12. staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15); 13. 14. // raise everyone's salary by 5% 15. for (Employee e : staff) 16. e.raiseSalary(5); 17. 18. // print out information about all Employee objects 19. for (Employee e : staff) 20. System.out.println("name=" + e.getName() 21. + ",salary=" + e.getSalary() 22. + ",hireDay=" + e.getHireDay()); 23. } 24. } 25. 26. class Employee 27. { 28. public Employee(String n, double s, int year, int month, int day) 29. { 30. name = n; 31. salary = s; 32. GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); 33. // GregorianCalendar uses 0 for January 34. hireDay = calendar.getTime(); 35. } 36. 37. public String getName() 38. { 39. return name; 40. } 41. 42. public double getSalary() 43. { 44. return salary; 45. } 46. 47. public Date getHireDay() 48. { 49. return hireDay; 50. } 51. 52. public void raiseSalary(double byPercent) 53. { 54. double raise = salary * byPercent / 100; 55. salary += raise; 56. } 57. 58. private String name; 59. private double salary; 60. private Date hireDay; 61. }

Use of 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 Employee class into a file Employee.java and the EmployeeTest class 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 because 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 file named Employee.class. 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.

NOTE

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.

Dissecting 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 tagged as public. The keyword public means that any method in any class can call the method. (The four possible access levels are covered in this and the next chapter.)

Next, notice that three instance fields 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.

NOTE

You could 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. Any method of any class can modify public fields and, in our experience, some code usually will take advantage of that access privilege when you least expect it. 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 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 can take zero, one, or more parameters.

  • A constructor has no return value.

  • A constructor is always called with the new operator.

C++ NOTE

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.

CAUTION

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.

Implicit and Explicit Parameters

Methods operate on objects and access their instance fields. For example, the method

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

sets a new value for the salary instance field in the object on which this method is invoked. Consider the call

number007.raiseSalary(5);

The effect is to increase the value of the number007.salary field 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.

C++ NOTE

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. Finding opportunities for inline replacement is the job of the Java virtual machine. The just-in-time compiler watches for calls to methods that are short, commonly called, and not overridden, and optimizes them away.

Benefits of Encapsulation

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; and

  • A public field mutator method.

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

  1. You can change the internal implementation 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 to the trouble.

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

CAUTION

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

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

Class-Based Access Privileges

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.

C++ NOTE

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 because public data are dangerous. But what about the methods? While most methods are public, private methods are used in certain circumstances. Sometimes, you may wish to break up the code for a computation into separate helper methods. Typically, these helper methods should not become part of the public interface 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.

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 has been 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 because it never changes after the object is constructed there is no setName method.

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

The final modifier is particularly useful for fields whose type is primitive or an immutable class. (A class is immutable if none of its methods ever mutate its objects. For example, the String class is immutable.) For mutable classes, the final modifier is likely to confuse the reader. For example,

private final Date hiredate;

merely means that the object reference stored in the hiredate variable doesn't get changed after the object is constructed. That does not mean that the hiredate object is constant. Any method is free to invoke the setTime mutator on the object to which hiredate refers.


       

    Категории