Methods: Passing & Returning References of an Object

Objects as Instance Variables (Has-A)

A class's instance variable can itself be an object of another class — a has-a relationship. A Subsystem might have a MotorConfig (its tuning settings) as an instance variable:

class MotorConfig
{
    private double rampRate;
    private int currentLimit;
    // constructor, getters, setters not shown
}

class Subsystem
{
    private String name;
    private MotorConfig config; // instance variable of type MotorConfig
}

One public class per file. A .java file can hold several classes (like MotorConfig and Subsystem above), but at most one of them can be public, and (with a normal javac compile) a public class's name must match the file name exactly (Subsystem must live in Subsystem.java) — that's a compile error otherwise, not just a style rule. A class with no modifier at all (like both shown here) is fine too — see 7.4 for what that means for access. When a file has a main method, put the class containing it first in the file: running it directly with java File.java (no separate compile step, 1.2) runs the first class's main, not necessarily the one matching the file name — true on both Java 25 and Java 17, so this one rule covers both.

Call by Value, for Object References

Java always uses call by value — but for an object, the value being copied is the reference (the pointer to the object), not the object itself. Passing a MotorConfig into a method copies the reference — the parameter and the argument then both point at the same object.

public Subsystem(String initName, MotorConfig initConfig)
{
    name = initName;
    config = initConfig; // stores the SAME reference the caller passed in
}

If MotorConfig is mutable (has setters), this matters a lot: whoever else holds a reference to that same MotorConfig object can change it, and the Subsystem sees the change too — because it's not a separate copy, it's the same object viewed from two places.

Defensive Copying

If you don't want outside code to be able to reach in and change your object's data after the fact, make a copy instead of storing the reference you were handed:

public Subsystem(String initName, MotorConfig initConfig)
{
    name = initName;
    // Defensive copy — a brand new MotorConfig with the same values,
    // not the same object
    config = new MotorConfig(initConfig.getRampRate(), initConfig.getCurrentLimit());
}

Now changing the original initConfig object afterward has zero effect on Subsystem's own copy.

Notice name (a String) needed no defensive copy above — String is immutable (Ch.6), so there's no "reaching in and changing it later" to defend against; only a mutable object reference (like MotorConfig, which has setters) needs one. Also watch for a null argument: initConfig.getRampRate() throws a NullPointerException if initConfig itself is null — a defensive copy still has to handle (or explicitly rule out) that case.

Returning an Object Reference

The same rule applies in reverse: a method that returns an object returns a reference to the real object, not a copy. If that object is mutable, whoever calls the getter can reach in and change it:

public MotorConfig getConfig()
{
    return config; // returns the SAME object — caller can mutate it!
}
MotorConfig c = subsystem.getConfig();
c.setRampRate(0.5); // this actually changes subsystem's real config!

If that's not what you want, return a defensive copy from the getter too.

Chaining Method Calls

Calling a method directly on another method's return value — subsystem.getConfig().getRampRate() — is called chaining. It only compiles if the first call's declared return type actually has the second method — getConfig()'s return type is MotorConfig, and MotorConfig has a getRampRate(), so the chain is valid; the compiler checks this, not the programmer.

Common Pitfalls

  • Assuming storing a passed-in object automatically makes an independent copy. It doesn't — only the reference is copied, so the stored object is the exact same one the caller still has.
  • Returning a mutable instance variable directly from a getter. This hands the caller a live reference to your object's actual internal state — they can change it without going through any of your class's own methods.
  • Chaining calls without checking each return type. a.getX().getY() only compiles if getX()'s return type actually has a getY() method.

Key Takeaways

  • An object can be another object's instance variable — a "has-a" relationship.
  • Passing or returning an object copies its reference, not the object itself — both sides then point at the same object.
  • If the object is mutable and you don't want outside code able to change it later, make a defensive copy — on the way in (constructor/setter) and/or on the way out (getter).
  • Chaining method calls (a.getX().getY()) compiles only if each call's declared return type actually has the next method in the chain.
  • A file can hold several classes but only one public one; put the class with main first so java File.java runs it, on both Java 25 and Java 17.

Derived from csawesome-2026: unit-3-class-creation/3.06-methods-passing-and-returning-references-of-an-object.md (CSA 3.6)