In today's lecture we look at aggregation and how it affects the use and copying of objects.
The material covered reflects Chapter 8 of the 3rd Edition of the textbook (there are significant differences between the 2nd and 3rd Editions in Section 8.2).
Call number of textbook on reserve in Steacie: PCOP.2629STEACIE
In Java, a class defines an aggregation if one or more of its attributes is an object reference.
A class is not an aggregation if all of its
attributes have primitive type (or
String
according to the textbook).
For example, the Investment
class (in type.lib
) encapsulates an investment
in a Stock
:
Stock stock = new Stock("HR.A"); // purchase 100 shares of stock at $1.10 per share Investment inv = new Investment(stock, 100, 1.10);
Every Investment
object has-a
reference to a Stock
object. The
relationship has-a indicates an aggregation.
Another way to think about aggregation is that it is
a whole-part relationship. Every Investment
object (the whole) is made up of a Stock
object (the part).
Examples of aggregation:
Examples that are not aggregation:
Composition is a stronger form of aggregation. In a composition, a class (the 'whole') that has-an object reference (the 'part') also owns that object reference.
The standard definition of composition is that the lifetime of the 'part' is controlled by the 'whole'. This means that when the 'whole' dies, the 'parts' also die.
In garbage collected languages like Java, the lifetime of an object is not so easily determined. The book offers a rule of thumb that we will try to be consistent with:
If creating an object (the 'whole') causes other objects to be created (the 'parts') then the relationship is a composition.
A unified modeling language (UML) diagram illustrates aggregation relationships between classes:
Investment
has 1 Stock
reference (aggregation)CreditCard
has 2 Date
references (composition)Portfolio
has a variable number of Investment
references (aggregation)When a client creates an object (the 'whole') who is responsible for creating the 'parts'? Either:
In the Investment
class, the client is responsible for
supplying a Stock
object that is aggregated by
the Investment
object:
// a Stock is born Stock s = new Stock("HR.A"); // an Investment is born Investment inv = new Investment(s, 100, 1.10);
Notice that both the Stock
object and
Investment
object have separate lifetimes.
The Investment
can even be garbage
collected with affecting the Stock
.
Stock s = new Stock("HR.A"); { Investment inv = new Investment(s, 100, 1.10); } output.println(s);
In the CreditCard
class, the client is not always
responsible for supplying the various Date
objects that are aggregated by the CreditCard
object.
Instead, the CreditCard
constructor creates
the Date
objects internally:
// client does not specify issue and expiry dates CreditCard visa = new CreditCard(123456, "John Doe"); Date issued = visa.getIssueDate(); Date expires = visa.getExpiryDate();
A composition can be constructed even if the client supplies a 'part'. In such cases, the 'whole' makes a new copy of the 'part'.
final int NUMBER = 123456; String name = "John Doe"; final double LIMIT = 5000.0; final Date ISSUED = new Date(); CreditCard visa = new CreditCard(NUMBER, name, LIMIT, ISSUED); output.println("same Date value: " + ISSUED.equals(visa.getIssueDate()); output.println("same Date object: " + ISSUED == visa.getIssueDate());
The code fragment prints:
true false
which indicates that the CreditCard
has
created its own Date
instance to hold
the issue date.
The client is not supposed to care how the implementer has programmed the class; why should the client care about aggregation and composition? Because the implementer's choice affects the client!
Stock s = new Stock("HR.A"); // purchase 10 shares at $2 per share Investment inv = new Investment(s, 10, 2.00); // mutate stock using s s.setSymbol("HR.Z"); // what does inv return? Stock sinv = inv.getStock(); output.println(sinv == s);
The above code fragment prints true
indicating
that sinv
is a reference
to a stock with symbol HR.Z
.
import java.io.PrintStream; import type.lib.Investment; import type.lib.Stock; public class Aggregation1 { public static void main(String[] args) { PrintStream output = System.out; Stock s = new Stock("HR.A"); // purchase 10 shares at $2 per share Investment inv = new Investment(s, 10, 2.00); // mutate stock using s s.setSymbol("HR.Z"); // what does inv return? Stock sinv = inv.getStock(); output.println(sinv == s); } }
In the previous example, the client was able to change the
stock in the investment object without using a method
in the Investment
class. In fact, the reference
s
and the stock reference held by the investment
both refer to the same object.
main | ||
s ⇒ | 300 | |
inv ⇒ | 400 | |
| | ||
300 | Stock object | |
| | ||
400 | Investment object | |
stock ⇒ | 300 |
When two different references refer to the same object
(in this case the references are s
and
stock
) we say that the references are aliases.
The Investment
class defines an accessor
method that returns a reference to its Stock
attribute. From the previous discussion, we already know
that the returned reference points to the aggregated
stock object; however, the implementer could have chosen
to return a reference to a copy of the aggregated stock object.
The difference between the two approaches is important. If a reference to the aggregated object is returned then the client can modify the aggregated object using the reference:
String origSymbol = "HR.A"; Investment inv = new Investment(new Stock(origSymbol), 10, 2.00); Stock sinv = inv.getStock(); String newSymbol = "HR.M"; sinv.setSymbol(newSymbol); Stock sinv2 = inv.getStock(); output.println(newSymbol.equals(sinv2.getSymbol()));
The above code fragment prints true
indicating
that getStock
returns a reference to the
aggregated stock object.
If an accessor returns a reference to a copy of an aggregated object, then the client will not be able to modify the aggregated object using the reference:
CreditCard card = new CreditCard(123456, "John Doe");
Date issueDate = card.getIssueDate();
issueDate.setTime(0); // Jan 1, 1970 GMT
output.println(card.getIssueDate());
The above code fragment prints today's date. This means that the accessor returned a reference to a copy of the aggregated date.
Sometimes a client will want to create a copy of an object. The cleanest way to copy an object is to use what is called a copy constructor, but many existing classes do not provide them. In these cases, the client has to do some extra work.
The simplest form of copying an object is to create an alias:
Investment inv = new Investment(new Stock("HR.A"), 10, 2.00); Investment alias = inv;
inv ⇒ | 400 | |
alias ⇒ | 400 | |
| | ||
400 | Investment object | |
stock ⇒ | 500 | |
| | ||
500 | Stock object | |
A shallow copy of an object creates a new object with attributes that are identical to the original object. If the original object is an aggregation, then the copied object aggregates the same objects as the original.
Investment inv = new Investment(new Stock("HR.A"), 10, 2.00); Investment shallow = new Investment(inv.getStock(), inv.getQty(), inv.getBookValue());
How can the client show that shallow
is (probably) a shallow copy of inv
?
output.println(inv.getStock() == shallow.getStock());
The above code fragment prints true
indicating
that stock aggregated by inv
is the same object
as the stock aggregated by shallow
.
Note that if getStock
returns a copy of a stock
then there is no way for the client to make a shallow copy
of inv
in this example.
The memory diagram for the previous example is:
inv ⇒ | 400 | |
shallow ⇒ | 600 | |
| | ||
400 | Investment object | |
stock ⇒ | 500 | |
| | ||
500 | Stock object | |
| | ||
| | ||
600 | Investment object | |
stock ⇒ | 500 |
A deep copy of an object creates a new object with attributes that are copies of those of the original object. If the original object is an aggregation, then the copied object aggregates different objects as the original.
Investment inv = new Investment(new Stock("HR.A"), 10, 2.00); Stock s = new Stock(inv.getStock().getSymbol()); Investment deep = new Investment(s, inv.getQty(), inv.getBookValue());
How can the client show that deep
is (probably) a deep copy of inv
?
output.println(inv.getStock() == deep.getStock());
The above code fragment prints false
indicating
that stock aggregated by inv
is a different object
from the stock aggregated by deep
.
The memory diagram for the previous example is:
inv ⇒ | 400 | |
deep ⇒ | 600 | |
| | ||
400 | Investment object | |
stock ⇒ | 500 | |
| | ||
500 | Stock object | |
| | ||
| | ||
600 | Investment object | |
stock ⇒ | 700 | |
| | ||
700 | Stock object | |