Subsection 9.1.1 Objects as instance variables
A class can declare instance variables whose type is another class. For example, a
Person class could have an instance variable of type
Address which has its own instance variables that hold a street, city, state, and zipcode. This is sometimes called
has-a relationship because a person
has an address.
The person class, with just its instance variables and a constructor might look like this:
class Person {
private String name;
private Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
And the
Address class that it uses might look like this:
class Address {
private String street;
private String city;
private String state;
private String zipcode;
public Address(String street, String city, String state, String zipcode) {
this.street = street;
this.city = city;
this.state = state;
this.zipcode = zipcode;
}
}
This is preferable to having the
Person class define its own
street,
city,
state, and
zipcode instance variables for a couple reasons. The main one is it makes
Person simplerβa
Person is just made up of two things, a name and an address. When trying to understand the
Person class we donβt have to deal with the complexity of how an address is represented. And then when we do look at the
Address class, we donβt have to think about the
Person class at all; we can understand the
Address class entirely on its own terms.
Also, people are not the only things that have addresses. If we want to represent other things that have addresses in our program, such as houses or schools, itβs better to have all of our code related to addresses live in one place rather than duplicating it in every class that represents something with an address.
Subsection 9.1.2 Passing objects as arguments
The
Person constructor above demonstrates one of the ways that objects can be connected, by passing one object to the constructor of another. Remember that every object is made up of two parts: its object data that lives somewhere in memory and a reference to that data which is the value that can be passed around and stored. So when we write code like this to construct an
Address:
Address addr = new Address("345 Cave Stone Road", "Bedrock", "CA", "12345");
the value stored in the variable
addr is a reference to the object data that was allocated to hold the data that makes up the
Address object. And when we write a line like this to create a
Person:
Person fred = new Person("Fred Flintstone", addr);
The value of
addr, i.e. the reference, is what is passed to the
Person contructor which then stores it in the
address instance variable in the
Person object data.
This way of passing arguments is known as
call by value and is used for both constructor and method calls in Java. Because the value of
addr is passed to the constructor, the code in the constructor cannot affect the variable
addr.
However because the value of
addr is a reference to the
Address object, if the
Address class defined methods that let us modify an
Address object, then code in the constructor or elsewhere in
Person could use those methods to change the
Address object and those changes would be visible to any code that had a reference to that
Address object.
Consider, for example, if given the previous two lines of code, we added this line:
Person wilma = new Person("Wilma Flintstone", addr);
Fred and Wilma live at the same address so it makes sense that they share an
Address object. Now suppose that
Address provided a
setCity method that changed the
city instance variable in an
Address. What happens to Fred and Wilma if we execute this line?
addr.setCity("Orbit City");
In this case, thereβs just one
Address object so the change would affect both
Person objects in that if you got their address and then got the city from that address it would now return
"Orbit City". Sometimes thatβs what we want and the whole point of passing around mutable objects. Other times it can be the source of subtle bugs if we forget or donβt realize that an object referenced by one object may be referenced elsewhere.
This is the same thing that happens with arrays as we discussed in
SubsectionΒ 6.1.5Β Array references as arguments. It doesnβt happen with primitive types because the value of primitive types
is the value that is copied and passed as an argument. And it doesnβt happen with reference types like
String whose values are immutable.
Activity 9.1.1.
Try the following code. Scroll down to see both the Person and the Address class definitions. The Person class has an Address object as an instance variable. Add code in the main method that changes the original address to your city. Does it also change in the person object? This can be a problem with references to mutable objects.
Subsection 9.1.4 Copying object arguments
The
Person class above has an
Address instance variable. As we saw with the example above of Fred and Wilma sharing an
Address, if the
Address object is mutable and we change it, it changes for both
Person objects since they both reference the same
Address object.
Sometimes that exactly what we want. If Fred and Wilma live together it makes sense that changing their
Address object changes it for both of them. But sometimes that can lead to surprising results where you think code is just changing the
Address for one
Person but is actually changing it for every
Person that shares the same
Address object.
If thatβs not what you want, sometimes it makes sense to copy the object passed in to the constructor and store the copy in the instance variable instead. How to make the copy will depend on the class of the object, but often you can just construct a new object of the appropriate class using values from the original object as shown below. This way the instance variable
addr does not hold a reference to the original object
initAddr, and the methods in the
Person class cannot modify the state of the original object.
public class Person {
private String name;
private Address addr; // Assumes an Address class is already defined
// constructor: initialize instance variable and call Address constructor to
// make a copy
public Person(String initName, Address initAddr) {
name = initName;
addr =
new Address(
initAddr.getStreet(),
initAddr.getCity(),
initAddr.getState(),
initAddr.getZipcode());
}
}
Another way to handle this is to provide a
copy constructor in the
Address class that takes an
Address object as a parameter and makes a copy of it. Then, the
Person constructor can call the
Address copy constructor.
class Address {
// Instance variables
private String street;
private String city;
private String state;
private String zipcode;
// Main constructor
public Address(String street, String city, String state, String zipcode) {
this.street = street;
this.city = city;
this.state = state;
this.zipcode = zipcode;
}
// Copy constructor
public Address(Address other) {
this(other.street, other.city, other.state, other.zipcode);
}
}
class Person {
// Instance variables
private String name;
private Address addr; // instance variable of type Address defined below
// Use Address's copy constructor to make a copy of the passed-in Address
public Person(String name, Address addr) {
this.name = name;
this.addr = new Address(addr);
}
}
Try the variation of the code below where the constructor copies the Address object so that it is separate from the original object. Add code in the main method that changes the original address to your city. It will not change the address in the Person object because it was copied.
Activity 9.1.4.
In the following
Person class, the constructor method copies the
Address object so that it is separate from the original object. Complete the
setAddress method in
Person so that it also makes a copy of
otherAddr object like the constructor. Then, add code in the
main method that changes the original address to your city. It should not change the copy! Add code that uses the
setAddress method. It should not change the original!
Subsection 9.1.5 Parameters of the same class
In general code in methods and constructors cannot access the
private data and methods of their parametersβthatβs what it means for something to be
private. However if a parameter is of the same type as the method or constructorβs enclosing class it can. We saw an example of that in the
Address copy constructor in the code above. For another example, in the following code, the
Person class accesses the
Adress instance variable of another
Person object because they are of the same class type, but it cannot access the
city instance variable of an
Address object directly because it is private and not of the same class.
public class Person {
private String name;
private Address addr; // instance variable of type Address defined below
public void copyAddress(Person otherPerson) {
// This code can directly access otherPerson.addr because otherPerson
// is an instance of Person, the class we are in.
addr = new Address(otherPerson.addr);
}
public void copyCity(Address otherAddr) {
// This code can't directly access the city instance variable
// on either Address object because and has to use the getter
// and setter provided by Address. (Assuming they exist.)
addr.setCity(otherAddr.getCity());
}
}
Try it in the code below.
Activity 9.1.5.
Run the code to see that the Person class can directly access the instance variables of objects of the same class, but cannot directly access the instance variable of an Address object because it is not of the same class. Change the code in the copyAddressFromAddress() method to use get/set methods instead.
Subsection 9.1.6 Returning objects
Methods can also return references to objects. Just like when we pass arguments
to a method, when we return values
from a method we return a copy of the value. For primitive types like
int or
double the value itself is copied, just like when we pass a primitive value as an argument. And just like when we pass an object as an argument to a method or constructor and a copy of the reference is passed, when we return an object from a method the value that is returned is also a copy of the reference.
For example, the
Person classβs
getAddress getter returns the
Address stored in the
addr instance variable which means it returns a reference to the same object referenced by
addr. And that means that if some code calls
getAddress and then mutates the returned
Address, it mutates it for the
Person that is still holding a reference to that same object.
This is another way that mutable objects can lead to surprising results. Even if
Person made a defensive copy of the
Address it was passed in its constructor, if it returns a reference to that object from any of its methods then code outside the
Person class can still mutate it. Sometimes thatβs exactly the point of having mutable objects but other times it can lead to subtle bugs with code in different classes interfering with each other.
Activity 9.1.6.
Run the code to see that the
Person class can return the
Address object which is mutable. Add code in the
main method that changes the zipcode of the
Address object returned by the
getAddress method. It will change the zipcode in the
Person object too.
Subsection 9.1.7 Chaining method calls
Programmers will sometimes call methods in a chain, one after another on the same line, for example
p.getAddress().getCity(). The method calls are separated by a dot and the return value of each method is used as the object for the next method call. It is important to know the types that each method returns so that you can call the methods of that object. For example, if the first method returns a String, the next method must be one that is defined for a String object. In the code below, the
getAddress method returns an Address object, and then the
getCity method is called on that Address object.
Person p1 = new Person("Skyler", new Address("123 Main St", "Anytown", "Anystate","12345"));
System.out.println( p1.getAddress().getCity() );
Activity 9.1.7.
Activity 9.1.8.
Activity 9.1.9.
Activity 9.1.10.
Print out the first two letters of the state in the address of the person below by chaining together the method calls in one line.