Oop
Relationships & Object Behaviour
How objects hold on to each other, and what happens when you copy them. Association links objects that live their own lives; aggregation groups parts it did not create and may share; composition creates and owns its parts, which only works if no reference to a part escapes. Shallow copies copy references, deep copies copy what they point to, and a copy constructor beats clone(). Every example runs in Java and Python, with verified output.
Suggest an editRelationships & Object Behaviour — Who Owns Whom
Close a customer's account, and their saved addresses should go with it. Close a department, and its employees should certainly not disappear. Both are "one object holds others", yet they need opposite behaviour. Which one you mean decides who creates the held objects, who may change them, whether anyone else can hold them, and what a copy of the holder should contain.
Object-oriented design names three kinds of link between classes. Association is any link: one object holds a reference to another and uses it. Aggregation is a whole–part association where the parts have their own life: the whole groups them but does not own them. Composition is a whole–part association where the whole owns its parts: it creates them, and they end when it ends. These are the lines and diamonds of a UML class diagram, and the reason composition is often preferred over inheritance. The second half of the lesson covers copying an object, which is where those ownership decisions turn into bugs or into correct code.
💡 The core idea.
- A field that holds an object holds a reference to it. Two objects that hold the same reference share one object, and a change made through either is seen by both.
- Association and aggregation share on purpose: the held objects were created elsewhere and live on without the holder. Composition must not share: the whole creates its parts and never lets a reference to them escape.
- A shallow copy copies the references, so the copy and the original share everything they point to. A deep copy also copies the objects those references point to, as far down as you need.
This builds on Abstraction & Interfaces. Copying only makes sense once you know that a variable holds a reference to an object, not the object itself; if that is new, read the Java guide's References, Equality & the Object Model first. Every output below was produced by running the code on Java 21 and Python 3.11.
You'll be able to: tell association, aggregation and composition apart by asking who creates the held object and whether it outlives the holder; decide whether a class should keep the collection it is given or take its own copy; keep a composite's parts from escaping through a getter; predict what a shallow copy shares, in Java's clone() and Python's copy.copy; write a deep copy with a copy constructor, and explain why clone() is the weaker tool.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — which references exist, and who can reach the object through them.
- A concrete bite — a specific, runnable program where a shared reference produces a surprise.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Association: objects that know each other
- Aggregation: a whole that groups parts it did not create
- Composition: a whole that owns its parts
- Shallow copy: copying the references
- Deep copy: copying what the references point to
- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. Association: objects that know each other
An association exists when objects of one class hold references to objects of another and use them. Neither object owns the other: each was created on its own, and each can outlive the link. Associations are described by how many objects sit at each end, called their multiplicity:
- One-to-one. One object is linked to exactly one other. A
Personholds onePassport. - One-to-many. One object is linked to many. A
Teacherteaches manyStudents. - Many-to-many. Many objects on each side. A
Coursehas manyStudents, and aStudenttakes manyCourses.
In code, an association is a field: a single reference for "one", and a collection of references for "many".
Output:
Alex holds passport P-4417
Mrs. Rao teaches Sam
Mrs. Rao teaches Riya
Maths: Sam, Riya
Physics: SamAnalysis. Every link here is a field holding a reference to an object that was created somewhere else. Sam is one object, referred to by two courses and by the teacher's list. Nothing in Course or Teacher creates a Student, and dropping a course would not affect any student. That independence is what makes these links associations rather than ownership.
Intuition.
Mechanism. A field that holds an object holds a reference to it, never a private copy. When a constructor stores a parameter in a field, the new object and the caller both hold references to the same object. For a single immutable object like a Passport with a final number, that sharing is harmless. For a mutable collection, it means the caller can still change what your object sees.
Concrete bite. Teacher stores the list it is given. If the caller reuses that list afterwards, for example to build next term's group, the teacher silently starts teaching the new students too:
Output:
sharing teacher teaches: Sam, Riya, Jo, Kim
copying teacher teaches: Sam, RiyaNobody called a method on rao, yet its class doubled. A copy made when the object is constructed is called a defensive copy: List.copyOf in Java and tuple(...) in Python give the teacher a collection that the caller cannot reach. Both copy only the list. The Student objects inside are still shared, which section 4 explains.
💡 Earned rule. When a constructor or setter receives a mutable collection, decide on purpose whether your object should see the caller's later changes. Usually it should not, so store a copy (List.copyOf, tuple(...)). Keep the caller's collection only when sharing is the point, and say so in the documentation.
The cost is one copy per call, which takes time and memory in proportion to the collection's size. And the caller can no longer change your object through its own list; it has to call one of your methods, which is usually what you want.
2. Aggregation: a whole that groups parts it did not create
Aggregation is a whole–part association in which the parts have their own life. The whole groups them, but it did not create them, and it does not end them. A Department groups Employees: the employees were hired by the company, they exist before the department is formed, and they still exist after it is dissolved. Because the department doesn't own them, nothing stops an employee from being in two departments at once.
Output:
Engineering: Alex (Lead), Sam (Engineer)
Security guild: Alex (Lead)
Engineering dissolved. Staff: Alex (Lead), Sam (Engineer)Analysis. One promote call changed what both departments print, because both hold a reference to the same Alex object. Setting engineering to null (or None) removed the only reference to the department, but the employees are still reachable through staff, so they live on. Neither Java nor Python has a statement that destroys an object: an object stays alive for as long as something can reach it, and the garbage collector reclaims it only after nothing can.
Intuition. Mechanism. In aggregation the parts are created outside the whole and passed in, so other objects can hold references to them too. The whole is one of possibly several holders, and the part's life is decided by whoever else still refers to it. UML deliberately leaves the precise meaning of this "shared" aggregation to the modeller and the application area [1], so treat the hollow diamond as a statement of intent: "groups, does not own".
Concrete bite. Alex is in Engineering and in the Security guild at once, and promoting Alex once changes both listings. That sharing is exactly what aggregation allows and composition forbids. If the departments had each made their own Employee for Alex, the promotion would have shown up in only one of them, and the company would have two disagreeing records of one person.
💡 Earned rule. If a part has its own identity and life (an employee, a book in a library, a player in a team), let the whole receive it rather than create it, and expect it to be shared and changed by others. Keep the part's own rules inside the part (here, promote), because the whole cannot police changes made through other references.
The cost is that the whole cannot guarantee anything about its parts' state: any holder of a reference can change them. When the whole needs that guarantee, you want composition instead.
3. Composition: a whole that owns its parts
Composition is a whole–part relationship in which the whole owns its parts. UML calls it composite aggregation: a part belongs to at most one composite at a time, and when the composite is deleted, its parts are deleted with it [1]. A House and its Rooms are the classic example: the house creates its rooms, a room is in only one house, and a room without its house makes no sense.
In a garbage-collected language that last guarantee has a catch. Nothing deletes the rooms when the house goes; they disappear only when nothing refers to them any more. Here is a House that hands out its own list:
Output:
rooms: 3
house dropped, still holding: Living RoomThe house now has a room it never created, and a room has outlived its house. Both happened because the getter returned a reference to the house's own list. The fix keeps every change inside the House: rooms are added only through a method that creates the Room and checks the house's rule, and the getter returns a read-only view.
Output (Java):
add through getRooms() refused: UnsupportedOperationException
addRoom refused: a house has at most 4 rooms
rooms: 4Output (Python):
add through rooms refused: AttributeError
add_room refused: a house has at most 4 rooms
rooms: 4Analysis. The outside add now fails, and the only way to add a room is through addRoom, which enforces the four-room rule. Java's Collections.unmodifiableList returns a view: reads go through to the house's real list, so the view shows rooms added later, and every attempt to modify it throws UnsupportedOperationException [6]. Python's tuple(...) is a snapshot: a tuple has no append, so the attempt raises AttributeError, and a tuple fetched before add_room("Study") would not show the study.
Intuition. Mechanism. Composition is a promise about references: the whole creates its parts, and no reference to a part, or to the collection holding them, is handed out in a form that lets someone else change or keep it. Keep that promise and the part really does live and die with the whole, because the whole holds the only reference. Break it, as the first program did, and the part becomes shared, which is aggregation whatever the class diagram says.
Concrete bite. In the first program, kept still printed Living Room after the house was dropped. In a garbage-collected language, "the part dies with the whole" is only true if no reference to the part escapes. The fixed version still lets a Room reference escape through the view, and that is safe only because Room is immutable: a caller can hold it, but cannot change the house through it. If Room were mutable (say, with a setArea), the getter would have to return copies, or only the data the caller needs, such as the room names.
💡 Earned rule. For a composite, create the parts inside the whole, add them only through the whole's own methods, and never return the internal collection. Return a read-only view (Collections.unmodifiableList), a copy (List.copyOf, tuple(...)), or only the data callers need. Make the parts immutable where you can, so a reference that does escape is harmless.
The cost is more methods on the whole (addRoom, removeRoom, and so on) instead of one getter, and a copy per call if you return copies. That is the price of being able to enforce a rule like "at most four rooms".
One class, several relationships
A real class usually takes part in several kinds of relationship at once. A Library aggregates Books: a book exists before it is catalogued and can be moved to another library. A Book is composed of Chapters: a chapter is created as part of one book and makes no sense without it. The same Book class is a part in one relationship and a whole in another, and its code should follow both decisions: receive books in Library.add(Book), but create chapters inside Book.
Comparing the three
| Aspect | Association | Aggregation | Composition |
|---|---|---|---|
| Meaning | one object uses another | a whole groups parts | a whole owns its parts |
| Who creates the held object | someone else | someone else | the whole |
| Can the held object be shared? | yes | yes | no: at most one whole at a time |
| Does it outlive the holder? | yes | yes | no, provided no reference escapes |
| UML notation | plain line or arrow | line with a hollow diamond at the whole | line with a filled diamond at the whole |
| Example | Teacher and Student | Department and Employee | House and Room, Car and Engine |
In a UML class diagram the three look like this; the UML lesson covers the notation in full.
Your Turn — Practice: Composition
A University owns its Colleges: it creates each one inside addCollege, so no caller ever holds a College that the university doesn't know about.
4. Shallow copy: copying the references
Object cloning means creating a new object with the same state as an existing one. The new object is a separate object, at a different place in memory, so changing one of its fields does not change the original's. Whether changing an object that a field refers to affects the original depends on how deep the copy went.
There are a few good reasons to copy an object:
- Independence. You need a version you can change without affecting the original, as the defensive copy in section 1 did.
- Preserving state. You keep a snapshot of an object's current state, to restore later for undo, or to compare against.
- Prototyping. You make new objects by copying a configured example, which is the Prototype pattern in Creational Design Patterns.
To get a modifiable version of an unmodifiable collection, you don't clone it; you copy its elements into a new mutable collection, such as new ArrayList<>(list) in Java or list(t) in Python.
How Java's clone() works
Java's built-in copying has two parts.
Cloneableis a marker interface: it has no methods. Implementing it tellsObject.clone()that it is legal to make a field-for-field copy of objects of this class. If a class doesn't implement it,Object.clone()throwsCloneNotSupportedExceptioninstead [3].Object.clone()creates a new object of the same class and sets each of its fields to the same value as the original's, as if by assignment. A primitive field gets the same number. A reference field gets the same reference, so the copy refers to the same object as the original. The Javadoc calls this a "shallow copy" of the object, not a "deep copy" [2]. It does this without running any constructor.
A shallow copy is a new object whose fields hold the same values as the original's. For a reference field, that means the same reference: the copy and the original point at one shared object.
Output (Java):
same Person object? false
same Address object? true
Rahul lives in New Delhi
Rahul lives in New Delhi
Ticket: CloneNotSupportedExceptionOutput (Python):
same Person object? False
same Address object? True
Rahul lives in New Delhi
Rahul lives in New DelhiAnalysis. The copy is a different Person, but its address field holds the same reference as the original's, so moving the copy to New Delhi moved Rahul too. Ticket doesn't implement Cloneable, so Object.clone() threw CloneNotSupportedException. Python needs no opt-in: copy.copy makes a shallow copy of any object. The documentation describes it as constructing a new object and then inserting into it references to the objects found in the original [5].
Reading the Java clone() line by line:
implements Cloneableis what makessuper.clone()succeed. Without it, the same call throws, asTicketshowed.public Person clone()overridesObject.clone(), which isprotectedand returnsObject. Making the overridepubliclets other classes call it, and returningPerson(a covariant return type) spares every caller a cast. Bloch recommends exactly this form [4]. Older code often declaresprotected Object clone() throws CloneNotSupportedExceptioninstead, which forces a cast and atryon every caller.- The
try/catchis needed becauseObject.clone()declares the checkedCloneNotSupportedException. SincePersonimplementsCloneable, it can't actually be thrown, so the handler turns it into anAssertionError. super.clone()does the copying.namerefers to the sameStringas before, which is harmless because aStringcannot be changed.addressrefers to the sameAddress, which is the problem, because anAddresscan be changed.
Intuition.
Mechanism. A shallow copy duplicates exactly one object, the one you copied. Everything its fields refer to is shared between the copy and the original. A shared object that cannot change (a String, an Integer, a LocalDate) can never cause a surprise. A shared object that can change makes the two copies one object as far as that state is concerned.
Concrete bite. Someone takes a copy of a customer record to try out a change of address, intending to throw the copy away if the customer cancels. With a shallow copy, the "trial" move to New Delhi has already moved the real customer. Nothing threw an error, and the copy and the original look like two separate records until you change something they share.
💡 Earned rule. A shallow copy is enough when every field is a primitive or refers to an immutable object. As soon as a field refers to something mutable (an Address, a List, an array, a Date), a shallow copy shares it, and you need to copy that field too.
The cost of copying more is time, memory, and the code that does it. The cost of copying less is a bug that appears only when someone changes the shared object, often far from where the copy was made.
5. Deep copy: copying what the references point to
A deep copy also copies the objects that the fields refer to, so the copy shares no mutable state with the original. Java's Object.clone() never does this for you; each mutable field has to be copied by hand. There are two ways to write it in Java. A recursive clone() makes a shallow copy and then replaces each mutable field with a copy of its own. A copy constructor is an ordinary constructor that takes an object of the same class and builds the copy field by field. Python's copy.deepcopy copies the whole object graph for you, and you can also write the copy by hand.
Output:
Rahul lives in Mumbai
Rahul lives in New Delhi
Rahul lives in PuneAnalysis. All three objects now have their own Address, so each city change stayed where it was made. Address.clone() can simply call super.clone(), because its only field refers to an immutable String; a shallow copy of an Address is already a complete copy. copy.deepcopy needed no code at all: it copies recursively, and it keeps a memo of the objects it has already copied, so an object referred to twice is copied once, and cycles don't loop forever [5].
Intuition.
Mechanism. A deep copy is only as deep as the code that makes it. super.clone() copies the top level, and each mutable field is then copied by an explicit line of code. A copy constructor is the same idea written as a constructor: it assigns every field, which means it runs the class's normal construction rules and works with final fields. Bloch concludes that a copy constructor or a static copy factory is usually the better approach, because Cloneable relies on a method the interface doesn't declare, creates objects without a constructor, and conflicts with final fields that refer to mutable objects [4].
Concrete bite. A deep copy that was correct when it was written goes wrong when someone adds a field. Here phones was added to Person after clone() was written, and nobody updated clone():
Output:
original city: Mumbai
original phones: [+91 98200 00001, +91 98100 00002]The address was copied, so the city is safe, but the phone list is shared, and the original has gained the copy's number. Nothing warned anyone: clone() compiled, and every field was set to something. A copy constructor fails more loudly here. If phones is a final field with no initializer, Java requires every constructor to assign it, so a copy constructor that forgets it does not compile ("variable phones might not have been initialized").
💡 Earned rule. In new Java code, write a copy constructor (new Person(other)) or a static copy factory (Person.copyOf(other)), and copy every mutable field inside it. Use clone() only where existing code or an API requires it, and arrays, whose clone() is simple and correct for one level. In Python, use copy.deepcopy for plain data objects, and a hand-written copy method when some fields must stay shared (a database connection, a logger).
The cost of a copy constructor is that it must be updated whenever a field is added, just like clone(). The difference is that with final fields the compiler checks it for you. The cost of copy.deepcopy is that it copies everything it can reach, which can be slow, and wrong for fields that were meant to be shared.
Shallow copy vs deep copy
| Aspect | Shallow copy | Deep copy |
|---|---|---|
| Primitive fields | copied | copied |
| Reference fields | the reference is copied, so the copy points at the same object | the referred-to object is copied too, so the copy points at its own object |
| Nested objects shared with the original? | yes | no |
| Extra code needed in nested classes (Java)? | no | yes: each mutable nested class needs its own copy method |
| How to make one | super.clone(), copy.copy |
a copy constructor, a recursive clone(), copy.deepcopy |
| Enough when… | every field is a primitive or refers to an immutable object | any field refers to a mutable object |
Your Turn — Practice: Object Cloning
A Library holds a list of Books. Implement a shallow clone and a deep clone, then watch which one follows a change made to the original's books.
6. Mental-model summary
| Principle | Consequence |
|---|---|
| A field that holds an object holds a reference to it | Two holders of one reference share one object, and see each other's changes |
| Association: objects linked, created separately | Either can outlive the link; decide whether to copy collections you receive |
| Aggregation: a whole groups parts it did not create | Parts can be shared and changed by others; keep their rules inside the parts |
| Composition: the whole creates and owns its parts | Only true if no reference to a part or to the internal collection escapes |
| A shallow copy copies references | Copy and original share every object their fields point to |
| A deep copy also copies the referred-to objects | Only as deep as the code that makes it; prefer a copy constructor to clone() |
7. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| An object's contents change though no one called its methods | it stored a collection the caller still holds | store a defensive copy (List.copyOf, tuple(...)) |
| A composite gains parts it never created | a getter returns the internal collection | return an unmodifiable view or a copy; add parts through the whole's methods |
| A "deleted" part is still in use | a reference to the part escaped | make parts immutable, or return copies or plain data |
| Changing a copy changes the original | a shallow copy shares a mutable field | copy that field too (deep copy) |
| A deep copy worked, then stopped working | a mutable field was added and the copy code wasn't updated | use a copy constructor with final fields, so the compiler checks it |
CloneNotSupportedException at run time |
the class doesn't implement Cloneable |
implement it, or use a copy constructor instead |
clone() has protected access in Object |
the nested class doesn't override clone() |
override it as public, or use a copy constructor |
cannot assign a value to final variable in clone() |
clone() tries to replace a final field |
use a copy constructor |
✅ Check yourself
One check per objective. Answer before you open anything.
A Playlist holds a List<Song>, and each Song has a mutable rating. Users can duplicate a playlist and then reorder the copy and re-rate its songs, without affecting the original. What kind of copy do you need, and how would you write it?
The duplicate must not share the list, because reordering it would reorder the original, and it must not share the Song objects, because re-rating a song would change the original's rating. So it needs a deep copy, two levels down. Write a copy constructor Playlist(Playlist other) that creates a new ArrayList and adds new Song(s) for every song, with Song(Song other) copying its fields. If ratings were stored per playlist instead, Song could be made immutable, and copying the list alone (new ArrayList<>(other.songs)) would be enough: the songs could then be shared safely.
📚 Sources
- Object Management Group, OMG Unified Modeling Language (UML), version 2.5.1 (2017), §9.5.3 "Semantics" (composite aggregation) and §9.9.1 "AggregationKind" — https://www.omg.org/spec/UML/2.5.1/PDF
java.lang.Object.clone(), Java SE 21 API — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html#clone()java.lang.Cloneable, Java SE 21 API — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Cloneable.html- Joshua Bloch, Effective Java, 3rd edition (Addison-Wesley, 2018), Item 13 "Override
clonejudiciously". - Python 3 documentation,
copy— Shallow and deep copy operations — https://docs.python.org/3/library/copy.html java.util.Collections.unmodifiableList, Java SE 21 API — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collections.html#unmodifiableList(java.util.List)
🧪 Predict, then check.
- In section 2, delete the line
guild.add(alex). Predict what the Security guild line prints now, and whether the Engineering line changes. - In the first program in section 5, make the field
final Address address;inPerson. Predict which of the two copy methods still compiles, then compile it. - In the same program, delete the
clone()method fromAddress, but keepimplements Cloneable. Predict whetherPersonstill compiles. - In the Python program in section 4, add
print(dup.name is rahul.name). Predict what it prints, and why that sharing is harmless.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.