2 Javanotes 9.0, Section 5.1 — Objects, Instance Methods, and Instance Variables
Section 5.1
Objects, Instance Methods, and Instance Variables
Object-oriented programming (OOP) represents an
attempt to make programs more closely model the way people think about and deal
with the world. In the older styles of programming, a programmer who is faced
with some problem must identify a computing task that needs to be performed in
order to solve the problem. Programming then consists of finding a sequence of
instructions that will accomplish that task. But at the heart of
object-oriented programming, instead of tasks we find objects—entities that
have behaviors, that hold information, and that can interact with one another.
Programming consists of designing a set of objects that somehow model the
problem at hand. Software objects in the program can represent real or abstract
entities in the problem domain. This is supposed to make the design of the
program more natural and hence easier to get right and easier to
understand.
To some extent, OOP is just a change in point of view. We can think of an
object in standard programming terms as nothing more than a set of variables
together with some subroutines for manipulating those variables. In fact, it is
possible to use object-oriented techniques in any programming language.
However, there is a big difference between a language that makes OOP possible
and one that actively supports it. An object-oriented programming language such
as Java includes a number of features that make it very different from a
standard language. In order to make effective use of those features, you have
to “orient” your thinking correctly.
As I have mentioned before, in the context of object-oriented programming,
subroutines are often referred to as methods. Now that
we are starting to use objects, I will be using the term “method” more often
than “subroutine.”
5.1.1 Objects, Classes, and Instances
Objects are closely related to classes. We have already been working with
classes for several chapters, and we have seen that a class can contain
variables and methods (that is, subroutines). If an object is also a collection of variables and
methods, how do they differ from classes? And why does it require a
different type of thinking to understand and use them effectively? In the one
section where we worked with objects rather than classes,
Section 3.9, it didn’t seem to make much difference: We
just left the word “static” out of the subroutine definitions!
I have said that classes “describe” objects, or more exactly that the
non-static portions of classes describe objects. But it’s probably not very
clear what this means. The more usual terminology is to say that objects
belong to classes, but this might not be much
clearer. (There is a real shortage of English words to properly distinguish all
the concepts involved. An object certainly doesn’t “belong” to a class in the
same way that a member variable “belongs” to a class.) From the point of view
of programming, it is more exact to say that classes are used to create
objects. A class is a kind of factory—or blueprint—for constructing objects. The non-static
parts of the class specify, or describe, what variables and methods the
objects will contain. This is part of the explanation of how objects differ
from classes: Objects are created and destroyed as the program runs, and there
can be many objects with the same structure, if they are created using the same
class.
Consider a simple class whose job is to group together a few static member
variables. For example, the following class could be used to store information
about the person who is using the program:
class UserData {
static String name;
static int age;
}
In a program that uses this class, there is only one copy of each of the
variables UserData.name and UserData.age. When the class is
loaded into the computer, there is a section of memory devoted to the class, and
that section of memory includes space for the values of the variables name
and age. We can picture the class in memory as looking like this:
An important point is that the static member variables are part of the representation
of the class in memory.
Their full names, UserData.name and UserData.age,
use the name of the class, since they are part of the class.
When we use class UserData to represent the user of the program,
there can only be one user, since we only have memory space to store data about one user. Note that
the class, UserData, and the variables it contains exist as long as the
program runs. (That is essentially what it means to be “static.”)
Now, consider a similar class that includes some non-static variables:
class PlayerData {
static int playerCount;
String name;
int age;
}
I’ve also included a static variable in the PlayerData class.
Here, the static variable playerCount is stored as part of the representation of the class in memory.
Its full name is PlayerData.playerCount, and there is only one of it, which exists
as long as the program runs. However, the other two variables in the class definition are non-static.
There is no such variable as PlayerData.name or
PlayerData.age, since non-static variables do not become part of the
class itself. But the PlayerData class can
be used to create objects. There can be many objects created using the class, and each
one will have its own variables called name and age.
This is what it means for the non-static parts of the class to be a template for objects:
Every object gets its own copy of the non-static part of the class. We can visualize
the situation in the computer’s memory after several objects have been created like this:
Note that the static variable playerCount is part of the class, and there
is only one copy. On the other hand, every object contains a name and an
age. An object that is created from a class is called an instance of
that class, and as the picture shows, every object “knows” which class was used to create it.
I’ve shown class PlayerData as containing something called a
“constructor;” the constructor is a subroutine that creates objects.
Now there can
be many “players,” because we can make new objects to represent new players on
demand. A program might use the PlayerData class to store information about multiple
players in a game. Each player has a name and an age. When a player joins the
game, a new PlayerData object can be created to represent that player.
If a player leaves the game, the PlayerData object that represents
that player can be destroyed. A system of objects in the program is being used
to dynamically model what is happening in the game.
You can’t do this with static variables! “Dynamic” is the opposite of “static.”
An object that is created using a class is said to be an instance
of that class. We will sometimes say that the object belongs to the class.
The variables that the object contains
are called instance variables. The methods (that is, subroutines)
that the object contains are called instance methods.
For example, if the
PlayerData class, as defined above, is used to create an object, then
that object is an instance of the PlayerData class, and name
and age are instance variables in the object.
My examples here don’t include any methods, but methods work similarly to variables.
Static methods are part of the class; non-static, or instance, methods become part of
objects created from the class. It’s not literally true that each object contains
its own copy of the actual compiled code for an instance method. But logically an instance
method is part of the object, and I will continue to say that the object “contains”
the instance method.
Note that you should distinguish between
the source code for the class, and the class itself (in memory). The
source code determines both the class and the objects that are created from
that class. The “static” definitions in the source code specify the things
that are part of the class itself (in the computer’s memory), whereas the non-static definitions in the
source code specify things that will become part of every instance object
that is created from the class. By the way, static member
variables and static member subroutines in a class are sometimes called
class variables and class methods,
since they belong to the class itself, rather than to instances
of that class.
As you can see, the static and the non-static portions of a class are very
different things and serve very different purposes. Many classes contain only
static members, or only non-static, and we will see only a few examples of
classes that contain a mixture of the two.
5.1.2 Fundamentals of Objects
So far, I’ve been talking mostly in generalities, and I haven’t given you
much of an idea about what you have to put in a program if you want to work with objects.
Let’s look at a specific example to see how it works. Consider this extremely
simplified version of a Student class, which could be used to store
information about students taking a course:
public class Student {
public String name; // Student's name.
public double test1, test2, test3; // Grades on three tests.
public double getAverage() { // compute average test grade
return (test1 + test2 + test3) / 3;
}
} // end of class Student
None of the members of this class are declared to be static, so the
class exists only for creating objects. This class definition says that any
object that is an instance of the Student class will include instance
variables named name, test1, test2, and
test3, and it will include an instance method named
getAverage(). The names and test grades in different objects will generally
have different values. When called for a particular student, the method
getAverage() will compute an average using that
student’s test grades. Different students can have different averages.
(Again, this is what it means to say that an instance method belongs to an
individual object, not to the class.)
In Java, a class is a type, similar to the built-in types
such as int and boolean. So, a class name can be used to
specify the type of a variable in a declaration statement, or the type of a formal
parameter, or the return type of a function. For example, a program could
define a variable named std of type Student with the
statement
Student std;
However, declaring a variable does not create an object!
This is an important point, which is related to this Very Important Fact:
In Java, no variable can ever hold an object.
A variable can only hold a reference to an object.
You should think of objects as floating around independently in the
computer’s memory. In fact, there is a special portion of memory called the
heap where objects live. Instead of holding an
object itself, a variable holds the information necessary to find the object in
memory. This information is called a reference or
pointer to the object. In effect, a reference to
an object is the address of the memory location where the object is stored.
When you use a variable of object type, the computer uses the reference in the
variable to find the actual object.
In a program, objects are created using an operator called new, which
creates an object and returns a reference to that object. (In fact, the new operator
calls a special subroutine called a “constructor” in the class.)
For example, assuming that std is a variable of type Student, declared as above,
the assignment statement
std = new Student();
would create a new object which is an instance of the class
Student, and it would store a reference to that object in the variable
std. The value of the variable is a reference, or pointer, to the object.
The object itself is somewhere in the heap.
It is not quite true, then, to say that the object is the “value
of the variable std” (though sometimes it is hard to avoid using this
terminology). It is certainly not at all true to say that the
object is “stored in the variable std.” The proper terminology is that
“the variable std refers to or points to
the object,” and I will try to stick to that terminology as much as possible.
If I ever say something like “std is an object,” you should read it as
meaning “std is a variable that refers to an object.”
So, suppose that the variable std refers to an object that is an instance of
class Student. That object contains instance variables name,
test1, test2, and test3. These instance variables
can be referred to as std.name, std.test1,
std.test2, and std.test3. This follows the usual naming
convention that when B is part of A, then the full name of
B is A.B. For example, a program might include the lines
System.out.println("Hello, " + std.name + ". Your test grades are:");
System.out.println(std.test1);
System.out.println(std.test2);
System.out.println(std.test3);
This would output the name and test grades from the object to which
std refers. Similarly, std can be used to call the
getAverage() instance method in the object by saying
std.getAverage(). To print out the student’s average, you could
say:
System.out.println( "Your average is " + std.getAverage() );
More generally, you could use std.name any place where a variable
of type String is legal. You can use it in expressions. You can assign
a value to it. You can even use it to call subroutines from the String
class. For example, std.name.length() is the number of characters in
the student’s name.
It is possible for a variable like std, whose type is given by a
class, to refer to no object at all. We say in this case that std
holds a null pointer or null reference. The null pointer is
written in Java as: null. You can store a null reference in the
variable std by saying
std = null;
null is an actual value that is stored in the variable, not a pointer
to something else. It is not correct to say that the variable “points to null”;
in fact, the variable is null. For example,
you can test whether the value of std is null by testing
if (std == null) . . .
If the value of a variable is null, then it is, of course, illegal
to refer to instance variables or instance methods through that variable—since
there is no object, and hence no instance variables to refer to! For
example, if the value of the variable std is null, then it
would be illegal to refer to std.test1. If your program attempts to
use a null pointer illegally in this way, the result is an error called a
null pointer exception. When this happens while the program
is running, an exception of type NullPointerException
is thrown.
Let’s look at a sequence of statements that work with objects:
Student std, std1, // Declare four variables of
std2, std3; // type Student.
std = new Student(); // Create a new object belonging
// to the class Student, and
// store a reference to that
// object in the variable std.
std1 = new Student(); // Create a second Student object
// and store a reference to
// it in the variable std1.
std2 = std1; // Copy the reference value in std1
// into the variable std2.
std3 = null; // Store a null reference in the
// variable std3.
std.name = "John Smith"; // Set values of some instance variables.
std1.name = "Mary Jones";
// (Other instance variables have default
// initial values of zero.)
After the computer executes these statements, the situation in the
computer’s memory looks like this:
In this picture, when a variable
contains a reference to an object, the value of that variable is shown as an
arrow pointing to the object. Note, by the way, that the Strings
are objects! The variable std3, with a value of
null, doesn’t point anywhere. The arrows from std1 and
std2 both point to the same object. This illustrates a Very Important
Point:
When one object variable is assigned
to another, only a reference is copied.
The object referred to is not copied.
When the assignment “std2 = std1;” was executed, no new object was
created. Instead, std2 was set to refer to the very same object that
std1 refers to. This is to be expected, since the assignment
statement just copies the value that is stored in std1 into
std2, and that value is a pointer, not an object.
But this has some consequences that might be surprising.
For example, std1.name and std2.name are two different names for the
same variable, namely the instance variable in the object that both
std1 and std2 refer to. After the string “Mary
Jones” is assigned to the variable
std1.name, it is also
true that the value of
std2.name is “Mary Jones”.
There is a potential for a lot of confusion here, but you can help protect
yourself from it if you keep telling yourself, “The object is not in the
variable. The variable just holds a pointer to the object.”
You can test objects for equality and inequality using the operators == and
!=, but here again, the semantics are different from what you are used to. When
you make a test “if (std1 == std2)“, you are testing whether the
values stored in std1 and std2 are the same. But the values that you are comparing
are references to objects; they are not objects. So, you are testing whether
std1 and std2 refer to the same object. This is fine, if it’s what you want to do.
But sometimes, what you want to check is whether the instance variables in the
objects have the same values. To do that, you would need to ask whether
“std1.test1 == std2.test1 && std1.test2 == std2.test2 &&
std1.test3 == std2.test3 && std1.name.equals(std2.name)“.
I’ve remarked previously that Strings are objects, and I’ve shown
the strings “Mary Jones” and “John Smith” as objects in the
above illustration. (Strings are special objects, treated by Java in a special way, and I
haven’t attempted to show the actual internal structure of the String objects.)
Since strings are objects, a variable of type String can only hold a
reference to a string, not the string itself. This
explains why using the == operator to test strings for equality is not
a good idea. Suppose that greeting is a variable of type
String, and that it refers to the string “Hello”. Then
would the test greeting == “Hello” be true? Well, maybe, maybe not.
The variable greeting and the String literal “Hello”
each refer to a string that contains the characters H-e-l-l-o. But the strings
could still be different objects, that just happen to contain the same
characters; in that case, greeting == “Hello” would be false.
The function greeting.equals(“Hello”) tests whether
greeting and “Hello” contain the same characters, which is
almost certainly the question you want to ask. The expression
greeting == “Hello” tests whether greeting
and “Hello” contain the same characters stored in the same memory location.
(Of course, a String variable such as greeting
can also contain the special value null, and it would make
sense to use the == operator to test whether “greeting == null“.)
The fact that variables hold references to objects, not objects themselves,
has a couple of other consequences that you should be aware of. They follow
logically, if you just keep in mind the basic fact that the object is not
stored in the variable. The object is somewhere else; the variable points to it.
Suppose that a variable that refers to an object is declared to be
final. This means that the value stored in the variable can never be
changed, once the variable has been initialized. The value stored in the
variable is a reference to the object. So the variable will continue to refer
to the same object as long as the variable exists. However, this does not
prevent the data in the object from changing. The variable is
final, not the object. It’s perfectly legal to say
final Student stu = new Student();
stu.name = "John Doe"; // Change data in the object;
// The value stored in stu is not changed!
// It still refers to the same object.
Next, suppose that obj is a variable that refers to an object.
Let’s consider what happens when obj is passed as an actual parameter
to a subroutine. The value of obj is assigned to a formal parameter in
the subroutine, and the subroutine is executed. The subroutine has no power to
change the value stored in the variable, obj. It only has a copy of
that value. However, the value is a reference to an object. Since the
subroutine has a reference to the object, it can change the data stored in the
object. After the subroutine ends, obj still points to the same
object, but the data stored in the object might have changed.
Suppose x is a variable of type int and stu is a
variable of type Student. Compare:
void dontChange(int z) { void change(Student s) {
z = 42; s.name = "Fred";
} }
The lines: The lines:
x = 17; stu.name = "Jane";
dontChange(x); change(stu);
System.out.println(x); System.out.println(stu.name);
output the value 17. output the value "Fred".
The value of x is not The value of stu is not
changed by the subroutine, changed, but stu.name is changed.
which is equivalent to This is equivalent to
z = x; s = stu;
z = 42; s.name = "Fred";
5.1.3 Getters and Setters
When writing new classes, it’s a good idea to pay attention to the issue
of access control. Recall that making a member of a class public
makes it accessible from anywhere, including from other classes. On the
other hand, a private member can only be used in the class
where it is defined.
In the opinion of many programmers, almost all member variables should
be declared private. This gives you complete control over what
can be done with the variable. Even if the variable itself is private,
you can allow other classes to find out what its value is by providing
a public accessor method that returns the
value of the variable. For example, if your class contains a private
member variable, title, of type String, you
can provide a method
public String getTitle() {
return title;
}
that returns the value of title. By convention, the name of
an accessor method for a variable is obtained by capitalizing the name of variable and
adding “get” in front of the name. So, for the variable title, we get
an accessor method named “get” + “Title”, or getTitle().
Because of this naming convention, accessor methods are more often referred to
as getter methods. A getter method provides “read access” to
a variable. (Sometimes for boolean variables, “is” is used in place
of “get”. For example, a getter for a boolean member variable named
done might be called isDone().)
You might also want to allow “write access” to a private variable.
That is, you might want to make it possible for other classes to specify a new value
for the variable. This is done with a setter method. (If you don’t
like simple, Anglo-Saxon words, you can use the fancier term mutator method.)
The name of a setter method should consist of “set” followed by a capitalized copy of
the variable’s name, and it should have a parameter with the same type as the
variable. A setter method for the variable title could be written
public void setTitle( String newTitle ) {
title = newTitle;
}
It is actually very common to provide both a getter and a setter method for
a private member variable. Since this allows other classes both to see and to
change the value of the variable, you might wonder why not just make the
variable public? The reason is that getters and setters are not
restricted to simply reading and writing the variable’s value. In fact,
they can take any action at all. For example, a getter method might keep
track of the number of times that the variable has been accessed:
public String getTitle() {
titleAccessCount++; // Increment member variable titleAccessCount.
return title;
}
and a setter method might check that the value that is being
assigned to the variable is legal:
public void setTitle( String newTitle ) {
if ( newTitle == null ) // Don't allow null strings as titles!
title = "(Untitled)"; // Use an appropriate default value instead.
else
title = newTitle;
}
Even if you can’t think of any extra chores to do in a getter or setter
method, you might change your mind in the future when you redesign and
improve your class. If you’ve used a getter and setter from the beginning,
you can make the modification to your class without affecting any of the
classes that use your class. The private member variable
is not part of the public interface of your class; only the public
getter and setter methods are, and you are free to change their implementations
without changing the public interface of your class.
If you haven’t used get and set from
the beginning, you’ll have to contact everyone who uses your class and
tell them, “Sorry people, you’ll have to track down every use that you’ve
made of this variable and change your code to use my new get and set
methods instead.”
A couple of final notes: Some advanced aspects of Java rely on the
naming convention for getter and setter methods, so it’s a good idea to
follow the convention rigorously. And though I’ve been talking about using
getter and setter methods for a variable, you can define get and set
methods even if there is no variable. A getter and/or setter method defines
a property of the class, that might or might not correspond
to a variable. For example, if a class includes a public void
instance method with signature setValue(double), then the class
has a “property” named value of type double, and
it has this property whether or not the class has a member variable
named value.
5.1.4 Arrays and Objects
As I noted in Subsection 3.8.1, arrays are objects. Like Strings
they are special objects, with their own unique syntax. An array type such as
int[] or String[] is actually a class, and arrays
are created using a special version of the new operator.
As in the case for other object variables, an array variable can never
hold an actual array—only a reference to an array object. The array object
itself exists in the heap. It is possible for an array variable to hold the value
null, which means there is no actual array.
For example, suppose that list is a variable of type int[].
If the value of list is null, then any attempt to access
list.length or an array element list[i] would be an error
and would cause an exception of type NullPointerException. If
newlist is another variable of type int[], then the
assignment statement
newlist = list;
only copies the reference value in list into newlist.
If list is null, the result is that newlist will also be null.
If list points to an array, the assignment statement does not make a copy of the
array. It just sets newlist to refer to the same array as list. For
example, the output of the following code segment
list = new int[3]; list[1] = 17; newlist = list; // newlist points to the same array as list! newlist[1] = 42; System.out.println( list[1] );
would be 42, not 17, since list[1] and newlist[1] are just different names
for the same element in the array. All this is very natural, once you understand that
arrays are objects and array variables hold pointers to arrays.
This fact also comes into play when an array is passed as a parameter to a subroutine.
The value that is copied into the subroutine is a pointer to the array. The array is
not copied. Since the subroutine has a reference to the original array, any changes that
it makes to elements of the array are being made to the original and will persist after
the subroutine returns.
Arrays are objects. They can also hold objects.
The base type of an array can be a class. We have already seen this when we used
arrays of type String[], but any class can be used as the base type.
For example, suppose Student is the class defined earlier in
this section. Then we can have arrays of type Student[]. For an array
of type Student[], each element of the array is a variable of type
Student. To store information about 30 students,
we could use an array:
Student[] classlist; // Declare a variable of type Student[]. classlist = new Student[30]; // The variable now points to an array.
The array has 30 elements, classlist[0], classlist[1],
… classlist[29]. When the array is created, it is filled with the
default initial value, which for an object type is null. So, although
we have 30 array elements of type Student, we don’t yet have
any actual Student objects! All we have is 30 nulls.
If we want student objects, we have to create them:
Student[] classlist;
classlist = new Student[30];
for ( int i = 0; i < 30; i++ ) {
classlist[i] = new Student();
}
Once we have done this, each classlist[i] points to an object of type
Student. If we want to talk about the name of student number 3,
we can use classlist[3].name. The average for student number i
can be computed by calling classlist[i].getAverage(). You can do anything
with classlist[i] that you could do with any other variable of type
Student.