Unit 2 · Lesson 2

Data Types

Java is a strongly typed language — every variable must declare what kind of data it holds before it can hold anything. That constraint isn't bureaucracy; it's the compiler protecting you from a whole class of bugs that would otherwise only appear at runtime, on a robot, in a match.

By the end of this lesson, you will:

  • Name all eight Java primitive types and describe what each stores
  • Explain the difference between primitive types and reference types, and identify which category String belongs to
  • Choose the correct type for a given robot value — motor speed, port number, sensor flag, or dashboard label
  • Perform widening and narrowing type conversions, and predict where data loss occurs
  • Recognize the three most common type-related bugs in FRC code: the float/double precision trap, integer division truncation, and int overflow

The Two Families of Types

Before diving into the eight individual primitive types, it helps to understand the fundamental split in Java's type system. Every type you'll use falls into one of two categories, and the difference between them matters when you start working with WPILib objects and sensor data.

Primitive Types

Store a raw value directly in memory. Fast, fixed-size, always have a value (never null). Java has exactly eight of them.

  • The value is the variable
  • Lowercase names: int, double, boolean
  • Cannot call methods on them
  • Default to zero, false, or '\u0000'
Reference Types

Store a reference (memory address) to an object. Can be null. Created with new, or as string literals.

  • The variable points to the value
  • Capitalized names: String, TalonFX, XboxController
  • Can call methods: str.length()
  • Default to null — calling a method on null crashes

For most of Unit 2, you'll work with primitives. Reference types become central in Unit 4 (Object-Oriented Programming), but you'll encounter them immediately when you create a motor controller or a joystick object in Unit 5 — those are all reference types.

The Eight Primitive Types

Java has exactly eight primitive types. You won't use all of them in FRC code, but you need to know they exist and why some were designed the way they were. Click any type below to see its size, range, and — most importantly — when and why you'd reach for it on a robot.

FRC everyday types Rarely used in FRC
← click any type to see its range and FRC context
💡 Why does size matter on a roboRIO?

The roboRIO runs on a dual-core ARM processor with 256 MB of RAM. That's not a lot by modern standards. While a few extra long variables won't crash the robot, understanding size is what lets you read WPILib source code and vendor library APIs confidently — you'll see methods return double instead of float, and you'll know exactly why.

String: The Reference Type You'll Use Constantly

Even though String is a reference type — not a primitive — it gets special treatment in Java and appears in almost every FRC program. You'll use it for Shuffleboard labels, fault messages, chooser options, and CAN bus names.

Three things about String that trip people up:

1. Compare with .equals(), not ==

For primitives, == compares values. For reference types including String, == compares memory addresses — whether two variables point to the exact same object in memory. This almost never does what you intend. Use .equals() to compare the actual text.

String mode = "auto"; // ❌ Wrong — compares memory addresses, not content if (mode == "auto") { ... } // ✅ Correct — compares the actual characters if (mode.equals("auto")) { ... } // ✅ Also correct — avoids NullPointerException if mode is null if ("auto".equals(mode)) { ... }

2. Strings are immutable

You cannot change a String after it's created. Every operation that looks like it modifies a string — +, .toUpperCase(), .trim() — actually creates a new String object. The original is untouched. This matters in robot code that builds log messages in a loop: constructing strings with + inside a periodic method creates a new object every 20ms. For performance-sensitive logging, WPILib has better tools (covered in Unit 6).

3. Concatenation with +

You can attach any value to a String using +. Java will automatically convert the non-string value to text. This is useful for dashboard output and debugging.

double speed = 0.75; boolean running = true; // Concatenation — prints: "Intake speed: 0.75 | running: true" System.out.println("Intake speed: " + speed + " | running: " + running);

Type Casting: Moving Between Types

Sometimes you have a value of one type and need it in another. Java handles this in two ways depending on whether information might be lost in the conversion.

Widening converts a smaller type to a larger one — like pouring a cup of water into a bucket. No information is lost, so Java does it automatically without any syntax from you.

The widening chain goes: byte → short → int → long → float → double

// Widening happens silently — Java promotes automatically int encoderTicks = 4096; double position = encoderTicks; // ✅ int → double, no cast needed // This happens constantly in FRC — many WPILib methods return double, // and you'll often start from an int port number or tick count int canId = 5; double scaledId = canId; // 5.0 — perfectly safe

You'll see widening conversion in WPILib every time you pass an int port number into a method that expects a double, or when math involving both types produces a double result automatically.

Narrowing converts a larger type to a smaller one — like pouring a bucket into a cup. Information might be lost, so Java requires you to write an explicit cast using parentheses. The compiler is making you acknowledge the risk.

// Narrowing requires an explicit cast — (targetType) double sensorReading = 9.78; int rounded = (int) sensorReading; // 9 — decimal is truncated, NOT rounded // ⚠️ Truncation is NOT rounding double almost10 = 9.999; int result = (int) almost10; // result is 9, not 10 // ⚠️ Overflow — the value doesn't fit, wraps unpredictably int bigNumber = 300; byte small = (byte) bigNumber; // small is 44, not 300 — data corruption // ✅ In FRC, a common valid use: WPILib returns double, you need int double rawTicks = encoder.getPosition(); // returns double int ticksInt = (int) rawTicks; // acceptable if you know it's whole

The rule of thumb: before you cast narrowing, ask yourself "do I know for certain this value fits in the target type?" If the answer is "probably," that's not certain enough. A value that overflows an int or truncates unexpectedly can produce motor commands that send a mechanism in the wrong direction at full speed.

The Three Type Traps in FRC Code

These three mistakes are common enough that they have their own names in the 2910 shop. All three compile without errors, all three produce incorrect robot behavior, and all three are preventable by choosing types carefully.

Trap 1: Integer Division Truncation

When you divide two int values in Java, the result is also an int — the decimal portion is thrown away silently. This catches almost every programmer at least once.

// ❌ Looks like it calculates a midpoint — actually returns 0 int minSpeed = 0; int maxSpeed = 1; int midpoint = (minSpeed + maxSpeed) / 2; // 1 / 2 = 0 in integer math // ✅ Use double, or force floating-point division with a cast double midpointD = (minSpeed + maxSpeed) / 2.0; // 0.5 double midpointC = (double)(minSpeed + maxSpeed) / 2; // also 0.5 // FRC context: calculating a velocity setpoint from a range int minRPM = 1000; int maxRPM = 5000; double cruiseRPM = (minRPM + maxRPM) / 2.0; // 3000.0 — correct

Trap 2: The float / double Precision Gap

Both float and double are floating-point types, but they store very different amounts of precision. float has about 7 significant decimal digits; double has about 15. WPILib's entire API is built on double. Mixing in float introduces precision errors that are essentially invisible until you try to tune a PID controller and the gains behave strangely.

// ❌ float literal — notice the 'f' suffix float kP = 0.1f; // stored as 0.10000000149011612 internally // ✅ double — what WPILib expects double kP = 0.1; // stored as 0.1 with full double precision // The implicit conversion from float → double carries the error float pidGain = 0.05f; double passed = pidGain; // 0.05000000074505806 — error propagates into PID math
🔍 LRI Observation

I've watched teams spend a full evening trying to tune a PID loop that refused to settle — adjusting kP, kI, kD over and over, seeing the mechanism oscillate no matter what they tried. The root cause turned out to be float constants being implicitly widened to double when passed to WPILib, introducing accumulated precision error into every feedback calculation. Switching every gain to double fixed the oscillation in one test. Use double everywhere. There is no FRC scenario where float is the right choice.

Trap 3: int Overflow

An int can store values up to about 2.1 billion. That sounds enormous, but some FRC calculations get there faster than you'd expect — particularly raw encoder tick counts, timestamps in microseconds, or accumulated error in a manual integration loop. When an int exceeds its maximum, it doesn't crash: it silently wraps around to a large negative number. The robot keeps running. The math just starts producing nonsense.

// int max is 2,147,483,647 int ticks = 2_147_483_647; // at the limit ticks++; // now ticks == -2,147,483,648 // ⚠️ In FRC: a high-resolution encoder running for a long match // CTRE Falcon encoders have 2048 ticks/rev. At 6000 RPM for 150 seconds: // 6000 * 2048 * 150 / 60 = 30,720,000 ticks — well within int range. // But if you accumulate position over a multi-hour test session... // ✅ When in doubt about accumulation, use long long accumulatedTicks = 0L; // range: ±9.2 quintillion
🔍 Event Observation

A team was logging match timestamps using an int variable counting microseconds elapsed since robot enable. During a three-hour practice session — not a match, but a long pit test — the counter overflowed. Their logging system started reporting negative timestamps, which broke the sorting in their log viewer. They spent an hour assuming the log viewer was broken before someone thought to check the counter type. A long would have held microsecond timestamps for over 292,000 years. The fix was one word.

Quick Reference: Types by Robot Role

When you're writing robot code and need to pick a type, run through this mental checklist:

  • Motor output, PID gain, velocity, angle, or any measurement with a decimal → double
  • CAN ID, port number, current limit (amps, whole number), loop counter → int
  • Sensor state, toggle flag, "is X running," button pressed → boolean
  • Dashboard label, fault message, chooser option, log entry → String
  • Accumulated tick count over a long session, high-precision timestamp → long
  • Everything else: default to double for decimals, int for whole numbers, and only change if you have a specific reason
💡 When the API decides for you

WPILib and vendor libraries have already made most type decisions. TalonFX.set() takes a double. XboxController.getAButton() returns a boolean. new TalonFX(int deviceId) takes an int. When you're calling an API method, let the method signature tell you the type — your IDE will show it on hover. The decisions in this lesson are most important when you're declaring a variable to hold intermediate results or store state.

🔌 System Check

⚙️ Type Decisions That Affect Hardware

The compiler won't catch most of these — which is exactly why they make it onto robots.

  • Never use float for anything that feeds into WPILib or a motor controller. WPILib's entire math layer is double. A float variable that gets widened to double carries its precision error with it into every calculation downstream.
  • Current limits must be int or double depending on the vendor API, not float. Check the method signature. CTRE's current limit config takes a double. REV's takes an int. Passing the wrong type causes a silent cast and the limit ends up at an unexpected value.
  • Watch for integer division in any calculation that produces a motor output. If the result of a division feeds into motor.set(), make sure at least one operand is a double or the result will be truncated to 0 or 1.
  • Initialize booleans to the safe default — almost always false. A boolean intakeRunning = true as a field means the intake motor starts the instant the robot enables. Safe defaults prevent pit table accidents and unexpected enable-mode behavior.
  • If your code does any accumulation over time (integration, tick counting, timestamps), audit whether int is large enough. The match is 150 seconds, but your robot will run far longer in testing. Size for the worst case.

Knowledge Check

Click an answer to check your understanding.

A student writes int result = 7 / 2; and expects the answer to be 3.5. What value is actually stored in result, and why?
  • 13.5 — Java rounds to the nearest decimal automatically
  • 23 — both operands are int, so Java performs integer division and truncates the decimal portion; the result type is int, which cannot hold 3.5
  • 34 — Java rounds up when dividing integers
  • 4A compiler error — you cannot divide an int by another int
You're storing a PID proportional gain that needs to be passed to a WPILib PIDController. The constructor takes a double. Which declaration is correct, and why does the other one cause problems?
  • 1float kP = 0.1f;float is smaller so it uses less memory on the roboRIO
  • 2Either is fine — Java automatically converts float to double with no side effects
  • 3double kP = 0.1;float has only ~7 digits of precision; when it widens to double to match the constructor, the precision error in the float value gets promoted into the PID calculation and can cause oscillation or instability
  • 4float kP = 0.1f; — WPILib's PIDController internally uses float anyway
Your code has if (currentMode == "teleop") { ... } where currentMode is a String. The condition never evaluates to true, even when the string contains "teleop". What is the problem?
  • 1Java strings are case-sensitive — try "Teleop" with a capital T
  • 2== compares memory addresses for reference types, not content — two different String objects containing "teleop" are at different addresses, so == returns false; use currentMode.equals("teleop") instead
  • 3You cannot use == with strings at all — it will cause a compile error
  • 4String comparisons require importing java.lang.StringCompare
💪 Practice Prompt

Type Audit and Correction

The following class represents a simplified arm subsystem. It compiles without errors, but it contains multiple type-related bugs that would cause incorrect behavior on a real robot. Find them all.

// Find every type problem — there are five public class ArmSubsystem { private static final float ARM_KP = 0.08f; private static final float ARM_KD = 0.002f; private static final int ARM_MAX_ANGLE_DEG = 120; private static final int ARM_MIN_ANGLE_DEG = 0; private boolean armAtTarget = true; // tracks whether arm reached setpoint public double calculateOutput(double currentAngle, double targetAngle) { int midpoint = (ARM_MAX_ANGLE_DEG + ARM_MIN_ANGLE_DEG) / 2; double error = targetAngle - currentAngle; double output = ARM_KP * error; return output; } public boolean isAtTarget(String status) { return status == "at_target"; } }
  1. List each of the five type problems. For each one, write the corrected declaration or expression and explain in one sentence what could go wrong on the robot if left uncorrected.
  2. The midpoint variable calculates the center of the arm's range. With the values as written, what value does it actually produce? Show the math.
  3. Rewrite the entire class with all five bugs corrected. Add a // fixed: comment on each corrected line explaining the change.
  4. Bonus: The calculateOutput() method ignores the derivative term (ARM_KD). What additional variable would you need to calculate a proper PD controller output, and what type should it be? Write the corrected method signature and body.