Given what we've seen so far, Java allows us to separate the interface from the implementation, but it doesn't enforce that separation. Programmers who make use of our classes can access data members (fields) and helper functions, even if we don't intend them to. This is an important point. It means that we can hope that folks stay away from our implementation, but we can't rely on it. Why does this matter? Well, for starters, it means reckless programmers can mess up our code — say by setting a data member (field) to an inappropriate value, for example setting a "distance" data member to a distance in miles when you were assuming the value was in kilometers. It also means that you cannot change your implementation without having to worry about breaking the code of folks who rely on your classes. After all, they may have been using elements of the implementation unbeknownst to you.
To truly realize the benefits of OOP (and, indeed, of separation of interface from implementation) we need to enforce this separation. The mechanism for this is access modifiers.
There are three basic modifiers: public, private, protected. Within the scope of the language as we know it, only public and private are meaningful. The access modifier (if present) is the very first thing in a declaration.
The golden rule: In most situations, the rule you want to follow is
simple: make the class itself and all member-functions
(methods) you intend outsiders to
use public, make all other member-functions
(methods) and all data-members (fields) private.
If you do this, you have a well-defined interface (the public
methods), and a well-defined implementation (everything else),
and their separation is enforced by the compiler and the JVM.
Data Hiding
is a big deal!
ctr is initialized to 10, so that every
instance that gets created has its ctr field
set to 10.
public class Countdown
{
private int ctr = 10;
public String next()
{
if (ctr < 0) return null;
if (ctr == 0) { ctr--; return "Blastoff!"; }
return "" + ctr--;
}
}
$ java Ex1
3
2
1
Blastoff!
|
Countdown(int start)
present, it's not possible to instantiate a Countdown like
this
Countdown C = new Countdown();anymore, since that would require a
Countdown()
constructor.
Note: a real advantage to constructors is we can handle errors or allow more fine-grained control over the state of the objects we create. For example, in the Countdown class we are in trouble if the user initializes with a value less than -1. We can address this though by having the constructor detect and correct for this.
Let's modify the Pos class from last lecture to
make proper use of data hiding. It's easy, we should declare
our data members to be private and, since we now know about
constructors, we should add a constructor as well.
|
The cool thing about this is that
we can completely change our implementation
of the Pos class, and as long as the prototypes (aka
signatures) of the original public methods stay the same,
we are guaranteed that any existing code that
relies on the Pos class will still work!
To the right is an example of a very different implementation. Row/col coordinates are stored in an array, and I play some shenanigans with that to allow me to support a new method, undo(), but I keep all the original method signatures the same. |
The cool thing is that you can check the "Track" class from last lecture, and it works exactly the same with the two different versions of Pos even though their implementations are so different. In fact, you can compile and run Track with the first Pos implementation, then switch to the second Pos implementation and recompile it without ever recompiling Track.java, and the program still works flawlessly. We can rely on that because we know that Track.java can only rely on the public methods of Pos ... it is literally forbidden from accessing anything else.
static" methods and fieldsUp to this point, we've made a big deal that all member fields in a class have separate copies in each instance of the class, so if we have two variables: Point one,two; then one.x is a different variable from two.x.
There is an exception to this. We can declare a member field as static. In that case there is a single variable that is shared between all instances.
class Point {
int x,y;
static int num;
}
In this case one.num is the same variable as two.num. If I change one.num, I have changed two.num as well.
This is handy for information that should be shared across all objects of the class. Imagine you wanted to establish a unique ID number to each object. Keeping a static field for the next free ID would be handy.
But, what is the value of num initially? We can't initialize it because we don't have an object yet in order to name it. The answer is to use the name of the class not the name of a variable instance of the class. We could make num 0 by saying Point.num=0; Note that this is only valid for static fields.
Stylistically, since we can always access num via the class name instead of a variable name, it has become preferred that we always do. This way it signals to the reader of our code that this is not a regular member field.
Another use for the static modifier on fields is in conjunction with final. Final means that the value of this thing cannot be changed, which makes it good for a constant:
class Tools {
final double SQRT2 = 1.41421356;
...}
but the problem with this is we couldn't access this thing without creating an object of type Tools:
Tools t = new Tools();
double d = t.SQRT2;
That seems wasteful, but if static fields exist even when no objects of that type exist, thaen we can declare it as:
class Tools {
final static double SQRT2 = 1.41421356;
...}
and access it directly using the class name:
double d = Tools.SQRT2;
We've seen the keyword static also applied to methods. A static method still is a member method of that class, but like static fields, it is not associated with any particular object. What this means is that inside this method, you cannot access any member fields that are not static. Consider adding the following to Point:
public static int foo() {
return x;
}
If foo were not static, we could happily do one.foo(), and it would access the x field of the object one. But, since this is static, we would access this function as Point.foo(). So which object's x do we access? It's not clear!
The compiler agrees with your confusion. For this reason, we would get a
compile time error: "non-static variable x cannot be referenced from a
static context."
However, there is no impediment to accessing other static fields or functions:
public static int foo() {
return num; // the field 'num' is declared static above
}
Static methods are used in 2 cases: