Unit 2 · Lesson 7

Arrays and ArrayLists

A swerve drive has four modules. A robot might have six motors, three sensors, and two controllers. The moment you find yourself naming variables motor0, motor1, motor2… you need an array. This lesson is about storing and working with collections of related values — and knowing when a fixed-size array is the right tool versus a dynamic list.

By the end of this lesson, you will:

  • Declare, initialize, and access elements in a Java array using correct index syntax
  • Explain zero-indexing and predict what happens when code accesses an out-of-bounds index
  • Iterate over arrays with both indexed for loops and for-each loops
  • Use ArrayList for collections that grow or shrink at runtime — fault logs, command queues, sensor histories
  • Choose correctly between an array and an ArrayList for a given robot problem

The Variable Naming Problem

Suppose your robot has four swerve modules. Without arrays, you might write this:

// ❌ Four variables, nearly identical — every operation requires four lines SwerveModule frontLeft = new SwerveModule(0, 1, 2); SwerveModule frontRight = new SwerveModule(3, 4, 5); SwerveModule backLeft = new SwerveModule(6, 7, 8); SwerveModule backRight = new SwerveModule(9, 10, 11); frontLeft.update(); // applying current limit? four lines. frontRight.update(); // brake mode change? four lines. backLeft.update(); // firmware update check? four lines. backRight.update();

Now imagine adding a fifth module if the team changes the drive base. Or iterating over all modules to check for faults. Every operation on the group requires duplicating code for each variable. An array turns that group into a single named entity you can loop over.

Arrays: Fixed Collections of One Type

An array is an ordered collection of elements, all of the same type, stored at contiguous memory locations. Its size is fixed at creation — you cannot add or remove elements after the array is created. In FRC, arrays are the right choice whenever you have a known, constant number of hardware objects: four swerve modules, three motors in a climber gearbox, eight CAN bus devices.

Declaration and initialization

// Syntax: type[] name = new type[size]; // Declare and create — elements default to null (objects) or 0/false (primitives) TalonFX[] driveMotors = new TalonFX[4]; double[] currentAmps = new double[4]; boolean[] faultFlags = new boolean[4]; // Declare with an initializer list — size is inferred from the values int[] canIds = { 0, 3, 6, 9 }; String[] moduleNames = { "FL", "FR", "BL", "BR" }; // Check the size at any time int count = driveMotors.length; // 4 — note: no parentheses, it's a field not a method
💡 .length is a field, not a method

Array length is accessed with array.length — no parentheses. Contrast with ArrayList.size(), which is a method call and uses parentheses. Mixing these up (array.length() or list.size) is a compile error that appears often when switching between the two collection types.

Zero-Indexing: How Arrays Are Addressed

Every element in a Java array has an index — a number identifying its position. The first element is at index 0, not 1. An array of four elements has valid indices 0, 1, 2, and 3. Accessing index 4 throws ArrayIndexOutOfBoundsException.

The swerve module array below has four elements. Click any module to see how it is accessed in code and what happens at each position.

SwerveModule[] modules — click any element
SwerveModule[] modules = new SwerveModule[4];
index FL Module
index FR Module
index BL Module
index BR Module
[0]
[1]
[2]
[3]
← click any module to see how it is accessed and what index rules apply

Iterating Over Arrays

The loop patterns from Lesson 5 map directly onto arrays. The right choice depends on whether you need the index during the loop body.

When you want to apply the same operation to every element and don't need to know which position you're at, for-each is cleaner and immune to off-by-one errors.

// Configure every module — no index needed, for-each is cleanest for (SwerveModule module : modules) { module.setCurrentLimit(DRIVE_CURRENT_LIMIT); module.setNeutralMode(NeutralModeValue.Brake); } // Fault scan — stop at first fault (Lesson 5's break pattern) for (TalonFX motor : driveMotors) { if (motor.getFault_Hardware().getValue()) { logFault("Drive motor hardware fault"); break; } }

When the index carries meaning — matching a module to its corresponding desired state, building a string with position numbers, or writing to a paired array — use an indexed for loop.

// Apply desired states to matching modules — index is essential here for (int i = 0; i < modules.length; i++) { modules[i].setDesiredState(desiredStates[i]); // same index, different array } // Log current draw with module name — index used for the label for (int i = 0; i < driveMotors.length; i++) { double amps = driveMotors[i].getStatorCurrent().getValueAsDouble(); SmartDashboard.putNumber("Drive Motor " + i + " Amps", amps); } // Populate an array after creation — assign each index individually for (int i = 0; i < driveMotors.length; i++) { driveMotors[i] = new TalonFX(canIds[i]); // canIds[] maps i → CAN ID }

The two-array pattern (modules[i] and desiredStates[i] sharing the same index) is used extensively in swerve drive code. WPILib's kinematics classes return an array of SwerveModuleState objects that are positionally matched to your module array — index 0 is always front-left, index 1 front-right, and so on. Getting the order wrong here produces a robot that drives sideways.

🔍 Event Observation

One of the most reliable ways to identify a swerve drive array ordering mistake at a competition is to watch the robot drive forward: if it instead spins or crabs sideways, the module array and the desired states array are mismatched by index. During inspection I've asked teams to show me how they initialize their module array and almost always that reveals whether front-left is at index 0 or somewhere else. The fix is a one-line swap in the array initializer. The diagnosis takes under a minute if you know to look there — and much longer if you don't.

Two-Dimensional Arrays

A 2D array is an array of arrays — a grid structure. In FRC, they appear in field zone grids, pre-computed lookup tables, and matrix math for odometry transformations. You access elements with two indices: grid[row][col].

int[3][3] fieldZones — row = field zone row, col = zone column
[0][1][2]
[0] 012
[1] 345
[2] 678
// Declare a 3x3 grid — row = 3 rows, col = 3 columns int[][] fieldZones = { { 0, 1, 2 }, // row 0 { 3, 4, 5 }, // row 1 { 6, 7, 8 } // row 2 }; // Access center element: row 1, col 1 (highlighted in green above) int center = fieldZones[1][1]; // 4 // Iterate over every cell for (int row = 0; row < fieldZones.length; row++) { for (int col = 0; col < fieldZones[row].length; col++) { System.out.print(fieldZones[row][col] + " "); } System.out.println(); }

ArrayList: When the Size Isn't Known in Advance

An array's fixed size is a strength when you have exactly four modules — and a problem when you don't know how many elements you'll have at compile time. ArrayList<T> is Java's resizable list: elements can be added or removed at runtime, and it handles the internal memory management automatically.

// Import required — ArrayList lives in java.util import java.util.ArrayList; // Declare and create — <T> is the type parameter (generics) ArrayList<String> faultLog = new ArrayList<>(); ArrayList<Double> speedHistory = new ArrayList<>(); // Add elements faultLog.add("Motor 0: overcurrent at T=12.4s"); faultLog.add("CAN bus: timeout on device 5"); // Access by index — same zero-based indexing as arrays String first = faultLog.get(0); // Check size — note: size() not length int count = faultLog.size(); // Remove by index or by value faultLog.remove(0); // removes first element, shifts others down faultLog.remove("stale fault entry"); // removes by content if present // Iterate — for-each works identically to arrays for (String fault : faultLog) { SmartDashboard.putString("Last Fault", fault); }
💡 ArrayList uses wrapper types for primitives

ArrayList stores objects, not primitives. You can't write ArrayList<double> — you must use the wrapper class: ArrayList<Double>. Java automatically converts between double and Double (autoboxing/unboxing), so in practice you rarely notice — but it does have a small performance cost. For large numerical collections in performance-sensitive robot code, a plain double[] array is faster. For a fault log that might have zero to ten entries, ArrayList<String> is fine.

Common ArrayList methods in FRC context

// Building a fault log during a match ArrayList<String> matchFaults = new ArrayList<>(); // add() — append to end matchFaults.add("T=4.2s: arm motor stall"); // size() — how many entries if (matchFaults.size() > 10) { matchFaults.remove(0); } // keep last 10 // contains() — has a specific fault been logged? if (matchFaults.contains("CAN bus timeout")) { alertDriver(); } // clear() — wipe the log between modes matchFaults.clear(); // isEmpty() — guard before accessing if (!matchFaults.isEmpty()) { SmartDashboard.putString("Latest Fault", matchFaults.get(matchFaults.size() - 1)); }

Choosing Between Array and ArrayList

Both store collections of the same type. The decision comes down to one question: do you know the size at compile time, and will it stay fixed?

Array — type[]

Fixed size, set at creation. Faster iteration. Works with primitives directly. Simpler syntax for hardware collections.

  • Swerve modules (always 4)
  • Drive motors (known at build)
  • Sensor port assignments (fixed)
  • CAN ID lookup tables
  • WPILib kinematics inputs (e.g. SwerveModuleState[])
ArrayList<T>

Resizable at runtime. Requires wrapper types for primitives. Rich API (add, remove, contains). Slightly slower for tight numerical loops.

  • Fault log entries (unknown count)
  • Command queues for auto sequences
  • Sensor history for smoothing
  • Dashboard widget registrations
  • Any collection that grows during a match
// ✅ Array: four swerve modules — fixed, known at build time SwerveModule[] modules = new SwerveModule[4]; // ✅ ArrayList: fault log — grows during a match, unknown max size ArrayList<String> faultLog = new ArrayList<>(); // ✅ ArrayList: speed history for a rolling average ArrayList<Double> speedHistory = new ArrayList<>(); static final int HISTORY_SIZE = 10; private double getRollingAverage(double newSample) { speedHistory.add(newSample); if (speedHistory.size() > HISTORY_SIZE) { speedHistory.remove(0); } double sum = 0.0; for (double v : speedHistory) { sum += v; } return sum / speedHistory.size(); }
🔍 LRI Observation

The most common array-vs-ArrayList mistake I see during code review is using an ArrayList for something that never changes size — like the set of swerve modules or drive motors. It works, but it forces wrapper types, adds allocation overhead, and makes the code harder to read for anyone expecting the WPILib convention of SwerveModuleState[]. The second most common is the inverse: using a fixed array for a fault log, pre-allocating 100 slots, and then writing complex index-tracking logic instead of just using an ArrayList. When in doubt: if WPILib gives you an array type in its API, match it. If you're building a collection at runtime, use ArrayList.

The Three Array Bugs That Appear Every Season

Bug 1: Accessing a null element

Creating an array with new TalonFX[4] allocates space for four references, but doesn't create any TalonFX objects. Every element starts as null. Calling a method on a null element throws NullPointerException.

TalonFX[] motors = new TalonFX[4]; // motors[0] through motors[3] are all null right now motors[0].set(0.5); // ❌ NullPointerException — no TalonFX object exists yet // ✅ Always populate the array before using it for (int i = 0; i < motors.length; i++) { motors[i] = new TalonFX(canIds[i]); // now each slot holds a real object } motors[0].set(0.5); // ✅ safe — object exists

Bug 2: Parallel arrays drifting out of sync

When two arrays are positionally matched — modules[i] paired with desiredStates[i] — inserting, removing, or reordering one without updating the other produces subtle, hard-to-diagnose mismatches. On a swerve drive, this makes modules respond to each other's commands.

SwerveModule[] modules = { fl, fr, bl, br }; SwerveModuleState[] desiredStates = kinematics.toSwerveModuleStates(chassisSpeeds); // WPILib returns states in the same order as the modules array was defined. // If you defined modules as { fl, fr, bl, br }, states[0] is for fl. // Reordering the modules array without reordering the state array sends // the front-left state to the back-right module. // ✅ Document the order with constants: static final int FL = 0, FR = 1, BL = 2, BR = 3; // Then modules[FL], modules[FR] etc. are self-documenting

Bug 3: Forgetting that ArrayList.remove(int) is index-based

ArrayList<Integer> has an ambiguity: remove(0) removes the element at index 0, not the element with value 0. To remove by value, cast to the wrapper type.

ArrayList<Integer> canIds = new ArrayList<>(); canIds.add(5); canIds.add(0); canIds.add(3); canIds.remove(0); // ⚠️ removes element at index 0 (value 5) — is that what you meant? canIds.remove(Integer.valueOf(0)); // ✅ removes the element with value 0

🔌 System Check

⚙️ Arrays, ArrayLists, and Hardware Collections
  • Never call a method on an array element before verifying it isn't null. Creating an array only allocates the slots. Every hardware object — TalonFX, CANcoder, SwerveModule — must be explicitly constructed and assigned to its slot in robotInit() before any periodic method touches it.
  • Match WPILib array conventions exactly. SwerveDriveKinematics, SwerveDriveOdometry, and PathPlanner all return and accept SwerveModuleState[] in the order you defined your module array. Reordering one without the other produces a robot that drives sideways. Document your index order with named constants (FL = 0, etc.).
  • Use array.length (no parentheses) and list.size() (with parentheses). Mixing these up is a compile error when switching between the two types, and easy to do when refactoring.
  • For collections that grow at runtime, use ArrayList. Pre-allocating a 50-element array with an index counter to simulate a list is harder to read, harder to maintain, and easier to break than just using ArrayList.
  • For performance-critical numerical loops, use primitive arrays. A double[] with 1000 elements iterates faster than ArrayList<Double> with 1000 elements. For 4–8 hardware objects in a periodic method, the difference is negligible. For sensor history with thousands of samples, it matters.
  • Clear or bound ArrayList collections that grow during a match. A fault log or sensor history that is only ever appended to will grow without bound over a long testing session. Either cap it (if (list.size() > MAX) list.remove(0)) or clear() it between modes.

Knowledge Check

Click an answer to check your understanding.

A robot has this code: TalonFX[] motors = new TalonFX[4]; followed immediately by motors[0].set(0.5);. What happens at runtime?
  • 1The motor at index 0 is set to 50% speed — the constructor creates the objects automatically
  • 2A NullPointerException is thrown — new TalonFX[4] allocates four null slots but does not construct any TalonFX objects; calling .set() on a null reference crashes the program
  • 3A compile error — you cannot call methods on array elements directly
  • 4The call is silently ignored — WPILib skips null hardware objects automatically
Your swerve drive works correctly when driving straight but spins clockwise when you command a left turn. A teammate suggests "check the module array order." What are they referring to, and why would this cause that symptom?
  • 1The modules array being in the wrong order would cause the robot to fail to initialize entirely
  • 2This is a motor inversion issue unrelated to array ordering
  • 3WPILib's kinematics returns desired states in the same positional order as the modules array was defined — if the array order doesn't match, each module receives the state intended for a different module; a turn command distributes states incorrectly, producing rotation instead of translation or vice versa
  • 4Array ordering only affects initialization; it cannot cause incorrect movement behavior after setup is complete
You need to store CAN bus fault messages as they occur during a match. You don't know how many there will be. After the match you want to check whether a specific message was logged. Which collection and method call is most appropriate?
  • 1String[] faultLog = new String[100]; with a manual index counter — arrays are faster
  • 2String[] faultLog = new String[0]; — Java will resize it automatically when needed
  • 3ArrayList<String> faultLog = new ArrayList<>(); — unknown count at compile time means use a resizable collection; faultLog.contains("message") handles the post-match check cleanly
  • 4Neither — use a HashMap for any collection where you need to search by value
💪 Practice Prompt

Hardware Collections and Fault Logging

Implement the following class skeleton. It manages four drive motors as an array and maintains a runtime fault log as an ArrayList. No robot hardware required — you can run this as a standalone Java class with a main() method using mock values.

import java.util.ArrayList; public class DriveManager { private static final int NUM_MOTORS = 4; private static final double STALL_THRESHOLD = 55.0; // amps private static final int MAX_FAULTS = 20; // TODO 1: declare a double[] of size NUM_MOTORS to store simulated current readings // TODO 2: declare an ArrayList<String> for the fault log // TODO 3: implement — populate the currentReadings array with the provided values public void setCurrentReadings(double[] readings) { } // TODO 4: implement — iterate over currentReadings, log a fault message for any // motor whose current exceeds STALL_THRESHOLD. Format: "Motor N: XX.Xamps" // If the log would exceed MAX_FAULTS, remove the oldest entry first. public void checkForFaults() { } // TODO 5: implement — return the highest current reading across all motors public double getMaxCurrent() { return 0.0; } // TODO 6: implement — return true if the fault log contains an entry for the given motor index public boolean hasMotorFault(int motorIndex) { return false; } // TODO 7: implement — clear the fault log (called at mode transitions) public void clearFaults() { } // TODO 8: implement — print all fault log entries to the console public void printFaultLog() { } }
  1. Implement all eight TODOs. Every method that iterates over the array must use the appropriate loop style — for-each when the index isn't needed, indexed for when it is.
  2. Write a main() method that calls setCurrentReadings() with {30.0, 58.2, 42.1, 61.7}, then checkForFaults(), then printFaultLog(). Verify that motors 1 and 3 are logged (0-indexed) and motors 0 and 2 are not.
  3. Call getMaxCurrent() and print the result. Confirm it returns 61.7.
  4. Call hasMotorFault(3) and hasMotorFault(0) and print both results. Explain why contains() works for this even though the fault message contains more than just the motor number.
  5. Bonus: Modify checkForFaults() to also compute and log the average current across all four motors in a single pass, appending it as a summary entry at the end of the fault log. What is the minimum number of array iterations required to do this without an extra loop?