When weβre dealing with objects, the actual value that is stored in a variable, passed as an argumment, or returned from a method is a reference to the objectβs data. This means itβs possible to connect objects to each other in all kinds of ways. Weβll look at some of the most important in this section.
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.
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 even 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 in our program we want to represent houses or schools or other things that have addresses 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.
In Java, the source code for each class is saved in its own file named after the class, so the Person class would be in Person.java and the Address class would be in Address.java. If the files are in the same folder, they can be compiled together so that they can interact. In Runestone, we cannot have separate files, so we put them in 1 coding area. In Runestone, only the class that has the main method can be public; the other classes can leave out the public keyword.
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 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.
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?
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 String values because even though String is a reference type, it is immutable.
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.
The this variable can be used anywhere you would use an object variable. You can even pass it to another method as an argument. Consider the classes below, Pay and Overtime. The Pay class declares an Overtime object and passes in this (the current Pay object) to its constructor which computes the overtime with respect to that Pay object. Try this code in the active code exercise below with the Show CodeLens button to trace through it step by step. Here is an image that shows how this, myPay and p all refer to the same object in memory.
What does this code print out? Trace through the code with the Show CodeLens button. Notice how the this Pay object is passed to the Overtime constructor.
public class Pay {
private double pay;
public Pay(double p) {
pay = p;
}
public double getPay() {
return pay;
}
public void calculatePayWithOvertime() {
// this Pay object is passed to the Overtime constructor
Overtime ot = new Overtime(this);
pay = ot.getOvertimePay();
}
}
public class Overtime {
private double payWithOvertime;
public Overtime(Pay p) {
payWithOvertime = p.getPay() * 1.5;
}
public double getOvertimePay() {
return payWithOvertime;
}
}
The following code segment appears in a class other than Pay or Overtime.
New objects are saved in variables of a reference type which holds a reference to an object. A reference is a way to find the object in memory. It is like a tracking number that you can use to track the location of a package in the mail.
A special reference value null (which means none) can be used when a variable doesnβt refer to any object. For instance, you can declare a variable and initialize it to null (Turtle t1 = null;) meaning the variable doesnβt refer to any object yet.
The code Turtle t1 = null; creates a variable t1 that refers to a Turtle object, but the null means that it doesnβt refer to an object yet. You could later create the object and set the object variable to refer to that new object (t1 = new Turtle(world1)). Or more commonly, you can declare an object variable and initialize it in the same line of code (Turtle t2 = new Turtle(world1);).
Subsection9.1.5Arguments, Parameters, and Call by Value
When a constructor like Date(2005,9,1) is called, the parameters, (year, month, and day), are set to copies of the arguments, (2005, 9, and 1). This is call by value which means that copies of the argument values are passed to the constructor. These values are used to initialize the objectβs attributes. A constructor call interrupts the sequential execution of statements in that the program executes the statements in the constructor before continuing. Once the last statement in the constructor has been executed, the flow of control is returned to the point immediately following where the constructor was called.
The type of the values being passed in as arguments have to match the type of the parameter variables. We cannot give a constructor a String object when it is expecting an int. The order of the arguments also matters. If you mix up the month and the day in the Date constructor, you will get a completely different date, for example January 9th (1/9) instead of Sept. 1st (9/1).
This lesson introduces a lot of vocabulary, but donβt worry if you donβt understand everything about classes and constructors yet. You will learn more about how this all works in later units when you write your own classes and constructors. And you will see parameters again with methods in the next lessons.
The Person class above has an Address object as an instance variable. We found that changing the Address object outside the class also changed the address in the Person object. This is because the Address object is mutable, and the reference to the object is passed in as an argument to the constructor.
When you pass object references as parameters to constructors, those references refer to the same objects as the references in the caller. If the objects are immutable, like String objects it doesnβt matter at all. On the other hand, if the objects are mutable, meaning their instance variables can change after they are constructed, then storing the passed-in reference in an instance variable in your object can lead to surprising results: if some other code changes the object it will change for you too. If thatβs not what you want, sometimes it makes sense to copy the object passed 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;
// constructor
public Address(String initStreet, String initCity, String initState, String initZipcode) {
street = initStreet;
city = initCity;
state = initState;
zipcode = initZipcode;
}
// copy constructor
public Address(Address otherAddr) {
street = otherAddr.getStreet();
city = otherAddr.getCity();
state = otherAddr.getState();
zipcode = otherAddr.getZipcode();
}
}
class Person {
// instance variables
private String name;
private Address addr; // instance variable of type Address defined below
// constructor: initialize instance variable and call Address constructor to
// make a copy
public Person(String initName, Address initAddr) {
name = initName;
// call the copy constructor of Address to make a defensive copy
addr = new Address(initAddr);
}
}
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.
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!
Methods cannot access the private data and methods of a parameter that holds a reference to an object unless the parameter is the same type as the methodβs enclosing class. In the following code, the Person class can access 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) {
// otherPerson.addr will work because the parameter is of the same class Person
addr = new Address(otherPerson.addr);
}
public void copyCity(Address otherAddr) {
// This will not work because the city instance variable is private
// and the parameter is not of the same class type
// addr.city = otherAddr.city;
// But you can use the get method to access the city
addr.setCity(otherAddr.getCity());
}
}
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.
Methods can also return objects. Remember that methods can only return the value of a variable. If the return value is of a primitive type like int or double, only a copy of the value is returned; the original variable cannot be changed. But when the return expression evaluates to an object reference, the reference is returned, not a reference to a new copy of the object. This means that there can be multiple references to the same object. If the object is mutable, then any changes made to the object through one reference will be seen through any other references to the object.
For example, the Person class can have a getAddress method that returns the Address instance variable which is an object. Since Address has set methods which make it mutable, the caller can change the Address object through the reference returned by the method.
public class Person {
// instance variables
private String name;
private Address addr; // instance variable of type Address
public Address getAddress() {
return addr;
}
public static void main(String[] args) {
Person p1 =
new Person("Skyler", new Address("123 Main St", "Anytown", "Anystate", "12345"));
System.out.println(p1);
Address a = p1.getAddress();
a.setCity("Othertown");
System.out.println(p1);
}
}
Notice that we created the new Address object above inside the call to the Person constructor, instead of saving it into a variable first: Person p1 = new Person("Skyler", new Address("123 Main St", "Anytown","Anystate","12345")). This is called an anonymous object where we create a new object without associating it with a variable. This is useful when you only need the object for a short time. This new Address object will be copied into the instance variable addr in the Person constructor.
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.
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.
Create a class called Date with the following instance variables as ints: month, day, and year. Add a constructor, getters, and setters for the instance variables.
Test your methods in the main method by creating a Friend object with a friendβs name and birthday. Then, create a Date object with todayβs date and call the isBirthday method to see if it is the friendβs birthday.
Complete the Date and Friend classes below. The Friend class should have a method isBirthday that returns true if the birthdate matches the given date parameter. Test your methods in the main method.
Many people keep their money in a bank account. The bank may keep track of the account holderβs name, the acount balance which is the amount of money in the account, and assign an account number to each account. At the bank or an ATM (automatic teller machine) or on a phone app, the account holder can deposit (add) or withdraw (subtract) an amount from their account. Hereβs a video that shows the steps to use an ATM to withdraw money from a bank acount. Phone apps like Venmo and Paypal connect to your bank account or credit card to send and get money from businesses or friends.
Create a class called BankAccount below that keeps track of the account holderβs name, the account number, and the balance in the account. Make sure you use the appropriate data types for these.
Write 2 constructors for the class: one that initializes all the instance variables and one that only has 2 parameters for the name and account number and initializes the balance to 0. For the parameters, use the same variable names as your instance variables. Use the this keyword to distinguish between the instance variables and the parameter variables.
Write withdraw(amount) and deposit(amount) methods for the class. The withdraw method should subtract the amount from the balance as long as there is enough money in the account (the balance is larger than the amount). And deposit should add the amount to the balance. Use the this keyword to refer to the balance.
Test your class below with a main method that creates a BankAccount object and calls its deposit and withdraw methods and prints out the object to test its toString method.
Create a class called BankAccount that keeps track of the account holderβs name, the account number, and the balance in the account. Create 2 constructors using this (one constructor to initialize all 3 instance variables and one that only has 2 parameters for the name and account number and initializes the balance to 0), a toString() method, and withdraw(amount) and deposit(amount) methods. Test your class in a main method.
(AP 3.4.A.3) When a mutable object is a constructor parameter, the instance variable should be initialized with a copy of the referenced object. In this way, the instance variable does not hold a reference to the original object, and methods are prevented from modifying the state of the original object.
(AP 3.6.A.1) When an argument is an object reference, the parameter is initialized with a copy of that reference; it does not create a new independent copy of the object. If the parameter refers to a mutable object, the method or constructor can use this reference to alter the state of the object. It is good programming practice to not modify mutable objects that are passed as parameters unless required in the specification.
(AP 3.6.A.3) Methods cannot access the private data and methods of a parameter that holds a reference to an object unless the parameter is the same type as the methodβs enclosing class.
(AP 3.9.A.1) Within an instance method or a constructor, the keyword this acts as a special variable that holds a reference to the current objectβthe object whose method or constructor is being called.
this.instanceVariable can be used to distinguish between this objectβs instance variables and local parameter variables that may have the same variable names.
public class Liquid {
private int currentTemp;
public Liquid(int ct) {
currentTemp = ct;
}
public int getCurrentTemp() {
return currentTemp;
}
public void addToJar(LiquidJar j) {
j.addLiquid(this);
}
}
public class LiquidJar {
private int totalTemp;
public LiquidJar() {
totalTemp = 0;
}
public void addLiquid(Liquid l) {
totalTemp += l.getCurrentTemp();
}
public int getTotalTemp() {
return totalTemp;
}
// Constructor not shown.
}
Consider the following code segment, which appears in a class other than Liquid or LiquidJar.
Liquid water = new Liquid(50);
Liquid milk = new Liquid(15);
LiquidJar j = new LiquidJar();
water.addToJar(j);
milk.addToJar(j);
System.out.println(j.getTotalTemp());
What, if anything, is printed out after the execution of the code segment?