Interfaces, Generics, and Lists
In earlier lessons, we wrote classes like Point and RobotTracker to represent things in our program.
In this lesson, we’ll learn about interfaces, a way to describe what a class can do without saying how it does it,
generics, a way to write code that works with more than one type, and the List interface, a more flexible alternative to arrays.
Why Interfaces?
Section titled “Why Interfaces?”Suppose your robot needs to measure its distance from a wall during autonomous. One year, your team might use an ultrasonic sensor; the next, a LiDAR sensor. Both measure distance, but they’re different pieces of hardware, controlled by different code.
If the rest of your robot code is written to only work with one specific sensor class, swapping hardware means rewriting everything that used it. An interface solves this by describing what a sensor can do, without saying which specific sensor it is:
interface DistanceSensor { double getDistanceMeters();}An interface looks like a class, but its methods have no bodies, just a signature ending in a semicolon.
It’s a contract: any class that implements DistanceSensor must provide a getDistanceMeters() method.
Here are two classes that each implement that contract, in their own way:
class UltrasonicSensor implements DistanceSensor { @Override public double getDistanceMeters() { // In real life, this would actually interact with hardware return 1.5; }}class LidarSensor implements DistanceSensor { @Override public double getDistanceMeters() { // In real life, this would actually interact with hardware return 1.2; }}A real sensor’s getDistanceMeters() would read a value from hardware.
These simplified versions just return a fixed number, so we can focus on the
interface itself.
Because both classes implement DistanceSensor, code that only knows about DistanceSensor can work with either one:
DistanceSensor ultrasonic = new UltrasonicSensor();DistanceSensor lidar = new LidarSensor();System.out.println(isTooClose(ultrasonic)); // falseSystem.out.println(isTooClose(lidar)); // falseHere, isTooClose is defined to take a DistanceSensor, so it doesn’t matter whether we pass in an UltrasonicSensor or a LidarSensor.
If your team switches sensors next season, this method doesn’t need to change at all.
A Generic Method
Section titled “A Generic Method”Interfaces let one piece of code work with several related types. Generics go a step further, letting a method work with any type. Here’s a method that returns the last element of an array, no matter what it’s an array of:
<T> T last(T[] items) { return items[items.length - 1];}The <T> before the return type introduces a type parameter, a placeholder for a type that isn’t decided until the method is called.
Inside the method, T acts like a real type: the parameter is T[], and the return type is T.
We can call last with completely unrelated array types, and it works for both:
Point[] path = {new Point(0, 0), new Point(1, 2), new Point(3, 3)};DistanceSensor[] sensors = {ultrasonic, lidar};
System.out.println(last(path).getX()); // 3.0System.out.println(last(sensors).getClass()); // class LidarSensorWhen we call last(path), Java fills in T with Point; when we call last(sensors), it fills in T with DistanceSensor.
We didn’t have to write a separate method for each case.
Packages and Imports
Section titled “Packages and Imports”So far, every class we’ve written has had no package, which is why our classes could use each other with no import statements at all.
Classes built into the Java Development Kit (JDK), classes from external libraries like WPILib,
and classes in our projects live in packages, named groups of classes.
A package’s name exactly matches the directory it lives in.
For example, all of the classes in the java.util package live in the java/util directory of the JDK library.
Robot code typically lives in the first.robot package, which you’ll see when you start Stage 1 of this course.
To use a class from a package, you need an import statement at the top of the file, naming the exact class you want:
import java.util.ArrayList;import java.util.List;This imports ArrayList and List from the java.util package, which we’ll use next.
Later in the course, you’ll import classes the same way from WPILib packages, such as org.wpilib.math.geometry.Translation2d.
The List Interface and ArrayList
Section titled “The List Interface and ArrayList”In the previous lesson, we used arrays to store multiple values together. An array’s size is fixed once it’s created, which works well when you know exactly how many elements you’ll need, like a fixed autonomous path. But sometimes you don’t know the size ahead of time, for example, if you want to record the robot’s position every time it moves, for as long as the match lasts.
List is an interface, like DistanceSensor, that describes a collection that can grow and shrink.
ArrayList is a class that implements List:
List<Point> waypoints = new ArrayList<>();Just like DistanceSensor sensor = new UltrasonicSensor();, the declared type (List<Point>) is an interface, and the object we create (new ArrayList<>()) is one specific implementation of it.
<Point> tells Java that this particular List holds Points; List itself is generic, the same way last was.
A List doesn’t have a fixed size, you add elements to it as you go, and it grows to fit:
waypoints.add(new Point(0, 0));waypoints.add(new Point(1, 2));System.out.println(waypoints.size()); // 2A List’s number of elements is read with the method size(), not the
field length you saw with arrays.
Giving RobotTracker a Memory
Section titled “Giving RobotTracker a Memory”RobotTracker, from a previous lesson, keeps track of the robot’s current position, but forgets everywhere it’s been.
Let’s add the ability to remember every position the robot has visited, not just the current one, using a List<Point>:
class RobotHistoryTracker { private Point position; private final List<Point> history = new ArrayList<>();
public RobotHistoryTracker(Point startPosition) { this.position = startPosition; this.history.add(startPosition); }
public void move(Point delta) { this.position = this.position.plus(delta); this.history.add(this.position); }
public Point getPosition() { return this.position; }
public List<Point> getHistory() { return this.history; }}Instead of making a new class, you could also add the new fields and methods
to the RobotTracker class.
getHistory() returns the List<Point> of every position move has ever moved to, in order.
We can loop over it with a for-each loop, exactly the way we looped over arrays in the previous lesson:
RobotHistoryTracker tracker = new RobotHistoryTracker(Point.ORIGIN);tracker.move(new Point(3, 0));tracker.move(new Point(0, 4));
for (Point visited : tracker.getHistory()) { System.out.println(visited.getX() + ", " + visited.getY());}Even though history grows every time move is called, the for-each loop doesn’t need to know how many positions it will visit; it simply visits all of them.
Interfaces, Generics, and Lists Exercise
Section titled “Interfaces, Generics, and Lists Exercise”WIP