Java Records, introduced in Java 16, are a concise way to create immutable data classes. They automatically generate constructors, accessors, equals(), hashCode(), and toString() methods. Records provide a compact syntax for creating classes that are primarily data carriers.
// Traditional class (70+ lines with all methods fully implemented)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override public boolean equals(Object o) { /* ... */ }
@Override public int hashCode() { /* ... */ }
@Override public String toString() { /* ... */ }
}
// Equivalent record (one line!)
public record Point(int x, int y) {}- Reduced Boilerplate: Dramatically reduce code for data containers
- Immutability: Prevent accidental modification of important values
- Type Safety: Replace generic tuples or arrays with named, typed fields
- Clear Intent: Signal that a class is a pure data carrier
- Auto-generated Methods: Get
equals(),hashCode(), andtoString()for free - Pattern Matching: Work well with Java's pattern matching features
// Simple data record with typed fields
public record CoralObjective(int branchId, ReefLevel reefLevel) {}
// Record with a custom constructor for validation or defaults
public record AlgaeObjective(int id, boolean low) {
public AlgaeObjective(int id) {
this(id, false);
}
}Records are perfect for capturing point-in-time values from sensors or systems:
public record DrivetrainState(
Pose2d pose,
ChassisSpeeds velocities,
double timestamp
) {}
// Usage
DrivetrainState currentState = new DrivetrainState(
drivetrain.getPose(),
drivetrain.getChassisSpeeds(),
Timer.getFPGATimestamp()
);Model game-specific elements as records for type safety:
public record ScoringPosition(int gridIndex, int nodeIndex, boolean isHigh) {}
// Usage
ScoringPosition target = new ScoringPosition(2, 1, true);
autoAim(target);Group related configuration values:
public record PIDConfig(double kP, double kI, double kD) {}
// Usage
PIDConfig shooterPID = new PIDConfig(0.1, 0.0, 0.05);
configureMotor(motor, shooterPID);Pass structured data between subsystems:
public record VisionMeasurement(
Pose3d targetPose,
double latencySeconds,
double confidence
) {}
// Usage
Optional<VisionMeasurement> result = vision.getLatestMeasurement();
result.ifPresent(m -> robotState.addVisionMeasurement(m));Add validation or provide convenient overloads:
public record ShooterParameters(double rpm, double hoodAngle, double backspinRatio) {
// Validation in canonical constructor
public ShooterParameters {
if (rpm < 0 || rpm > 6000) {
throw new IllegalArgumentException("RPM out of range: " + rpm);
}
if (hoodAngle < 0 || hoodAngle > 45) {
throw new IllegalArgumentException("Hood angle out of range: " + hoodAngle);
}
}
// Compact constructor overload
public ShooterParameters(double rpm, double hoodAngle) {
this(rpm, hoodAngle, 1.0); // Default backspin ratio
}
}Add factory methods for common instances:
public record ArmPosition(double shoulderRadians, double wristRadians) {
public static ArmPosition stowed() {
return new ArmPosition(0.1, 0.2);
}
public static ArmPosition scoring() {
return new ArmPosition(1.2, 0.8);
}
public static ArmPosition floor() {
return new ArmPosition(-0.7, 0.5);
}
}Add methods to perform operations on the record data:
public record Pose2dWithConfidence(Pose2d pose, double confidence) {
public Pose2dWithConfidence withRotation(Rotation2d newRotation) {
return new Pose2dWithConfidence(
new Pose2d(pose.getTranslation(), newRotation),
confidence
);
}
public boolean isReliable() {
return confidence > 0.7;
}
}Compose records for hierarchical data:
public record ModuleState(double speedMetersPerSecond, Rotation2d angle) {}
public record DriveState(
ModuleState frontLeft,
ModuleState frontRight,
ModuleState backLeft,
ModuleState backRight
) {
public double getAverageSpeed() {
return (frontLeft.speedMetersPerSecond() +
frontRight.speedMetersPerSecond() +
backLeft.speedMetersPerSecond() +
backRight.speedMetersPerSecond()) / 4.0;
}
}- Data transfer objects
- Value objects and measurements
- Configuration parameters
- API responses
- Immutable data containers
- Return types for multiple values
- Mutable state that needs to change after creation
- Classes with complex behavior beyond data access
- Classes requiring inheritance (records cannot extend other classes)
- Implementing frameworks that expect traditional classes
-
Adding Mutable Fields: Records are meant to be immutable. Don't try to add mutable fields.
// DON'T DO THIS public record BadRecord(int x, int y) { private List<Integer> history = new ArrayList<>(); public void addToHistory(int value) { history.add(value); // Breaks immutability! } }
-
Too Many Fields: Records with many fields become hard to use. Consider splitting them.
// Too many fields public record OvercomplicatedConfig( double kP, double kI, double kD, double kF, double maxVel, double maxAccel, double tolerance, double timeout, double minOutput, double maxOutput, boolean invertMotor, boolean enableSoftLimits, double forwardSoftLimit, double reverseSoftLimit ) {} // Better: Split into logical groups public record PIDConfig(double kP, double kI, double kD, double kF) {} public record MotionConfig(double maxVel, double maxAccel) {} public record LimitConfig(double minOutput, double maxOutput) {}
-
Using For Subsystems or Commands: Records aren't suitable for classes with behavior.
-
Start with Simple Data Classes: Look for POJOs (Plain Old Java Objects) that mainly hold data.
-
Replace Tuple Returns: Change methods returning arrays/tuples to return records.
// Before double[] getShooterParams() { return new double[] {rpm, angle}; } // After record ShooterParams(double rpm, double angle) {} ShooterParams getShooterParams() { return new ShooterParams(rpm, angle); }
-
Bundle Configuration: Group related configuration parameters.
-
Create Sensor Reading Records: Snapshot sensor readings in records.
-
Standardize Game Element Representation: Use records for game pieces, scoring positions, etc.
// Game piece tracking
public record GamePieceLocation(
Translation2d position,
GamePieceType type,
double timestamp,
double confidenceScore
) {
public boolean isStale() {
return Timer.getFPGATimestamp() - timestamp > 0.5;
}
public GamePieceLocation withUpdatedPosition(Translation2d newPosition) {
return new GamePieceLocation(
newPosition,
this.type,
Timer.getFPGATimestamp(),
this.confidenceScore
);
}
}
// Usage
List<GamePieceLocation> visibleGamePieces = vision.detectGamePieces();
for (GamePieceLocation piece : visibleGamePieces) {
if (!piece.isStale() && piece.confidenceScore() > 0.8) {
targetPiece = piece;
break;
}
}