☕ Java
Association
Association is a relationship between two classes where one class uses or interacts with another, but neither class owns the other and both can exist independently. It is the most general form of relationship between objects in object-oriented design — a connection that says two objects know about each other or collaborate with each other without either being a part of the other. Association is the foundation from which the more specific relationships of aggregation and composition are derived.
What Association Means and Why It Matters
Every non-trivial software system is built from objects that interact. Understanding the nature of those interactions — the relationships between objects — is the foundation of good object-oriented design. When two classes are related, the relationship always falls into one of a small number of categories: inheritance (is-a), association (uses-a or knows-a), aggregation (has-a with independence), or composition (has-a with ownership).
Association is the broadest of the non-inheritance relationships. It simply says that two classes are connected — objects of one class interact with objects of the other class. The interaction might be that one class holds a reference to the other, one calls methods on the other, or they collaborate to complete some operation. What makes it association (rather than aggregation or composition) is that neither object is a part of the other and neither object controls the lifetime of the other.
Real-world examples make this concrete. A Doctor treats Patients — a doctor can treat many patients, and a patient can be treated by many doctors, but neither doctors nor patients cease to exist when the association ends. A Student is enrolled in Courses — students exist independently of courses and courses exist independently of students. A Driver drives a Car — the driver can drive many cars over a lifetime, and a car can be driven by many drivers, but neither owns the other in the sense that one's existence depends on the other.
Understanding association — and distinguishing it from aggregation and composition — is important because it shapes how you design classes, what constructors and setters look like, how objects are created and destroyed, and what happens to related objects when one is deleted. Getting these relationships right produces designs that are accurate models of the problem domain and that change gracefully as requirements evolve.
Java
// ── Association — two independent classes that know about each other: ──
public class Doctor {
private String name;
private String specialisation;
public Doctor(String name, String specialisation) {
this.name = name;
this.specialisation = specialisation;
}
// Association: Doctor uses Patient but does NOT own it
public void treat(Patient patient) {
System.out.printf("Dr. %s (%s) treating %s%n",
name, specialisation, patient.getName());
patient.recordVisit(this);
}
public String getName() { return name; }
public String getSpecialisation() { return specialisation; }
}
public class Patient {
private String name;
private int age;
private List<String> visitHistory = new ArrayList<>();
public Patient(String name, int age) {
this.name = name;
this.age = age;
}
public void recordVisit(Doctor doctor) {
visitHistory.add("Visited Dr. " + doctor.getName() +
" (" + doctor.getSpecialisation() + ")");
}
public String getName() { return name; }
public List<String> getHistory(){ return new ArrayList<>(visitHistory); }
}
// ── Both exist independently: ─────────────────────────────────────────
Doctor cardiologist = new Doctor("Smith", "Cardiology");
Doctor neurologist = new Doctor("Jones", "Neurology");
Patient alice = new Patient("Alice", 42);
Patient bob = new Patient("Bob", 55);
// Association in action — neither owns the other:
cardiologist.treat(alice);
neurologist.treat(alice);
cardiologist.treat(bob);
// Doctor and Patient objects are completely independent —
// deleting cardiologist does not affect alice or bob.
System.out.println("Alice's visits: " + alice.getHistory());Unidirectional and Bidirectional Association
Association has a direction — which class knows about the other. In a unidirectional association, only one class holds a reference to the other. Class A knows about Class B, but Class B has no reference to Class A. This is the simpler and more common form: it imposes less coupling because B can be changed without affecting A's awareness of B.
In a bidirectional association, both classes hold references to each other. Class A has a reference to Class B and Class B has a reference to Class A. This gives both sides the ability to navigate the relationship and call methods on the other, but it increases coupling and requires careful management to keep both sides in sync. If you add a reference on one side but forget to update the other, the association becomes inconsistent.
Choosing between unidirectional and bidirectional association is a design decision based on which sides of the relationship actually need to navigate to the other. If only one class needs to call methods on the other, make it unidirectional and keep the coupling minimal. Only add the reverse direction when the other class genuinely needs to navigate back.
Managing bidirectional associations correctly requires disciplined link management — updating both sides atomically when establishing or breaking the association. The standard pattern is to make one side responsible for managing both references through a dedicated method, rather than letting callers set each side independently and risk creating inconsistent states.
Java
// ── Unidirectional association — Order knows about Customer: ─────────
public class Customer {
private String name;
private String email;
public Customer(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}
public class Order {
private int orderId;
private Customer customer; // Order knows about Customer
private double total;
public Order(int orderId, Customer customer, double total) {
this.orderId = orderId;
this.customer = customer; // holds a reference to Customer
this.total = total;
}
public void printReceipt() {
System.out.printf("Order #%d for %s (%.2f)%n",
orderId, customer.getName(), total);
}
}
// Customer has NO reference to Order — unidirectional.
// Customer can be used without knowing about Order at all.
// ── Bidirectional association — managed link: ─────────────────────────
public class Student {
private String name;
private List<Course> courses = new ArrayList<>(); // Student → Course
public Student(String name) { this.name = name; }
// Manages BOTH sides of the bidirectional link:
public void enrol(Course course) {
if (!courses.contains(course)) {
courses.add(course);
course.addStudentInternal(this); // update other side
}
}
public void withdraw(Course course) {
if (courses.remove(course)) {
course.removeStudentInternal(this); // update other side
}
}
public String getName() { return name; }
public List<Course> getCourses() { return new ArrayList<>(courses); }
}
public class Course {
private String title;
private List<Student> students = new ArrayList<>(); // Course → Student
public Course(String title) { this.title = title; }
// Package-private helpers — only Student.enrol/withdraw should call these:
void addStudentInternal(Student s) { students.add(s); }
void removeStudentInternal(Student s) { students.remove(s); }
public String getTitle() { return title; }
public List<Student> getStudents() { return new ArrayList<>(students); }
}
Student alice = new Student("Alice");
Course java = new Course("Java Programming");
Course python = new Course("Python Basics");
alice.enrol(java);
alice.enrol(python);
System.out.println("Alice's courses: " +
alice.getCourses().stream().map(Course::getTitle).toList());
System.out.println("Java students: " +
java.getStudents().stream().map(Student::getName).toList());Multiplicity in Associations
Multiplicity describes how many objects on each side of an association can be involved. The common multiplicities are one-to-one (each object relates to exactly one other), one-to-many (one object relates to many others), many-to-one (many objects relate to one), and many-to-many (many objects relate to many others). Multiplicity is expressed in UML diagrams but is implemented in Java through the choice of field type — a single reference for to-one, and a collection for to-many.
Getting multiplicity right in the design is important for correctness and performance. A one-to-many association implemented as a single reference loses data — only the last associated object is retained. A many-to-many association implemented as two one-to-many collections requires careful synchronisation to remain consistent. Choosing the wrong multiplicity produces models that cannot accurately represent the problem domain.
Many-to-many associations often benefit from being decomposed into two one-to-many associations through a join class — an object that represents the association itself and can carry additional data about the relationship. A Student-Course enrolment is a many-to-many association, but the Enrolment object that joins them can carry the date enrolled, the grade achieved, and the semester, none of which belong to Student or Course alone.
Java
// ── One-to-one: Manager has one Office: ──────────────────────────────
public class Manager {
private String name;
private Office office; // one Manager → one Office
public Manager(String name) { this.name = name; }
public void assignOffice(Office office) { this.office = office; }
public Office getOffice() { return office; }
public String getName() { return name; }
}
public class Office {
private String location;
private Manager occupant; // one Office → one Manager (bidirectional)
public Office(String location) { this.location = location; }
public String getLocation() { return location; }
public Manager getOccupant() { return occupant; }
public void setOccupant(Manager m) { this.occupant = m; }
}
// ── One-to-many: Department has many Employees: ───────────────────────
public class Department {
private String name;
private List<Employee> employees = new ArrayList<>(); // one → many
public Department(String name) { this.name = name; }
public void hire(Employee e) {
employees.add(e);
e.setDepartment(this);
}
public String getName() { return name; }
public List<Employee> getEmployees() { return new ArrayList<>(employees); }
}
public class Employee {
private String name;
private Department department; // many → one
public Employee(String name) { this.name = name; }
void setDepartment(Department d) { this.department = d; }
public String getName() { return name; }
public Department getDepartment() { return department; }
}
// ── Many-to-many with join class: ─────────────────────────────────────
public class Enrolment {
private final Student student;
private final Course course;
private final LocalDate enrolledOn;
private String grade; // additional association data
public Enrolment(Student student, Course course) {
this.student = student;
this.course = course;
this.enrolledOn = LocalDate.now();
}
public Student getStudent() { return student; }
public Course getCourse() { return course; }
public LocalDate getEnrolledOn() { return enrolledOn; }
public String getGrade() { return grade; }
public void setGrade(String grade) { this.grade = grade; }
}Related Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.