javaoopsoftware-engineering

Java and Object-Oriented Programming: Getting It Right From the Start

Java 1.1 is out and teams are moving fast. Here is what object-oriented design actually means in practice — and why getting it right from day one saves months of pain.

·5 min read

Java and Object-Oriented Programming: Getting It Right From the Start

Java 1.1 shipped last month and at Motorola we are making it our standard language for new development. Most of the team comes from C backgrounds — some C++ — and the transition to genuine object-oriented thinking is proving harder than just learning new syntax.

This post covers what OOP means in practice, the mistakes I see people making, and what clicks when you finally get it.

The Core Shift: Data Owns Its Behaviour

In C, a program is functions operating on data structures. You pass a struct pointer around and functions do things to it:

struct Device {
    char name[64];
    char ip_address[16];
    int  status;
};

void poll_device(struct Device *d);
void reset_device(struct Device *d);

It works. But poll_device and reset_device are free-floating — any code anywhere can call them with anything. The relationship between the data and valid operations on it exists only in programmer heads and comments.

Java inverts this. The data and its valid operations live together:

public class NetworkDevice {
    private String name;
    private String ipAddress;
    private int status;

    public NetworkDevice(String name, String ipAddress) {
        this.name = name;
        this.ipAddress = ipAddress;
        this.status = 0;
    }

    public void poll() {
        // polling logic lives here, with the device
    }

    public void reset() {
        // reset logic lives here
    }

    public int getStatus() {
        return status;
    }
}

status is private. Nothing outside this class can read or modify it except through the controlled interface you define. That is encapsulation and it is the foundation everything else builds on.

Inheritance: Useful, But Easy to Overdo

Once you understand classes, inheritance feels like the whole point of OOP. Define a base and specialise it:

public abstract class NetworkDevice {
    protected String name;
    protected String ipAddress;

    public NetworkDevice(String name, String ipAddress) {
        this.name = name;
        this.ipAddress = ipAddress;
    }

    public abstract void poll();

    public String getName() {
        return name;
    }
}

public class Router extends NetworkDevice {

    public Router(String name, String ipAddress) {
        super(name, ipAddress);
    }

    @Override
    public void poll() {
        System.out.println("Polling router via SNMP: " + name);
    }
}

public class Switch extends NetworkDevice {

    public Switch(String name, String ipAddress) {
        super(name, ipAddress);
    }

    @Override
    public void poll() {
        System.out.println("Polling switch: " + name);
    }
}

Our monitoring loop can now iterate a collection of NetworkDevice references and call poll() on each without knowing the concrete type. This is polymorphism and it is genuinely powerful.

The trap is deep inheritance hierarchies. Once a type needs behaviour from two different parent classes you are stuck — Java has no multiple inheritance of classes. The answer is interfaces.

Interfaces: The Right Way to Share Behaviour

public interface Pollable {
    void poll();
    boolean isReachable();
}

public interface Configurable {
    void applyConfig(String configBlock);
    String getCurrentConfig();
}

public class ManagedRouter implements Pollable, Configurable {

    private String name;

    public ManagedRouter(String name) {
        this.name = name;
    }

    public void poll() {
        System.out.println("Polling " + name);
    }

    public boolean isReachable() {
        return true; // ICMP echo check
    }

    public void applyConfig(String configBlock) {
        // push config via Telnet
    }

    public String getCurrentConfig() {
        return ""; // retrieve from device
    }
}

Now your monitoring component depends only on Pollable. Your configuration manager depends only on Configurable. A ManagedRouter satisfies both.

The Gang of Four book (Gamma et al., 1994) puts it plainly: program to an interface, not an implementation. This is the principle that makes codebases extensible without surgery.

Encapsulation in Practice

Make every field private by default. Write getters and setters explicitly. This feels like ceremony at first.

The value appears when requirements change. Our device status field is currently an int — 0 for up, 1 for down. Next quarter we need UNKNOWN, DEGRADED, CRITICAL and DOWN as distinct states. Because status is private with a getStatus() accessor, changing the representation is a single-class change. If status were public and read in 30 places across the codebase, it would be a refactoring session.

Design for change. Encapsulation is how you contain it.

Things That Catch People Out

No generics. Java 1.1 collections hold Object. Every retrieval requires a cast. This causes ClassCastException at runtime if you are not careful. Be disciplined about what types go into each collection and document it.

// This compiles fine, explodes at runtime
Vector devices = new Vector();
devices.addElement("not a device");
NetworkDevice d = (NetworkDevice) devices.elementAt(0); // ClassCastException

Heap allocation for everything. Objects in Java are always on the heap. The garbage collector handles deallocation but the current JVM implementations have stop-the-world pause times that can be hundreds of milliseconds. For our near-real-time monitoring work this is a real concern. We are benchmarking carefully.

Checked exceptions. You must either catch or declare every checked exception. This produces verbose code and — worse — encourages the pattern of catching exceptions and doing nothing:

try {
    device.poll();
} catch (IOException e) {
    // TODO handle this properly
}

Do not do this. Every swallowed exception is a future debugging nightmare.

The Bottom Line

Six months into serious Java development at Motorola, the OOP model is proving its worth. Team members cannot accidentally trample each other's internal state the way they could with shared C structs. Polymorphism through interfaces lets us add new device types without changing the monitoring engine. The codebase is growing and staying coherent.

The JVM performance headroom versus native C is still a valid concern for certain workloads. But for complex, team-built systems, the discipline Java's object model enforces is a genuine productivity gain. Take the time to understand what objects are actually for — the investment repays itself quickly.