Unit 2 · Lesson 6

Methods

You've been using methods since Lesson 1 — teleopPeriodic(), robotInit(), and motor.set() are all methods. This lesson is about writing your own: naming them well, giving them the right parameters, and using them to keep periodic code readable enough to debug at competition under pressure.

By the end of this lesson, you will:

  • Write methods with correct syntax — access modifier, return type, name, parameters, and body
  • Distinguish between void methods that perform actions and methods that compute and return a value
  • Pass arguments to methods and explain why primitive arguments are passed by value
  • Recognize method overloading and identify which overload Java selects at a given call site
  • Refactor a bloated periodic method into focused helper methods, and explain why this matters for competition debugging

You've Already Written Methods

Every time you wrote public void teleopPeriodic() { ... }, you defined a method. WPILib calls it. Every time you wrote motor.set(0.6), you called a method that someone else defined. The syntax you've been writing since Unit 1 is method syntax — this lesson is about understanding it fully and writing your own from scratch.

In robot code, methods do two things: they organize your logic into named, reusable chunks, and they make teleopPeriodic() readable to someone who has never seen your robot. A 200-line periodic method that does everything is a debug nightmare at 7 AM before eliminations. A periodic method with six well-named helper calls is readable in ten seconds.

Anatomy of a Method

A Java method has up to six distinct parts. Click each highlighted section of the method below to learn what it does and why each choice matters in robot code.

Method Anatomy Explorer — click any highlighted part
private access double return type calculateOutput name ( double targetDeg, double currentDeg parameters ) {
double error = targetDeg - currentDeg;
return ARM_KP * error; return statement
}
← click any highlighted part to learn what it means

void Methods vs. Methods That Return a Value

Every method either returns a value or it doesn't. This single design decision shapes how the method is used at the call site and what it's responsible for.

A void method performs an action — it changes state, sends a command to hardware, logs something, or updates a variable. It produces no value that the caller can use. The caller just runs it and moves on.

Most of the WPILib lifecycle methods you've written are void: they don't return anything to WPILib, they just do work. Motor commands, dashboard updates, and state changes are all void work.

// void: sends a command to the arm motor — no return value needed private void setArmTarget(double angleDeg) { armController.setSetpoint(angleDeg); armMotor.set(armController.calculate(armEncoder.getPosition())); } // void: updates the dashboard — caller doesn't need a result private void updateDashboard() { SmartDashboard.putNumber("Arm Angle", armEncoder.getPosition()); SmartDashboard.putBoolean("At Target", isAtTarget()); SmartDashboard.putString("Arm State", currentState.name()); } // Calling void methods — statement on its own line, no assignment setArmTarget(SCORE_ANGLE_DEG); updateDashboard();

Notice that calling a void method is a statement on its own — you can't assign it to a variable because there's nothing to assign. double x = updateDashboard() won't compile.

A method with a declared return type computes and sends back a value. The caller can use that value in an expression, assign it to a variable, or pass it directly to another method. The return keyword is mandatory — a non-void method that reaches the end without returning won't compile.

// Returns a double — caller uses the result in an if condition private double getArmError() { return targetAngleDeg - armEncoder.getPosition(); } // Returns a boolean — caller uses it directly as a condition private boolean isAtTarget() { return Math.abs(getArmError()) < TOLERANCE_DEG; } // Returns a String — caller passes it to SmartDashboard private String getStatusLabel() { return isAtTarget() ? "AT TARGET" : "MOVING"; } // Calling return-type methods — assign or use inline double error = getArmError(); // assign to variable if (isAtTarget()) { unlatchGamePiece(); } // use directly in condition SmartDashboard.putString("Status", getStatusLabel()); // pass to another call

A common beginner mistake: writing a helper that should return a value but is declared void, so the caller has no way to use the result. If a method computes something a caller needs, it must return it.

Parameters and Arguments

Parameters are the named inputs declared in the method signature — they're local variables that exist only inside the method body. Arguments are the actual values passed in at the call site. The distinction matters for talking about code with teammates: "the method has two parameters" vs. "I called it with these two arguments."

// "targetDeg" and "currentLimitAmps" are PARAMETERS — they name the inputs private void configureMotor(TalonFX motor, int currentLimitAmps) { TalonFXConfiguration config = new TalonFXConfiguration(); config.CurrentLimits.StatorCurrentLimit = currentLimitAmps; config.CurrentLimits.StatorCurrentLimitEnable = true; motor.getConfigurator().apply(config); } // driveMotor and DRIVE_CURRENT_LIMIT are ARGUMENTS — the actual values passed in configureMotor(driveMotor, DRIVE_CURRENT_LIMIT); configureMotor(steerMotor, STEER_CURRENT_LIMIT); // reused with different args configureMotor(intakeMotor, INTAKE_CURRENT_LIMIT); // reused again

This is the core value of parameters: the method is written once and called with different arguments. Without parameters, you'd write three nearly identical motor configuration blocks — three places to get out of sync, three places to update when a setting changes.

Pass-by-value for primitives

Java passes copies of primitive arguments into methods. Modifying the parameter inside the method has no effect on the original variable at the call site. This is one of the most common source of confusion for new Java programmers.

// The method modifies its local copy of "speed" — the original is unchanged private void clampAndSet(double speed) { speed = Math.max(-1.0, Math.min(1.0, speed)); // clamps the local copy motor.set(speed); } double requestedSpeed = 1.5; // over the limit clampAndSet(requestedSpeed); // motor gets 1.0 (clamped inside method) System.out.println(requestedSpeed); // still 1.5 — original unchanged
💡 Objects are different

Primitive arguments (double, int, boolean) are passed by value — the method gets a copy. Object arguments (TalonFX, TalonFXConfiguration, arrays) are passed by reference — the method gets the same object the caller has. Modifying an object's fields inside a method does affect the original. This is why configureMotor(driveMotor, LIMIT) actually configures the real motor object — not a copy of it.

The Refactor: From Spaghetti to Structure

This is the most important skill in this lesson. A well-named method is documentation. A bloated periodic method is a maintenance liability. The tabs below show the same robot logic before and after a method extraction — compare the readability.

This is real code structure that appears in competition robots. Every new feature added to this file made the problem worse. Reading it cold at 6 AM before elimination rounds is its own challenge.

@Override public void teleopPeriodic() { // ── Shooter ─────────────────────────────────────────── double shooterError = targetShooterRPM - shooter.getVelocity().getValueAsDouble(); double shooterOutput = SHOOTER_KP * shooterError; shooterOutput = Math.max(-1.0, Math.min(1.0, shooterOutput)); if (Math.abs(shooterError) < SHOOTER_TOLERANCE) { shooterAtSpeed = true; } if (driverController.getRightBumper()) { shooter.set(shooterOutput); } else { shooter.set(0.0); shooterAtSpeed = false; } // ── Intake ──────────────────────────────────────────── if (driverController.getLeftBumper() && !gamePieceHeld) { intakeMotor.set(INTAKE_SPEED); } else if (beamBreak.get()) { intakeMotor.set(0.0); gamePieceHeld = true; } else if (!driverController.getLeftBumper()) { intakeMotor.set(0.0); } // ── Arm ─────────────────────────────────────────────── double armError = targetArmDeg - armEncoder.getPosition(); double armOutput = ARM_KP * armError; armOutput = Math.max(-ARM_MAX_OUTPUT, Math.min(ARM_MAX_OUTPUT, armOutput)); if (Math.abs(armError) < ARM_TOLERANCE) { armAtTarget = true; } if (driverController.getAButton()) { targetArmDeg = SCORE_ANGLE_DEG; } else if (driverController.getBButton()) { targetArmDeg = INTAKE_ANGLE_DEG; } else { targetArmDeg = STOW_ANGLE_DEG; } armMotor.set(armOutput); // ── Dashboard ───────────────────────────────────────── SmartDashboard.putNumber("Shooter RPM", shooter.getVelocity().getValueAsDouble()); SmartDashboard.putBoolean("At Speed", shooterAtSpeed); SmartDashboard.putNumber("Arm Angle", armEncoder.getPosition()); SmartDashboard.putBoolean("Arm Target", armAtTarget); SmartDashboard.putBoolean("Has Piece", gamePieceHeld); }

The behavior is identical. The periodic method now reads like a summary of what the robot does each cycle. Each helper method is short enough to understand on its own, and short enough to unit test independently.

@Override public void teleopPeriodic() { updateShooter(); // reads like a checklist — one glance to understand updateIntake(); updateArm(); updateDashboard(); } // ── Each helper is focused, named, and independently readable ────── private void updateShooter() { if (!driverController.getRightBumper()) { shooter.set(0.0); shooterAtSpeed = false; return; } double error = targetShooterRPM - shooter.getVelocity().getValueAsDouble(); shooterAtSpeed = Math.abs(error) < SHOOTER_TOLERANCE; shooter.set(clamp(SHOOTER_KP * error, -1.0, 1.0)); } private void updateIntake() { if (beamBreak.get()) { intakeMotor.set(0.0); gamePieceHeld = true; return; } if (driverController.getLeftBumper() && !gamePieceHeld) { intakeMotor.set(INTAKE_SPEED); return; } intakeMotor.set(0.0); } private void updateArm() { if (driverController.getAButton()) targetArmDeg = SCORE_ANGLE_DEG; else if (driverController.getBButton()) targetArmDeg = INTAKE_ANGLE_DEG; else targetArmDeg = STOW_ANGLE_DEG; double error = targetArmDeg - armEncoder.getPosition(); armAtTarget = Math.abs(error) < ARM_TOLERANCE; armMotor.set(clamp(ARM_KP * error, -ARM_MAX_OUTPUT, ARM_MAX_OUTPUT)); } private void updateDashboard() { SmartDashboard.putNumber("Shooter RPM", shooter.getVelocity().getValueAsDouble()); SmartDashboard.putBoolean("At Speed", shooterAtSpeed); SmartDashboard.putNumber("Arm Angle", armEncoder.getPosition()); SmartDashboard.putBoolean("Arm Target", armAtTarget); SmartDashboard.putBoolean("Has Piece", gamePieceHeld); } // Reusable utility — called from multiple helpers private double clamp(double value, double min, double max) { return Math.max(min, Math.min(max, value)); }

The clamp() method at the bottom is called from both updateShooter() and updateArm(). One implementation, tested once, used everywhere. If you ever need to change the clamping logic — adding a deadband, logging when the value is out of range — you change it in one place.

🔍 LRI Observation

I've reviewed robot code for hundreds of teams as an LRI and a mentor. The teams that can debug a problem in two minutes at competition aren't necessarily the ones with the most sophisticated control theory — they're the ones whose periodic methods read like English. When a mechanism stops working, the programmer looks at the four-line periodic, identifies the relevant helper, reads twenty lines of focused code, and finds the bug. On the teams with 200-line periodic methods, the programmer is still scrolling when the match starts.

Method Overloading

Java allows multiple methods to share the same name as long as their parameter lists differ — in the number, type, or order of parameters. Java uses the argument types at the call site to pick the right version at compile time. This is called overloading.

The return type alone is not enough to overload — two methods that differ only in return type won't compile. The difference must be in the parameters.

configureMotor(TalonFX, int)

Standard configuration with a specified current limit. Used for any motor where you want explicit control over the limit.

configureMotor(driveMotor, 50);
configureMotor(TalonFX)

Convenience overload using the class-wide default limit. Shorter call for the common case, delegates to the full version.

configureMotor(intakeMotor);
configureMotor(TalonFX, int, boolean)

Full configuration with an additional inverted flag for motors that are physically mounted backwards.

configureMotor(rightMotor, 50, true);
// Three overloads of the same method — Java picks the right one at compile time private void configureMotor(TalonFX motor, int currentLimitAmps, boolean inverted) { TalonFXConfiguration cfg = new TalonFXConfiguration(); cfg.CurrentLimits.StatorCurrentLimit = currentLimitAmps; cfg.CurrentLimits.StatorCurrentLimitEnable = true; cfg.MotorOutput.Inverted = inverted ? InvertedValue.Clockwise_Positive : InvertedValue.CounterClockwise_Positive; motor.getConfigurator().apply(cfg); } // Overload 2: no inverted flag — defaults to false private void configureMotor(TalonFX motor, int currentLimitAmps) { configureMotor(motor, currentLimitAmps, false); // delegates to the full version } // Overload 3: no limit arg — uses class constant default private void configureMotor(TalonFX motor) { configureMotor(motor, DEFAULT_CURRENT_LIMIT, false); // delegates again }

The delegation pattern — shorter overloads calling the full-parameter version — means there's only one implementation of the actual configuration logic. When the electrical team changes the drive current limit, one number changes in one place and every configuration call that uses the default picks it up automatically.

Static Methods: Utilities That Don't Need an Object

A static method belongs to the class rather than to any specific instance of it. You call it using the class name, not an object reference. You've already used static methods throughout this course: Math.abs(), Math.min(), Math.max(), and SmartDashboard.putNumber() are all static.

// Static methods — called on the class, no object needed double clamped = Math.max(-1.0, Math.min(1.0, rawOutput)); double absError = Math.abs(targetAngle - currentAngle); // Writing your own static utility method public static double applyDeadband(double input, double threshold) { if (Math.abs(input) < threshold) { return 0.0; } return input; } // Calling a static method on the class — no "this" needed double y = Robot.applyDeadband(controller.getLeftY(), 0.05);

If a method doesn't need to read or write any instance variables — it just transforms inputs to outputs using its parameters — it's a good candidate to be static. Math utilities, unit converters, and string formatters all fit this pattern. Making them static signals to readers that the method has no side effects on the object's state.

💡 WPILib already has many utilities

Before writing your own utility methods, check edu.wpi.first.math.MathUtil. It provides MathUtil.clamp(), MathUtil.applyDeadband(), MathUtil.angleModulus(), and more — all tested, documented, and used by thousands of teams. Your clamp() method from the refactor example is already there. In Unit 5, you'll use these directly when building your first drive code.

One Job Per Method

The most important design principle for robot code methods isn't a Java syntax rule — it's a discipline. A method should do one thing, named clearly enough that the method name is its own comment.

Ask yourself: can you describe what this method does without using the word "and"? If the answer is "it updates the shooter and logs to the dashboard and checks for faults," the method should be split. If the answer is "it calculates the P-term output for the arm," it's well-scoped.

// ❌ Too broad — does three unrelated things private void doShooterStuff() { shooter.set(calculateShooterOutput()); SmartDashboard.putBoolean("At Speed", shooterAtSpeed); if (shooter.getFault_Hardware().getValue()) { triggerFault(); } } // ✅ Three focused methods — each tells you exactly what it does private void commandShooter() { shooter.set(calculateShooterOutput()); } private void publishShooterData() { SmartDashboard.putBoolean("At Speed", shooterAtSpeed); } private void checkShooterFaults() { if (shooter.getFault_Hardware().getValue()) triggerFault(); }
🔍 Event Observation

During a pit debugging session I watched a student hunt for why their shooter was cutting out. The method in question was called handleShooter(). It did seven things. The fault-check logic — three conditions nested two levels deep — was in the middle of the motor-command logic. It took eleven minutes to isolate the relevant four lines. When they renamed and split the method, the bug became visible in under thirty seconds. The code was exactly the same; the structure was the difference. At competition, eleven minutes is usually longer than the time between your queue call and your match start.

🔌 System Check

⚙️ Methods and Robot Maintainability
  • teleopPeriodic() should read like a table of contents. If your periodic method has more than ten non-trivial lines, extract helpers. The reader should understand what the robot does in each cycle by reading method names, not implementation details.
  • Every helper that touches hardware should be named for the mechanism and the action. updateArm(), commandShooter(), checkIntakeFaults() — not doStuff(), helper1(), or periodicLogic().
  • Pure computation methods (no hardware side effects) should be private static. A method that just does math is easiest to reason about and easiest to test when it's static. It signals clearly: this has no side effects.
  • Use overloading for convenience, delegation for correctness. Shorter overloads should call the full-parameter version — not duplicate logic. Duplicated logic in two overloads means two places to update when a hardware requirement changes.
  • Check MathUtil before writing a utility from scratch. Clamp, deadband, angle modulus, and interpolation are all in WPILib. Use the tested version.
  • Parameters that come directly from constants don't need to be parameters. A method that only ever receives SCORE_ANGLE_DEG for a particular argument doesn't need that parameter — just reference the constant directly. Parameters are for values that genuinely vary at the call site.

Knowledge Check

Click an answer to check your understanding.

A method declared private double calculateError() reaches the end of its body without a return statement. What happens?
  • 1Java returns 0.0 automatically for any numeric return type
  • 2The method runs normally and the caller receives null
  • 3The code does not compile — Java requires every code path in a non-void method to end with a return statement of the declared type
  • 4The method compiles but throws a RuntimeException when called
A method receives a double speed parameter and modifies it inside the body with speed *= 0.5. After the method returns, the caller's double requestedSpeed variable that was passed as the argument — what is its value?
  • 1Half of the original value — the method modified the caller's variable
  • 2The original value, unchanged — Java passes primitives by value, so the method received a copy; modifying speed inside the method has no effect on requestedSpeed at the call site
  • 3Zero — modifying a parameter inside a method always resets the original to zero
  • 4A compile error — you cannot assign to a method parameter in Java
A class has two methods: void configure(TalonFX motor, int limit) and void configure(TalonFX motor). A third programmer tries to add double configure(TalonFX motor, int limit). What does Java do?
  • 1Java accepts it — different return types are enough to overload a method
  • 2Java rejects it — the return type alone is not enough to distinguish overloads; this signature has the same parameter list as the first void version, so it is a duplicate and won't compile
  • 3Java accepts it and uses the return type at the call site to choose which version to call
  • 4Java replaces the original void version silently
💪 Practice Prompt

Extract, Name, and Overload

The following teleopPeriodic() method is functional but structured in a way that will cause debugging problems at competition. Refactor it completely.

@Override public void teleopPeriodic() { // Climber logic if (driverController.getYButton()) { climberMotor.set(0.6); } else if (driverController.getAButton()) { climberMotor.set(-0.4); } else { climberMotor.set(0.0); } // Intake logic double intakeOutput = driverController.getRightBumper() ? 0.75 : 0.0; if (intakeMotor.getStatorCurrent().getValueAsDouble() > 60.0) { intakeOutput = 0.0; } intakeMotor.set(intakeOutput); // Dashboard SmartDashboard.putNumber("Climber Amps", climberMotor.getStatorCurrent().getValueAsDouble()); SmartDashboard.putNumber("Intake Amps", intakeMotor.getStatorCurrent().getValueAsDouble()); SmartDashboard.putBoolean("Intake Running", intakeOutput > 0.0); }
  1. Extract the climber logic, intake logic, and dashboard updates into three separate private void helper methods with names that clearly describe what each does. Replace the body of teleopPeriodic() with three calls.
  2. Replace every magic number (0.6, -0.4, 0.75, 60.0) with named static final constants declared at the top of the class. Add a comment to each constant explaining what physical constraint it represents.
  3. Extract the stall-current guard (the if > 60.0 block) into a private boolean isStalled(TalonFX motor, double limitAmps) method that can be reused for both the climber and the intake. Show how you call it in both helpers.
  4. Write an overloaded version private boolean isStalled(TalonFX motor) that uses a class constant DEFAULT_STALL_AMPS as the limit, and delegates to the full version. Add a call to this overload in one of your helpers.
  5. Bonus: Write a private static double clamp(double value, double min, double max) utility and use it in both the climber and intake helpers to cap outputs. Then look up MathUtil.clamp() in the WPILib documentation and compare the signatures. What is different, and which would you use on a competition robot?