Class Relationships
Association
Basic relationship between classes
Association is the most fundamental structural relationship between classes in Object-Oriented Design. It establishes a connection where objects of one class can interact with or hold references to objects of another class, representing a pathway of communication without either object controlling the other's lifetime.
1. Learning Objectives
- Understand the structural concept of Association as a "uses-a" or "knows-a" relationship.
- Differentiate between Unidirectional and Bidirectional associations.
- Understand cardinality (1:1, 1:N, M:N) and map them to appropriate collection structures.
- Solve the cyclic reference memory leak problem in bidirectional associations (using weak references/pointers).
- Trace how associated objects are stored on the JVM heap and referenced from the stack.
2. Problem Statement
In procedural programming, code modules communicate by passing raw data structures into global operations. This creates tight coupling and makes it hard to modify one part of the system without breaking others.
In object-oriented systems, we want objects to act as self-contained entities with clear boundaries. If two entities (such as a Teacher and a Student) need to collaborate, they shouldn't merge into a single massive class, nor should they copy each other's state. We need a way to establish a structural link where they can collaborate and send messages to each other while remaining independent lifecycle entities.
3. Real-world Analogy
Think of a Doctor and a Patient. A Doctor treats patients, and a Patient visits doctors. They communicate, but they have completely independent lifecycles:
- If a doctor retires, the patients do not cease to exist. They simply associate with another doctor.
- If a patient is discharged, the doctor is not destroyed. The doctor remains active to treat other patients.
This dynamic connection is an Association. It's a peers-style relationship where both participants are independent.
4. Theory
Association represents a structural link between two classes. It means an object of one class is connected to an object of another class.
Key Characteristics of Association:
- Lifecycle Independence: There is no ownership. The lifetime of associated objects is managed independently.
- Uses-A / Knows-A: Represents structural dependency where objects call methods or read properties on one another.
- Multiplicity (Cardinality): Defines how many instances of class A can connect to one instance of class B:
- 1-to-1 (1..1): E.g., Person and Passport.
- 1-to-Many (1..*): E.g., Department and Employee.
- Many-to-Many (*..*): E.g., Teacher and Student.
Types of Directionality:
Unidirectional
Only one class holds a reference to the other. For instance, a Customer holds a reference to a Product to represent their purchase. The Product class has no field pointing back to the Customer.
Bidirectional
Both classes hold references to each other. A Teacher keeps a list of Students, and each Student keeps a list of their Teachers. This allows walking the relationship graph in both directions.
5. Visual Diagrams (UML & Memory structures)
Class Diagram (Bidirectional Many-to-Many)
In UML, an association is represented by a solid line. Multiplicity is indicated at both ends of the line.
Object Diagram
Shows instances at runtime. Two students are connected to one teacher, and they share this connection independently.
Memory Diagram
How values are mapped in memory. Stack frames contain local reference addresses that point to their actual heap allocations. The heap objects hold lists containing reference pointer targets.
Stack Frame (References)
Heap space (Objects)
name: "Dr. Smith"
students: [@0x70B2]
name: "Alice"
teachers: [@0x90A1]
Object Lifecycle
Unlike Composition, the lifecycles are decoupled:
6. Syntax Explanation
Setting up relationships is done via properties that store references or collections of references:
- Java: Uses instance variables of object types, e.g.,
private Student student;or collections likeList<Student> studentsto hold associated objects. - Python: Fields are declared in the initializer block. Typing hints like
List["Student"]are used to document the relationship. - C++: Direct object nesting is avoided, as it allocates child objects inside the parent layout. Instead, references or pointers are used. When using smart pointers, bidirectional relationships require
std::weak_ptrto break dependency cycles and prevent memory leaks.
7. Step-by-Step Implementation
Let's build a bidirectional association between a Student and a Teacher:
- Step 1: Create the
Studentclass containing private instance lists to holdTeacherreferences. - Step 2: Create the
Teacherclass containing lists ofStudentreferences. - Step 3: Write synchronization helper methods. When calling
student.addTeacher(teacher), the class must automatically invoketeacher.addStudent(this)(and vice versa) to keep both sides of the association synchronized. Avoid infinite loops by checking if the reference is already registered. - Step 4: In C++, implement the cyclic resolver using
std::weak_ptr. Keep one side asstd::shared_ptrand the other asstd::weak_ptr.
8. Complete Code (Mini Project)
9. Code Walkthrough
In the Java code above, the key logic resides in the synchronization helpers:
addTeacher(Teacher teacher)first checks if the teacher is null or already in the list. If it isn't, it adds it to its ownteacherslist and then callsteacher.addStudent(this).- Inside
addStudent, a similar check prevents an infinite loop. It appends the student and stops because the student already contains the teacher. This maintains synchronization automatically. - In the C++ version,
teachersare stored asstd::weak_ptr<Teacher>inside the Student. This breaks the reference cycle, allowing both class hierarchies to release memory cleanly whenmain()exits. Thelock()method is used at runtime to briefly convert the weak pointer to a shared pointer when displaying the details.
10. Execution Flow
- Instantiations: Two teacher objects are allocated in memory. Next, two student objects are allocated.
- Linkage 1:
alice.addTeacher(mathTeacher)runs:- Alice appends
mathTeacherreference. - Alice triggers
mathTeacher.addStudent(alice). - MathTeacher appends
alicereference. - MathTeacher triggers
alice.addTeacher(mathTeacher)(which exits immediately due to the duplicate check).
- Alice appends
- Linkage 2 & 3: Bob links to mathTeacher, and Alice links to physicsTeacher in the same manner.
- Output Trace: Calls print out list elements from both perspectives, resolving reference pointers to output names.
11. Internal Working
When new Student("Alice") is called, the JVM heap allocates memory for the Student instance. The constructor instantiates a fresh ArrayList on the heap, and a pointer to this list is saved inside the Student instance layout.
When an association is established, the reference address of the Teacher heap object is appended to the Student's list. No data copying occurs; only memory addresses are passed.
In languages with automatic memory management (JVM, Python VM), the garbage collector monitors reference counts. If the stack reference variable pointing to mathTeacher is set to null, the garbage collector will NOT collect the Teacher instance from the heap immediately. This is because the Student instances still hold references to it inside their internal list. The object is only swept once all incoming paths of references are severed.
12. Complexity Analysis
- Time Complexity:
- Adding association: $O(N)$ where $N$ is the number of elements already in the collection (due to the containment lookup check
contains()/std::find()). This can be optimized to $O(1)$ by using aHashSet/unordered_set. - Traversal: $O(M)$ to print or visit all $M$ associated targets.
- Adding association: $O(N)$ where $N$ is the number of elements already in the collection (due to the containment lookup check
- Space Complexity: $O(R)$ where $R$ is the number of relations established. Each reference takes 8 bytes (on 64-bit systems) inside the dynamic collections.
13. Best Practices
- Encapsulate collections: Do not return raw internal list variables directly. Return unmodifiable copies or read-only iterators to prevent clients from bypass-mutating the class invariants.
- Use null protection: Always guard association setters against null inputs.
- Keep associations weak: If two classes don't have a clear structural "whole-part" hierarchy, use association. Do not elevate associations to composition.
- Break circular dependency paths in C++: Keep parent/owner collections as
std::shared_ptrand peer/child references asstd::weak_ptr.
14. Common Mistakes
- Memory Leak in C++: Using
shared_ptron both sides of a bidirectional link, preventing reference counts from hitting zero and leaking heap allocations. - Infinite Recursion in Helpers: Writing synchronization wrappers without checking if the object is already present. This results in
StackOverflowErrordue to infinite mutual calls. - Infinite Recursion in toString() / serialization: Printing associated objects in a loop (e.g., Student prints Teachers, which prints Students, leading to crash). Break the chain by printing only ID fields in toString().
15. Interview Questions
Answer:
- Association: Peer-to-peer relationship. Independent lifecycles. E.g., Doctor and Patient.
- Aggregation: "Has-a" relationship representing a whole-part structure, but parts can exist without the whole. E.g., Library and Book.
- Composition: Strict ownership where parts cannot survive without the whole. Lifecycles are tied. E.g., House and Room.
Answer: Use
std::weak_ptr on one side of the bidirectional association. A weak pointer references an object without increasing its shared reference count, enabling clean deallocation.
Answer: It exposes the internal state collection to external modifications, bypassing safety/loop checks implemented inside synchronization helpers like
addTeacher().
16. Practice Exercises
- Easy: Build a unidirectional association between a
Customerand anAddress. Ensure Customer holds one Address. - Medium: Implement a bidirectional 1-to-N association between a
Driverand aCab. A Driver can associate with only one Cab, but a Cab can keep track of historical Drivers. Write synchronization logic. - Hard: Create a many-to-many bidirectional association between a
Flightand aPassenger. Make the association thread-safe in Java so that multiple threads booking seats simultaneously do not cause list state collisions.
17. Challenge Problem
Design a custom utility class or annotation processor in Java/Python that automatically manages bidirectional sync. When a client performs A.link(B), the library should use reflection or dynamic proxies to automatically invoke B.link(A), avoiding manual boilerplate synchronization helpers in production domain files.
18. Summary
- Association is a general connection representing independent entities communicating with each other.
- Lifecycles of associated objects are decoupled; deleting one does not affect the other.
- Bidirectional sync helpers must guard against infinite recursion checks.
- Memory management requires attention to cyclic pointer references in C++.
19. Cheat Sheet
| Relationship Type | Ownership Type | UML Notation | Lifecycle Constraint |
|---|---|---|---|
| Association | None (Peers) | Simple solid line (or arrow) | Fully Independent |
| Aggregation | Whole-Part (Weak) | Hollow diamond | Independent survival |
| Composition | Whole-Part (Strong) | Filled diamond | Bound to owner lifecycle |
20. Quiz
1. Which relationship holds true when associated objects exist independently?
A) Composition
B) Association (Correct)
C) Inheritance
2. What happens to Student objects if their associated Teacher object is collected by the GC?
A) They are instantly garbage-collected
B) They throw a NullPointerException
C) They remain completely unaffected in memory (Correct)
3. How is a standard unidirectional association represented in UML?
A) Solid line with an arrow pointing to the referenced class (Correct)
B) Hollow diamond pointing to the whole class
C) Solid diamond pointing to the owner class
4. What error can occur if bidirectional association synchronization doesn't check for existing elements?
A) OutOfMemoryError
B) StackOverflowError due to infinite recursion (Correct)
C) ClassCastException
5. Why are C++ smart shared pointers risky in bidirectional associations?
A) They decrease compiler validation speed
B) They cause reference cycles, leaking memory on heap (Correct)
C) They can only reference scalar primitive types
6. Which smart pointer breaks cycles in bidirectional associations in C++?
A) std::unique_ptr
B) std::weak_ptr (Correct)
C) std::shared_ptr
7. What is the complexity of establishing an association if we search an ArrayList first?
A) O(log N)
B) O(N) (Correct)
C) O(N log N)
8. Which relationship indicates a structural "knows-a" link?
A) Dependency
B) Association (Correct)
C) Composition
9. How should lists be returned via getters to protect encapsulation?
A) Return the direct internal list variable reference
B) Return a copy or an unmodifiable view of the collection (Correct)
C) Keep the list variable public
10. What can cause serialization stack overflow in bidirectional relations?
A) Cyclic toString() calls referencing associated objects (Correct)
B) GC running during serialization
C) Static imports
21. Next Lesson Preview
In the next lesson, we will explore Aggregation to see how whole-part structures are modeled when children can still survive independently outside the boundaries of the parent owner!