Introduction to Objects
2.4 Data Encapsulation, Accessor, and Mutator Methods
Observe that the body of the no-arg constructor consists of an empty block. When the following statement is executed, the instance variables are set to their initial values (specified or default), and the constructor is executed. In this case, nothing further happens.
Book b = new Book();
Be warned that when we provide a constructor, the default no-arg constructor is no longer available. If we want to use a no-arg constructor as well, we must write it explicitly, as in the previous example. We are free, of course, to write whatever we want in the body, including nothing.
As a final example, we provide a constructor that lets us set all the fields explicitly when an object is created. Here it is:
public Book(String a, String t, double p, int g, char b, boolean s) { author = a;
title = t;
price = p;
pages = g;
binding = b;
inStock = s;
}
If b is a variable of type Book, a sample call is as follows:
b = new Book("Noel Kalicharan", "DigitalMath", 29.95, 200, 'P', true);
The fields will be given the following values:
author "Noel Kalicharan"
title "DigitalMath"
price 29.95 pages 200 binding 'P' inStock true
This may not be desirable. Suppose NumParts is meant to count the number of objects created from Part. Any outside class can set it to any value it pleases, so the writer of the class Part cannot guarantee that it will always reflect the number of objects created.
An instance variable, as always, can be accessed via an object only. When a user class creates an object p of type Part, it can use p.price (or p.name) to refer directly to the instance variable and can change it, if desired, with a simple assignment statement. There is nothing to stop the user class from setting the variable to an unreasonable value. For instance, suppose that all prices are in the range 0.00 to 99.99. A user class can contain the following statement, compromising the integrity of the price data:
p.price = 199.99;
To solve these problems, we must make the data fields private; we say we must hide the data. We then provide public methods for others to set and retrieve the values in the fields. Private data and public methods are the essence of data encapsulation. Methods that set or change a field’s value are called mutator methods. Methods that retrieve the value in a field are called accessor methods.
Let’s show how the two problems mentioned can be solved. First, we redefine the fields as private:
public class Part {
private static int NumParts = 0; // class variable private String name; // instance variable
private double price; // instance variable }
Now that they are private, no other class has access to them. If we want NumParts to reflect the number of objects created from the class, we would need to increment it each time a constructor is called. We could, for example, write a no-arg constructor as follows:
public Part() { name = "NO PART";
price = -1.0; // we use –1 since 0 might be a valid price NumParts++;
}
Whenever a user class executes a statement such as the following, a new Part object is created and 1 is added to NumParts:
Part p = new Part();
Hence, the value of NumParts will always be the number of Part objects created. Further, this is the only way to change its value; the writer of the class Part can guarantee that the value of NumParts will always be the number of objects created.
Of course, a user class may need to know the value of NumParts at any given time. Since it has no access to NumParts, we must provide a public accessor method (GetNumParts, say; we use uppercase G for a static accessor, since it provides a quick way to distinguish between static and non-static), which returns the value. Here is the method:
public static int GetNumParts() { return NumParts;
}
The method is declared static since it operates only on a static variable and does not need an object to be invoked. It can be called with Part.GetNumParts(). If p is a Part object, Java allows you to call it with p.GetNumParts(). However, this tends to imply that GetNumParts is an instance method (one that is called via an
object and operates on instance variables), so it could be misleading. We recommend that class (static) methods be called via the class name rather than via an object from the class.
As an exercise, add a field to the Book class to count the number of book objects created and update the constructors to increment this field.
2.4.1 An Improved Constructor
Instead of a no-arg constructor, we could take a more realistic approach and write a constructor that lets the user assign a name and price when an object is created, as in the following:
Part af = new Part("Air Filter", 8.75);
We could write the constructor as:
public Part(String n, double p) { name = n;
price = p;
NumParts++;
}
This will work except that a user can still set an invalid price for a part. There is nothing to stop the user from writing this statement:
Part af = new Part("Air Filter", 199.99);
The constructor will dutifully set price to the invalid value 199.99. However, we can do more in a constructor than merely assign values to variables. We can test a value and reject it, if necessary. We will take the view that if an invalid price is supplied, the object will still be created but a message will be printed and the price will be set to –1.0.
Here is the new version of the constructor:
public Part(String n, double p) { name = n;
if (p < 0.0 || p > 99.99) {
System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f. Set to -1.0.\n", p);
price = -1.0;
}
else price = p;
NumParts++;
} //end constructor Part
As a matter of good programming style, we should declare the price limits (0.00 and 99.99) and the “null” price (-1.0) as class constants. We could use the following:
private static final double MinPrice = 0.0;
private static final double MaxPrice = 99.99;
private static final double NullPrice = -1.0;
These identifiers can now be used in the constructor.
2.4.2 Accessor Methods
Since a user class may need to know the name or price of an item, we must provide public accessor methods for name and price. An accessor method simply returns the value in a particular field. By convention, we preface the name of these methods with the word get. The methods are as follows:
public String getName() { // accessor return name;
}
public double getPrice() { // accessor return price;
}
Note that the return type of an accessor is the same as the type of the field. For example, the return type of getName is String since name is of type String.
Since an accessor method returns the value in an instance field, it makes sense to call it only in relation to a specific object (since each object has its own instance fields). If p is an object of type Part, then p.getName() returns the value in the name field of p and p.getPrice() returns the value in the price field of p.
As an exercise, write accessor methods for all the fields of the Book class.
These accessors are examples of non-static or instance methods (the word static is not used in their declaration). We can think of each object as having its own copy of the instance methods in a class. In practice, though, the methods are merely available to an object. There will be one copy of a method, and the method will be bound to a specific object when the method is invoked on the object.
Assuming that a Part object p is stored at location 725, we can picture the object as shown in Figure 2-2.
name price 725
725
getName() getPrice() p
Figure 2-2. A Part object with its fields and accessors
Think of the fields name and price as locked inside a box, and the only way the outside world can see them is via the methods getName and getPrice.
2.4.3 Mutator Methods
As the writer of the class, we have to decide whether we will let a user change the name or price of an object after it has been created. It is reasonable to assume that the user may not want to change the name. However, prices change, so we should provide a method (or methods) for changing the price. As an example, we write a public mutator method (setPrice, say) that user classes can call, as in the following:
p.setPrice(24.95);
This sets the price of Part object p to 24.95. As before, the method will not allow an invalid price to be set. It will validate the supplied price and print an appropriate message, if necessary. Using the constants declared in Section 2.4.1, here is setPrice:
public void setPrice(double p) { if (p < MinPrice || p > MaxPrice) { System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f; Set to %3.2f\n", p, NullPrice);
price = NullPrice;
}
else price = p;
} //end setPrice
With this addition, we can think of Part p as shown in Figure 2-3.
name price 725
725 p
setPrice()
getName() getPrice()
Figure 2-3. Part object with setPrice() added
Observe the direction of the arrow for setPrice; a value is being sent from the outside world to the private field of the object.
Again, we emphasize the superiority of declaring a field private and providing mutator/accessor methods for it as opposed to declaring the field public and letting a user class access it directly.
We could also provide methods to increase or decrease the price by a given amount or by a given percentage.
These are left as exercises.
As another exercise, write mutator methods for the price and inStock fields of the Book class.