Skip to main content
Object Oriented Software Design
Object Oriented Software Design
Difficulty: Rx 2h Lecture #6: Exceptions in Java

Lab #3: Simulating Circuits — Exceptions and Interactive Probes

Continues the circuit simulator from Lab #2. By the end of this lab, your components will be able to compute their own logical state, correctly refuse to do so when they aren’t fully wired, and you’ll be able to probe a circuit interactively.

Objectives

  • Model a genuine error condition with a custom checked exception, and see the compiler’s checked-exception rules bite in practice.
  • Compare three different strategies for handling the same exception: propagate all the way up, catch centrally, catch locally.
  • Use override polymorphism to collapse near-duplicate code into one shared method.
  • Add a new component type (an interactive probe) to an existing hierarchy without touching the rest of it.

Simulation basics

A component’s logical state comes from boolean getState(). A Switch is turned on or off directly, via on()/off(). Every other component computes its state from whatever is connected to its input(s) — which only works if it’s actually connected. If it isn’t, that’s a genuine error condition, modeled as a checked exception: NotConnectedException.

Here’s how the hierarchy from Lab #2 extends to support this (also available pre-packaged as simulation.tar):

public class NotConnectedException extends Exception {
}

public abstract class Component {
    // ... getId(), as before

    public abstract boolean getState() throws NotConnectedException;
}

public class Switch extends Component {
    protected boolean state;

    public void on()  { state = true; }
    public void off() { state = false; }

    public boolean getState() throws NotConnectedException {
        return state;
    }
}

public class Valve extends Component {
    // ... in, setIn(), as before

    public boolean getState() throws NotConnectedException {
        if (in == null) {
            throw new NotConnectedException();
        }
        return in.getState();
    }
}

public class Not extends Gate {
    // ... in, setIn(), as before

    public boolean getState() throws NotConnectedException {
        if (in == null) {
            throw new NotConnectedException();
        }
        return !in.getState();
    }
}

1. Evaluating gates

Difficulty: Rx

Integrate the code above into your hierarchy from Lab #2, then write getState() for And and Or following the same pattern.

Test it: three ways to handle the same exception

In CircuitTest, write a method traceStates(Component[] components) that prints each component’s description and its state, and call it from the // display section.

The first time you compile, you should see the checked-exception error unreported exception NotConnectedException; must be caught or declared to be thrown — this is expected if you haven’t dealt with the exception yet. Work through these three experiments in order; each one changes how the exception is handled.

Experiment 1 — let it propagate all the way to the top.

  1. Declare throws NotConnectedException on traceStates. Recompile — the same error now appears on main, because main calls traceStates, which can throw.
  2. Declare throws NotConnectedException on main as well.
  3. Run it, and watch the program terminate abruptly with a stack trace the moment it reaches an unconnected component (no polite “goodbye” message at the end).

Experiment 2 — catch it centrally, in main.

  1. Remove throws NotConnectedException from main.
  2. Wrap the call to traceStates in // display with a try/catch that prints "at least one component is not connected".
  3. Run it again — notice the program now ends politely, but still stops at the first unconnected component; nothing after it in the loop gets traced.

Experiment 3 — catch it locally, per component.

  1. Remove the try/catch you added around the traceStates(components) call in // display during Experiment 2 — go back to calling it plain, traceStates(components);, with no wrapper.
  2. Remove throws NotConnectedException from traceStates.
  3. Instead, wrap each individual call to getState() inside traceStates in its own try/catch, printing "not connected" for that one component.
  4. Run it once more: now the simulation no longer stops at the first problem — it reports every component correctly, connected or not, and reaches the end of the loop regardless.

This progression is worth pausing on: the same exception, handled at three different points in the call stack, produces three very different program behaviors. Where you catch an exception is a design decision, not just a syntax choice.

2. Getting more object-oriented

2.1 A shared traceState() method

Difficulty: Rx

Move the per-component tracing logic (description + state, or "not connected") into Component itself, as public String traceState(). Because this single method is written once against description() and getState() — and dynamic binding always resolves those calls to this particular object’s own overridden versions — every subclass gets a correct traceState() for free, without writing it again. This is override polymorphism doing real work: one method body, many behaviors, entirely because of which object it happens to be called on.

Update traceStates in CircuitTest to use it, and retest.

2.2 Factoring TwoInputGate

Difficulty: Rx

Look closely at getState() in And and Or: the connectivity check (both inputs present or throw) is identical in both — only the actual logical computation differs. Factor the shared part into TwoInputGate, introducing an abstract method with the exact signature boolean eval() throws NotConnectedException; (it needs the throws clause because its body will call getState() on in1/in2, which itself throws). Each subclass implements just its own logic: in1.getState() && in2.getState() for And, in1.getState() || in2.getState() for Or.

Test it: recompile only TwoInputGate, And, and Or — nothing else needs to change, not even CircuitTest, because the protocol (the set of public methods) these classes expose hasn’t changed. This is a small, concrete demonstration of why encapsulation and separate compilation matter: a well-factored internal change doesn’t ripple outward.

3. Probes

3.1 Interactive probes

Difficulty: Hard

Probing a component means substituting an interactive probe on one of its inputs — something that asks the user, live, what value to force there — so you can observe a circuit’s behavior without physically flipping switches.

Write a Probe class:

  • Probe is itself a subclass of Component, so it can be connected to any input via the usual setIn-style methods, exactly like any other component.
  • A Probe remembers, via its constructor, which component and which named input it’s standing in for — for example, replacing switch s1 on Or’s first input with an interactive probe:
    or.setIn1(new Probe(or, "in1"));
  • getState() on a Probe asks the user what value to force, e.g.:
    in1 of Or@55e83f9, true or false?

Test it: in CircuitTest’s // wire section, temporarily replace a couple of connections with probes instead, and trace the circuit’s state to see them prompt you interactively.

3.2 Lazy probes

Difficulty: Hard

You’ll notice a problem with plain probes on a real circuit: because evaluation works backward from outputs to inputs, a component feeding several downstream gates gets probed — and prompts the user — once per downstream use. That’s both annoying and risky, since nothing guarantees you answer the same way twice.

Write LazyProbe, a subclass of Probe, that fixes this:

  • Give it a constructor with the same parameters as Probe’s — (Component target, String inputName) — that just forwards to super(target, inputName). Constructors are never inherited in Java, so without this, new LazyProbe(target, inputName) won’t compile even though Probe already has a matching one.
  • The first time it’s asked for its state, it prompts the user as before, but remembers the answer.
  • Every subsequent call to getState() returns the remembered value without prompting again.
  • Add a reset() method so a LazyProbe can be primed for a fresh round of probing.

Retest with LazyProbe in place of Probe — thanks to override polymorphism, this should be a one-word change at each connection site, nothing else in CircuitTest needs to know the difference.