Advanced Classes
enums, nested classes, ArrayLists, and wrapper classes. patterns you'll see constantly in real robot code :)
Enums
we use enums constantly on 2974. robot states, game piece types (whatever object robots pick up and score in that year's FRC game), scoring positions, arm positions — all enums. if you forget this week, you won't be able to read our codebase. ngl this is one of the most important weeks.
before we get into syntax, let's talk about the problem enums solve, because the "why" here is way more important than the "how."
the enum mental model
imagine you're at a restaurant. you can't just walk up to the counter and say "i want a unicorn steak with a side of moon rocks." you have to order from the menu. the menu is a fixed list of valid options — you pick one of those, nothing else. an enum is the menu. you define the exact options upfront, and Java will only allow those options. nothing else compiles. nothing else runs.
that sounds limiting, but it's the entire point. before enums existed, people tracked robot state with strings or integers. string "TELEOP" works great — until someone types "Teleop" (capital T) or "TELE0P" (zero instead of an O). Java doesn't catch either of those. the robot just behaves wrong and you spend an hour debugging a typo. an integer 0 for disabled, 1 for teleop, 2 for auto works great — until someone passes 7 and you have no idea what that means. enum fixes all of this: only the exact declared values are valid, and the compiler checks it, not you at runtime.
arm positions, drivetrain modes, game piece types, scoring locations, LED states — all of these are "pick from a list" problems. the codebase uses enums for basically all of them. if you see a field declared as ArmState m_state in a WRT subsystem, that's an enum. if you see a switch statement checking m_state, that's an enum. this is the most foundational pattern in how we structure behavior.
the String problem in detail: say you track robot mode with String m_mode = "TELEOP". three weeks later, your teammate writes a condition: if (m_mode == "Teleop") — different capitalization. Java won't warn you. the condition is always false. the robot runs teleop code but your condition never triggers. good luck finding that bug at midnight before a competition. enum makes this literally impossible to compile.
declaring an enum
you use the enum keyword instead of class. the values inside are written in SCREAMING_SNAKE_CASE by convention — they're constants, so they get the constant treatment. the name of the enum follows PascalCase like any other type.
// declare the enum — like a class but for a fixed set of values public enum RobotState { DISABLED, TELEOP, AUTO, TEST } // using it — type is RobotState, value must be one of the four RobotState state = RobotState.TELEOP; // comparing with == — no .equals() needed for enums! if (state == RobotState.TELEOP) { System.out.println("running teleop"); } // this won't even compile — that's the whole point // RobotState bad = "TELEOP"; <-- compile error, good! // RobotState bad = 1; <-- compile error, also good!
why SCREAMING_SNAKE_CASE? enum values are implicitly static final constants — they're created once and never change. Java convention says constants should be all caps with underscores. so INTAKE_POSITION, SCORE_HIGH, FIELD_RELATIVE — not intakePosition or scoreHigh. it visually signals "this is a fixed constant, not a variable."
enums + switch statements (this combo is chef's kiss)
enums and switch statements are designed for each other. you switch on the enum value, and Java can warn you if you miss a case — it knows exactly how many possible values the enum has. compare that to switching on a string or integer where the compiler has no idea how many valid values exist.
RobotState state = RobotState.TELEOP; switch (state) { case TELEOP: runTeleopCode(); break; case AUTO: runAutoCode(); break; case DISABLED: stop(); break; default: stop(); }
key syntax detail: inside the switch you just write case TELEOP, not case RobotState.TELEOP. Java already knows the type from the switch expression — the enum name is redundant and leaving it out is correct style. outside a switch, you always need the full RobotState.TELEOP.
the "just use a String" gotcha (don't do this)
this is a mistake beginners make when they think "eh, what's the difference, a string works fine." let's show exactly what goes wrong. here's the fragile string version versus the correct enum version side by side:
// BAD: nothing stops invalid values from getting in private String m_mode = "IDLE"; public void setMode(String mode) { m_mode = mode; // what if someone passes "idle"? or "IDEL"? or "auto "? // no error, no warning. silent bug. } if (m_mode == "TELEOP") { // comparing strings with == is wrong anyway runTeleop(); // this might never run even if mode IS "TELEOP" }
// GOOD: the compiler enforces every value public enum Mode { IDLE, TELEOP, AUTO, DISABLED } private Mode m_mode = Mode.IDLE; public void setMode(Mode mode) { m_mode = mode; // only valid Mode values can be passed — period } if (m_mode == Mode.TELEOP) { // == works correctly for enums runTeleop(); }
enums can have methods
this is where enums get really interesting — they're actually full classes under the hood. you can add fields, constructors, and methods to them. the most common pattern is a boolean helper method directly on the enum itself, so the logic lives right where the values are defined.
public enum RobotState { DISABLED, TELEOP, AUTO, TEST; // semicolon required when you add methods // helper method — call as state.isActive() public boolean isActive() { return this == TELEOP || this == AUTO || this == TEST; } // another useful pattern: human-readable label for logging public String getLabel() { switch (this) { case TELEOP: return "Teleop"; case AUTO: return "Autonomous"; case DISABLED: return "Disabled"; default: return "Test"; } } } // usage RobotState state = RobotState.TELEOP; if (state.isActive()) { System.out.println("Robot is running: " + state.getLabel()); // "Robot is running: Teleop" }
enums in FRC — the WRT state machine pattern
on 2974, enums power our state machines (a state machine tracks what "mode" the robot is in — like INTAKING, SHOOTING, or IDLE — and behaves differently in each). a subsystem has a "current state" (stored as an enum field), and the periodic loop checks that state and runs the right code. this replaces spaghetti if/else chains with a clean, readable, extensible pattern. when we need to add a new state, we add one value to the enum — we don't hunt through a chain of conditions trying to figure out where to insert a new branch.
public enum ArmState { STOWED, INTAKING, SCORING, CLIMBING } public class ArmSubsystem extends SubsystemBase { // current state lives as a field — m_ prefix because it's a member var private ArmState m_state = ArmState.STOWED; // command calls this to request a state change public void setState(ArmState newState) { m_state = newState; } // runs every 20ms — switch drives all behavior @Override public void periodic() { switch (m_state) { case INTAKING: moveToIntakePos(); break; case SCORING: moveToScorePos(); break; case CLIMBING: moveToClimbPos(); break; default: holdPosition(); // STOWED — hold where we are } } // expose state for other subsystems or dashboard public ArmState getState() { return m_state; } }
ArmState.SCORING is way clearer than 2 or "scoring". anyone reading the code instantly knows what it means.isActive() directly to an enum. no separate helper class needed.WRT convention: if you catch yourself writing private String m_state = "IDLE" in a subsystem, stop and refactor it to an enum. strings for state is a code smell we actively avoid. the PR reviewer will flag it every time.
Topic 1 — Coding Prompt
Declare an enum ShooterState with four values: IDLE, SPOOLING, READY, FIRING.
Write a method void printAction(ShooterState state) with a switch on it:
• IDLE → print "Motor off"
• SPOOLING → print "Running at 50%"
• READY → print "Running at 90%"
• FIRING → print "Firing!"
In main, call it with each of the four states to confirm all cases work.
Topic 1 — Quick Check
ArrayList
you've used arrays before. you know the deal: declare the size upfront, access by index, done. but what happens when you don't know the size upfront? what if the size needs to change while the program is running? that's where ArrayList comes in.
ArrayList — arrays that grow
imagine a whiteboard where you write down a list of tasks. you can add new items at the bottom, erase items you finished, check if something is on the list — and the whiteboard is magic: it's as long as you need it to be. there's no "sorry, the whiteboard is full" — it just grows. that's an ArrayList.
contrast that with a regular array. a regular array is like a row of fixed lockers — you bolt them to the wall at construction time and you're stuck with that count forever. need one more locker? too bad. have an empty one? it just sits there wasting space. if you know you need exactly N items and that will never change, an array is great. but as soon as the count might change, ArrayList wins.
vision processing returns a variable number of detected targets — you might see 0, 1, or 5 game pieces and you don't know which until the camera tells you. a list of queued commands is dynamic — new commands get added and completed commands get removed. error logs, telemetry history, active subsystems — all of these have counts that change at runtime. that's ArrayList territory.
array vs ArrayList — the mental model
here's a quick rule of thumb that covers most cases: use an array when you KNOW the size upfront and it will never change. use ArrayList when the size is dynamic or unknown at compile time. a few examples from FRC to make it concrete:
// ARRAY: exactly 4 swerve modules (each of the 4 wheel assemblies on a swerve robot — each one can drive and steer independently), always exactly 4 — fixed forever int[] kModuleCanIDs = {1, 2, 3, 4}; // CAN ID — each motor on the robot needs a unique number so the code knows which one to talk to // ARRAYLIST: unknown number of vision targets — changes every frame ArrayList<String> detectedTargets = new ArrayList<>(); // ARRAY: exactly 3 PID constants — fixed (kP = how aggressively to correct error, kI = correction for persistent error, kD = dampens overshooting) double[] kPIDConstants = {0.5, 0.0, 0.1}; // ARRAYLIST: log of recent state transitions — grows over time ArrayList<String> m_stateLog = new ArrayList<>();
importing and creating an ArrayList
ArrayList lives in java.util, which is NOT automatically imported. you have to explicitly import it at the top of your file. then you create one with a type parameter in angle brackets — that type parameter says what kind of things this list holds.
// this import is REQUIRED — without it you get "cannot find symbol" import java.util.ArrayList; // ArrayList<String> means "a list that holds Strings" ArrayList<String> names = new ArrayList<>(); // the <> on the right is the diamond operator — Java infers the type // you could write new ArrayList<String>() but <> is cleaner and preferred // ArrayList of integers (note: Integer, not int — covered in topic 3) ArrayList<Integer> motorIDs = new ArrayList<>(); // starting with some items — use add() after creation ArrayList<String> subsystems = new ArrayList<>(); subsystems.add("Drivetrain"); subsystems.add("Shooter"); subsystems.add("Intake");
what's the <String> thing? that's called a generic type parameter. it tells Java what type of objects this list holds. think of it as labeling a box "only apples go in here." if you try to add an orange (Integer) to an apple box (ArrayList<String>), Java stops you at compile time. always specify the type. an untyped raw ArrayList is legal but a code smell.
the core methods — add, get, size, contains, remove, clear
these six methods handle probably 90% of everything you'll do with an ArrayList. learn them cold:
ArrayList<String> subsystems = new ArrayList<>(); // add() — appends to the end of the list subsystems.add("Drivetrain"); // list: [Drivetrain] subsystems.add("Shooter"); // list: [Drivetrain, Shooter] subsystems.add("Intake"); // list: [Drivetrain, Shooter, Intake] // get() — access by index, zero-based just like arrays String first = subsystems.get(0); // "Drivetrain" String second = subsystems.get(1); // "Shooter" // size() — how many elements are currently in the list int count = subsystems.size(); // 3 // contains() — checks if a value is in the list boolean hasShooter = subsystems.contains("Shooter"); // true boolean hasDrive = subsystems.contains("Climber"); // false // remove(object) — removes the first matching element subsystems.remove("Intake"); // list: [Drivetrain, Shooter] System.out.println(subsystems.size()); // 2 // isEmpty() — true if list has zero elements System.out.println(subsystems.isEmpty()); // false // clear() — removes everything from the list subsystems.clear(); System.out.println(subsystems.size()); // 0 System.out.println(subsystems.isEmpty()); // true
iterating over an ArrayList with for-each
the for-each loop works identically with ArrayList as it does with arrays. the syntax is the same, the behavior is the same. just plug the ArrayList in where you'd put the array:
ArrayList<String> subsystems = new ArrayList<>(); subsystems.add("Drivetrain"); subsystems.add("Shooter"); subsystems.add("Intake"); // for-each: same as with arrays — element type, variable name, list for (String name : subsystems) { System.out.println("active: " + name); } // active: Drivetrain // active: Shooter // active: Intake // you can also use a regular index-based for loop if you need the index for (int i = 0; i < subsystems.size(); i++) { System.out.println(i + ": " + subsystems.get(i)); } // 0: Drivetrain // 1: Shooter // 2: Intake
the remove-by-index vs remove-by-value gotcha (this WILL bite you)
ok here's a subtle one. remove() is overloaded: you can call it with a value to remove (like remove("Intake")) or with an index to remove (like remove(2)). for an ArrayList of Strings, this is fine. but for an ArrayList<Integer>, there's a trap: Java will prefer the index version if you pass a plain int.
ArrayList<Integer> ids = new ArrayList<>(); ids.add(10); ids.add(20); ids.add(30); // this removes the element AT INDEX 1 (which is 20), not the value 1! ids.remove(1); // list is now [10, 30] — might not be what you wanted // to remove the value 10 specifically, wrap it in Integer.valueOf() ids.remove(Integer.valueOf(10)); // now removes the value 10, not index 10 // for String lists this isn't an issue because String != int ArrayList<String> names = new ArrayList<>(); names.add("Alice"); names.remove("Alice"); // unambiguous: removes the value "Alice"
common gotcha: list.remove(1) on an ArrayList<Integer> removes the element at index 1, NOT the element with value 1. to remove by value from an integer list, use list.remove(Integer.valueOf(yourValue)). this catches a lot of people off guard the first time.
array vs ArrayList — full feature comparison
| Feature | Array | ArrayList |
|---|---|---|
| size | fixed forever at creation | grows and shrinks dynamically |
| types allowed | primitives + objects | objects only (use wrapper classes for primitives) |
| element access | arr[i] | list.get(i) |
| length/size | arr.length | list.size() |
| add element | not possible after creation | list.add(value) |
| remove element | not possible | list.remove(value) |
| check membership | must loop manually | list.contains(value) |
| use when | known, fixed size | dynamic or unknown size |
FRC real talk: in WPILib code you'll often see arrays for things that are truly fixed — the four module positions, the three PID gains, the set of auton options. but for anything that gets built up over time (vision targets, active commands, logs), ArrayList is the right tool. both show up, so you need to be comfortable with both.
Topic 2 — Coding Prompt
Create an ArrayList<String> called targets and add four entries: "NoteA", "NoteB", "NoteC", "NoteD".
Then do each step and print as you go:
1. Print the size
2. Print the element at index 2
3. Remove "NoteB"
4. Print the size again
5. Check if "NoteB" is still in the list using contains() — print the result
6. Use a for-each loop to print all remaining targets
Don't forget import java.util.ArrayList; at the top!
Topic 2 — Quick Check
Wrapper Classes
you just learned that ArrayList only holds objects, not primitives. so ArrayList<int> won't compile. but you might very well need a list of integers. what do you do? Java's answer is wrapper classes — object versions of every primitive type.
wrapper classes — boxing up primitives
imagine you need to ship a piece of candy in the mail. you can't just tape a single Skittle to a postcard — it's too small and fragile, it doesn't have an address, and the post office doesn't know what to do with it. but if you put it in a box, suddenly it has all the structure the mail system needs: an address label, dimensions, packaging. the candy itself didn't change — you just wrapped it.
a primitive (like int) is the raw candy. a wrapper class (like Integer) is the box. the value inside is the same, but now it's a full object with methods, and it can go anywhere an object is expected — like inside an ArrayList.
once you start using collections (ArrayList, HashMap, etc.) you will constantly see Integer, Double, and Boolean in type parameters. you also use the static utility methods on wrapper classes — Integer.parseInt() is everywhere in config reading and dashboard input handling.
the primitive-to-wrapper mapping
every primitive type has a corresponding wrapper class. the pattern is just "capitalize it" — with two exceptions: int maps to Integer (not "Int") and char maps to Character (not "Char").
| Primitive | Wrapper Class | Example use |
|---|---|---|
int | Integer | ArrayList<Integer> |
double | Double | ArrayList<Double> |
boolean | Boolean | ArrayList<Boolean> |
char | Character | ArrayList<Character> |
long | Long | ArrayList<Long> |
float | Float | ArrayList<Float> |
autoboxing — Java does the conversion for you
you might be thinking "do I have to manually wrap every int before adding it to a list?" — nope. Java has a feature called autoboxing that automatically converts primitives to their wrapper types when needed, and unboxing that converts wrapper types back to primitives. in practice this means you just write normal code and Java handles the boxing behind the scenes.
import java.util.ArrayList; ArrayList<Integer> motorIDs = new ArrayList<>(); // autoboxing: int literal 5 is automatically wrapped into Integer(5) motorIDs.add(5); // you write int, Java stores Integer — transparent motorIDs.add(12); motorIDs.add(3); // unboxing: Integer from the list is automatically unwrapped to int int first = motorIDs.get(0); // Java unboxes Integer → int automatically System.out.println(first); // 5 // doubles work the same way ArrayList<Double> speeds = new ArrayList<>(); speeds.add(0.75); // autoboxed: double → Double speeds.add(-1.0); double s = speeds.get(0); // unboxed: Double → double System.out.println(s); // 0.75
the mental model: autoboxing and unboxing happen at the Java compiler level. when you write list.add(5), the compiler silently rewrites it to list.add(Integer.valueOf(5)). when you write int x = list.get(0), it silently becomes int x = list.get(0).intValue(). you never see it, but it's happening. this is why autoboxing has a tiny performance cost — it's creating objects. in robot code it's never a bottleneck, but worth knowing.
useful static methods on wrapper classes
wrapper classes aren't just containers for primitives — they come packed with handy static utility methods. these are some of the most-used methods in all of Java programming, not just FRC:
// parseInt / parseDouble — convert Strings to numbers // used constantly for reading config values, dashboard input, etc. int port = Integer.parseInt("5800"); // int 5800 double speed = Double.parseDouble("0.75"); // double 0.75 // parse fails loudly if the string isn't a valid number // Integer.parseInt("abc") throws NumberFormatException — good to know // MAX_VALUE / MIN_VALUE — boundary constants int biggest = Integer.MAX_VALUE; // 2147483647 (2^31 - 1) int smallest = Integer.MIN_VALUE; // -2147483648 // toString — convert number to String String s = Integer.toString(42); // "42" String sd = Double.toString(3.14); // "3.14" // valueOf — explicitly create a wrapper object from a primitive Integer boxed = Integer.valueOf(99); // explicit boxing (autoboxing does this for you normally)
the NullPointerException trap (this WILL bite you)
here's the nastiest gotcha with wrapper classes, and it trips up intermediate programmers more than beginners. because wrapper classes are objects (not primitives), they can be null. that's fine for storage. the problem is when you try to unbox a null wrapper back to a primitive — Java throws a NullPointerException. and NPEs are runtime errors, not compile errors, which means the program compiles just fine and then crashes on a specific code path.
// BAD: unboxing null crashes at runtime Integer val = null; // Integer is an object — null is valid here int x = val; // NullPointerException! Java tries to call val.intValue() // but val IS null — you can't call methods on null // ───────────────────────────────────────────────────────────────── // SAFE pattern: null-check before unboxing Integer maybeNull = getSomeValueThatMightBeNull(); if (maybeNull != null) { int safe = maybeNull; // only unbox when you know it's not null doSomething(safe); } else { handleMissingValue(); } // or use a default with a ternary int withDefault = (maybeNull != null) ? maybeNull : 0;
in practice: this bites you most when reading from a Map (HashMap returns null for missing keys) or when a method signature returns a nullable Integer. if you're adding values to an ArrayList yourself and they're not null, you're fine. but any time a method CAN return null and you're unboxing the result, add a null check. your future self will thank you.
putting it all together — ArrayList + wrapper classes + enums
let's write a snippet that uses all three concepts from this week together. this is actually close to patterns you'll see in real subsystem code:
import java.util.ArrayList; public enum GamePiece { CONE, CUBE, NONE } public class PieceTracker { // ArrayList of enum values — tracks pieces seen this match private ArrayList<GamePiece> m_history = new ArrayList<>(); // ArrayList of wrapper Doubles — scores recorded over time private ArrayList<Double> m_scores = new ArrayList<>(); public void recordScore(GamePiece piece, double points) { m_history.add(piece); // autoboxing not needed — GamePiece IS an object m_scores.add(points); // autoboxed: double → Double transparently } public int getConeCount() { int count = 0; for (GamePiece p : m_history) { if (p == GamePiece.CONE) count++; // == works for enum values } return count; } public double getTotalScore() { double total = 0.0; for (Double s : m_scores) { total += s; // unboxed: Double → double for the addition } return total; } }
Topic 3 — Coding Prompt
Create an ArrayList<Integer> called motorIDs and add five CAN IDs: 1, 5, 8, 12, 20.
Then do each step in order:
1. Print how many IDs are in the list
2. Remove the ID at index 2 using remove(2) — that's the value 8
3. Add a new ID by converting the String "15" to an int using Integer.parseInt()
4. Print whether 5 is still in the list using contains(5)
5. Use a for-each loop to print all remaining IDs
Don't forget import java.util.ArrayList;!
Topic 3 — Quick Check
Fill in the Blanks
public RobotState { TELEOP, AUTO }
ArrayList<> names = new ArrayList<>();
String s = list.;
ArrayList<> ids = new ArrayList<>();
int x = Integer.;
Knowledge Check
Coding Challenge
practice the ArrayList operations before the project task. try to predict the output before running.
import java.util.ArrayList; ArrayList<String> list = new ArrayList<>(); list.add("alpha"); list.add("beta"); list.add("gamma"); list.remove("beta"); list.add("delta"); System.out.println(list.size()); System.out.println(list.get(1)); System.out.println(list.contains("beta")); for (String s : list) { System.out.println(s); }
answers: size() → 3, get(1) → "gamma" (after removing "beta", gamma shifts to index 1), contains("beta") → false. the foreach prints: alpha, gamma, delta.
Create RobotState.java in your minibot-project folder — the glue that brings your MiniBot's subsystems together. This class tracks what the robot is currently doing and what it should do next.
- Define an inner enum:
public enum State { IDLE, INTAKING, DRIVING, SHOOTING, STOPPED } - Private fields:
State m_currentState,ArrayList<String> m_stateHistory - Constructor: initialize state to
State.IDLE, initialize the ArrayList public void setState(State newState)— updates current state AND adds the state name to historypublic State getState()— returns current statepublic boolean isActive()— returns true if state is NOT IDLE and NOT STOPPEDpublic ArrayList<String> getHistory()— returns the history listpublic String getLastAction()— returns the last entry in history, or "none" if empty- In
main: create a RobotState, transition through several states, print the history
Weekly Test
covers everything from week 7. almost done with the summer section!! score goes to leads :)