UNC COMP 301 - F26/Lec 7 - Coupling, Composition over Inheritance; Error handling
Watch on YouTube →
Overview
Muhammad Sayeed Ghani explains how to choose among inheritance, composition, and aggregation, emphasizing loosely coupled packages and the design guideline “composition over inheritance” when no clear is-a hierarchy exists. He then introduces Java exception handling—from unsafe global error codes and sentinel return values to throwing and catching exception objects—and demonstrates catch matching, stack traces, and why file cleanup must still happen when an exception interrupts execution.
Key takeaways
- Prefer loosely coupled packages with high internal cohesion; when depending on another package, reference its interface rather than concrete classes where possible.
- Use inheritance only for a well-supported is-a hierarchy, such as Robin–Bird–Animal; otherwise, composition or aggregation keeps independent behaviors modular.
- Composition and aggregation differ by component lifetime and ownership: internally created components belong to the container, while externally supplied components can live independently.
- Global error codes and sentinel values such as null or String.indexOf’s -1 require callers to remember extra checks; Java exceptions instead propagate until a matching handler is found or the JVM terminates the program.
- Catch clauses match by subtype: a RuntimeException catch handles ArithmeticException, but an ArithmeticException catch cannot handle a general RuntimeException.
- A file writer must be closed even when an exception interrupts a loop; placing cleanup only after the risky code can lose buffered output, so cleanup needs an exception-safe approach such as finally.
Chapters
- Assignment 3 will focus on error handling and exceptions, motivating the lecture’s second half.
- Inheritance models an is-a relationship, such as Student is a Person; aggregation and composition model has-a relationships.
- A Course may aggregate a Professor, Room, and Students, while a Vehicle may compose its Engine and Wheels.
- Named references between classes create strong dependencies; references through interfaces reduce reliance on another package’s implementation.
- The diagrams contrast loose coupling and high cohesion with high coupling and low cohesion.
- Loose coupling makes packages easier to maintain and less vulnerable to changes in external packages or their releases.
- The example asks how one overall type can provide the Animal.eat, Bird.fly, and Robin.singSong contracts.
- A single Robin implementation can implement all three interfaces directly, but this can jumble many methods together as interfaces grow.
- The Animal–Bird–Robin example provides a clear hierarchy: a Robin is a Bird, and a Bird is an Animal.
- An Animal implementation supplies the Animal behavior, a Bird implementation extends it and implements Bird, and a Robin implementation extends Bird and implements Robin.
- Separating behavior across the hierarchy makes the design more modular and avoids placing every interface’s methods into one large class.
- Use this approach only when the is-a relationships are clear; uncertain relationships are a reason not to force inheritance.
- When interfaces have no genuine hierarchy, implement Animal, Bird, and Robin separately, then place those implementation objects inside a container.
- The container delegates methods such as eat to its Animal field rather than implementing all behavior itself.
- Creating component instances inside the container makes this example composition because those components do not have independent lifetimes.
- Passing component objects into a constructor or setter makes them externally created and independently managed, which is aggregation rather than composition.
- Inheritance uses one object across the class hierarchy; composition or aggregation uses a container plus separate component objects.
- Break problems into components, use inheritance for a clear hierarchy, and otherwise favor composition or aggregation over one oversized implementation.
- Invalid inputs such as a string where an integer is expected, a negative ID, or division by zero can otherwise cause an abrupt failure.
- Printing an error and returning control may not be enough when another method—not a person—called the failing code.
- Graceful handling should let an application report a problem and continue when appropriate, rather than crashing without a useful response.
- An older pattern stores status in a global or static variable: zero means success, while values such as -1 and -2 identify errors.
- The called method sets the code, and the caller must inspect it, interpret its documented meaning, and reset it before later calls.
- If the caller forgets to check the code, execution continues with invalid data instead of forcing the failure to be addressed.
- Methods can signal failure with an out-of-range primitive value or return null instead of an expected object.
- Java String.indexOf returns -1 when a search target is absent, illustrating a sentinel that remains in common use.
- Sentinels require documentation and caller checks, and a valid result range may leave no convenient special value.
- Java separates detecting a problem by throwing an exception from responding to it by catching the exception.
- An exception object can carry a type, message, and stack-trace details, allowing calling code to inspect the failure.
- If no code catches an exception, it propagates up the call stack and the JVM’s default handler reports it and terminates the program.
- A method that receives a negative age can use throw with a newly created IllegalArgumentException.
- A message such as “age can't be negative” adds context to the exception object.
- Choosing a specific exception subtype communicates more than throwing the general Throwable or Exception type.
- Place potentially failing calls inside a try block and provide one or more catch clauses after it.
- When a method throws, execution stops at the throw point, skips the remaining statements in the try block, and searches for a matching catch.
- If no catch matches, the exception continues to the calling method and ultimately to the JVM handler if it remains uncaught.
- A catch clause matches when the thrown object is an instance of the catch type or one of its subtypes.
- A broad catch such as Throwable placed before a narrower catch can make the narrower handler unreachable, producing a compile-time error.
- Catch clauses should be ordered from specific exception types to more general parent types.
- When the try block prints A and throws RuntimeException, a matching RuntimeException catch prints C, then execution resumes after the catch and prints E.
- An ArithmeticException catch does not handle a thrown RuntimeException: ArithmeticException is a subtype of RuntimeException, not its parent.
- With ArithmeticException followed by RuntimeException catches, a thrown RuntimeException skips the first handler, matches the second, and produces A, D, E; catching Exception also handles RuntimeException.
- The example writes results from 100 divided by each denominator to a FileWriter, then encounters an ArithmeticException when a denominator is zero.
- Because execution jumps out of the try block before reaching the ordinary close call, the writer may remain unclosed and buffered output may be lost.
- Ghani previews Java’s finally keyword as the upcoming way to ensure cleanup runs even when an exception interrupts normal execution.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Muhammad Sayeed Ghani.