slanted W3C logo

Day 19 — Aggregation

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

Aggregation

aggregation
a group composed of many distinct parts

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).

Aggregation

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).

Aggregation Examples

Examples of aggregation:

Examples that are not aggregation:

Composition

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.

Composition Examples

UML Diagrams

A unified modeling language (UML) diagram illustrates aggregation relationships between classes:


The Aggregate's Constructor

When a client creates an object (the 'whole') who is responsible for creating the 'parts'? Either:

  1. the client, or
  2. the constructor (of 'whole')

Client Creates the Part

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);

Whole Creates the Part

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();

Whole Creates the Part 2

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.

Why Should the Client Care?

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);
   }
}

Aliasing

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.

Accessors

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.

Accessors

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.

Copying Objects: Aliasing

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
 

Copying Objects: Shallow Copy

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.

Copying Objects: Shallow Copy

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

Copying Objects: Deep Copy

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.

Copying Objects: Deep Copy

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