Unit 4 · Lesson 7

Abstract Classes and Interfaces

Abstract classes and interfaces are both ways of defining a contract — a set of methods a class must provide — without implementing every method themselves. WPILib uses both extensively: SubsystemBase is an abstract class, Sendable is an interface. Knowing when to use each is the practical skill.

By the end of this lesson, you will:

  • Write an abstract class with abstract methods that subclasses must implement
  • Define an interface and implement it in a concrete class
  • Explain the key differences: abstract classes have state and partial implementation; interfaces are pure contracts (with optional defaults)
  • Implement common WPILib interfaces: Sendable, Supplier, Consumer
  • Choose between abstract class and interface for a given FRC design problem

The Problem They Both Solve

Inheritance from Lesson 5 lets you share concrete implementations. But sometimes you want to define a contract — "every class that claims to be one of these must provide this method" — without dictating how. You want the type guarantee without forcing a specific implementation.

Abstract classes and interfaces both solve this, but with different constraints. The quick mental model: an abstract class is a partially-built class; an interface is a to-do list.

Abstract Classes

An abstract class is a class that cannot be instantiated directly — you can only instantiate its concrete subclasses. It can contain a mix of concrete methods (with implementation) and abstract methods (with no body — subclasses must provide it). Abstract classes can have fields, constructors, and any access modifier.

// Abstract class — cannot do: new MotorSubsystem() public abstract class MotorSubsystem { protected final TalonFX motor; // concrete field public MotorSubsystem(int canId) { // concrete constructor this.motor = new TalonFX(canId); } // Concrete method — shared implementation public void stop() { motor.set(0.0); } // Abstract method — subclasses MUST provide this public abstract void periodic(); // Abstract method — subclasses MUST provide this public abstract double getOutput(); } // Concrete subclass — must implement all abstract methods public class IntakeSubsystem extends MotorSubsystem { public IntakeSubsystem() { super(Constants.INTAKE_MOTOR_ID); } @Override public void periodic() { /* update beam break, dashboard */ } @Override public double getOutput() { return motor.get(); } }

Interfaces

An interface is a pure contract — a list of methods a class promises to provide. A class can implement multiple interfaces, while it can only extend one class. Interface methods are implicitly public abstract unless marked default (which provides a fallback implementation).

// Interface definition — implicitly public abstract methods public interface Stoppable { void stop(); // must be implemented boolean isStopped(); // must be implemented // Default method — optional to override default void emergencyStop() { stop(); System.out.println("Emergency stop triggered"); } } // Class implementing MULTIPLE interfaces (not possible with extends) public class IntakeSubsystem extends SubsystemBase implements Stoppable, Loggable { @Override public void stop() { motor.set(0.0); running = false; } @Override public boolean isStopped() { return !running; } @Override public void log() { SmartDashboard.putBoolean("Intake", running); } }

WPILib relies heavily on functional interfaces — interfaces with a single abstract method — for callbacks and suppliers. You'll use these constantly once you reach Command-Based programming.

// Supplier<T> — returns a value of type T, no input Supplier<Double> speedSource = () -> driverController.getLeftY(); double speed = speedSource.get(); // Consumer<T> — accepts a value of type T, no return Consumer<Double> motorSetter = (output) -> motor.set(output); motorSetter.accept(0.6); // BooleanSupplier — returns boolean, used for Command triggers BooleanSupplier shootReady = () -> shooter.isAtSpeed() && !hasFault(); // Runnable — takes no input, returns nothing, runs a block Runnable resetOdometry = () -> drive.resetPose(new Pose2d()); // In Command-Based, triggers and bindings use these constantly: // new JoystickButton(controller, 1).whileTrue(new RunIntake(intake)); // The trigger is essentially a BooleanSupplier

Abstract Class vs. Interface: The Decision

Both tools define contracts. The choice between them comes down to whether you need shared state, shared implementation, or multiple inheritance. Select any scenario to see which tool fits and why.

Abstract Class

Use when subclasses share state and partial implementation.

  • Can have instance fields and constructors
  • Can mix abstract and concrete methods
  • A class can only extend one
  • Models an is-a relationship with shared scaffolding
  • FRC: SubsystemBase, CommandBase, your MotorSubsystem
Interface

Use when you need a capability contract across unrelated classes.

  • No instance fields (constants only)
  • All methods public (abstract by default)
  • A class can implement many
  • Models a can-do relationship
  • FRC: Sendable, Supplier, Consumer, BooleanSupplier
Abstract vs. Interface Decision Tool click to see the reasoning
01 You want all motor subsystems to share a TalonFX field and a stop() implementation, but each must provide its own periodic() logic
02 You want subsystems to be able to send their data to Shuffleboard, but the subsystems are otherwise unrelated
03 You need a Command that accepts "any callable that takes no args and returns nothing"
04 You want a base class that all auto-aiming commands share, but different mechanisms aim differently
Abstract Class

Interface

⚙️ 🔌 System Check
  • Use abstract classes when subclasses share state. If every subsystem variant needs a TalonFX, a current limit check, and a stop() method, an abstract class centralizes that. Subclasses provide the mechanism-specific behavior through abstract method implementations.
  • Use interfaces for capabilities that cross class hierarchies. Sendable is implemented by motors, sensors, subsystems, and controllers — objects with nothing else in common. The interface defines only what they can do, not what they are.
  • Functional interfaces (Supplier, Consumer, Runnable) enable lambda expressions. Any method that accepts a BooleanSupplier can receive a lambda (() -> sensor.get()). This is the foundation of Command-Based trigger bindings.
  • Never instantiate an abstract class directly. If a class is abstract, you need a concrete subclass. The compiler enforces this — new MotorSubsystem(5) is a compile error if MotorSubsystem is abstract.

Knowledge Check

A class is declared public abstract class BaseAuto { public abstract void run(); }. You write new BaseAuto(). What happens?
  • 1A BaseAuto is created with an empty run() method
  • 2A compile error — abstract classes cannot be instantiated directly; you must create a concrete subclass that implements run()
  • 3A runtime exception when run() is called
  • 4Works fine — abstract is just a hint that a method should be overridden
Why can a class implement multiple interfaces but only extend one class?
  • 1Interfaces are smaller so Java allows multiples
  • 2Multiple class inheritance creates the "diamond problem" — ambiguity about which superclass method to call when both parents have the same method; interfaces (with no concrete state) avoid this because method bodies are in the implementing class, not competing inherited classes
  • 3It's a Java performance optimization
  • 4Interfaces are compiled differently and don't count toward inheritance limits
WPILib's BooleanSupplier is a functional interface with one method: boolean getAsBoolean(). A method accepts a BooleanSupplier. Which can you pass?
  • 1Only a class that explicitly declares implements BooleanSupplier
  • 2Any lambda or method reference that takes no arguments and returns a boolean — e.g. () -> shooter.isAtSpeed() — because functional interfaces accept lambda expressions that match their single abstract method signature
  • 3Only a boolean value
  • 4Only anonymous inner classes
💪 Practice Prompt

Design a Subsystem Contract

  1. Convert your MotorSubsystem from Lesson 5 to an abstract class. Make periodic() and getOutput() abstract. Verify the compiler rejects a direct instantiation.
  2. Create a Loggable interface with a single void log() method. Implement it in IntakeSubsystem and ClimberSubsystem. Each implementation should call SmartDashboard.putNumber() with subsystem-specific data.
  3. Write a logAll(Loggable[] loggables) static method in a utility class. Pass it an array containing your two subsystems and verify each one logs its own data.
  4. Use a lambda to create a BooleanSupplier that returns true when the intake has a game piece. Pass it to a method that accepts BooleanSupplier and prints its result every second.
  5. Bonus: Find WPILib's Sendable interface. What is its single abstract method? What does implementing it allow your subsystem to do? How does SubsystemBase already implement Sendable on your behalf?