Java I/O
Practical for Week 10 (no lecture this week). Extends java-io (Week 8) with saving/loading a data model to/from files, plus a file-format design exercise.
Setup
Provided classes: Schedule, Airport, Flight, DayOfWeek, BadScheduleException (an airline schedule of flights between airports), plus an unrelated Maze class.
Airline schedule
Goal: make a Schedule’s data persistent by saving/loading it to/from a file.
Task 0 — implement Schedule.toString() per its Javadoc spec.
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(System.lineSeparator());
joiner.add(String.valueOf(getConnectedAirports().size()));
joiner.add(String.valueOf(this.flights.size()));
for (Airport airport : getConnectedAirports()) {
joiner.add(airport.toString());
}
for (Flight flight : this.flights) {
joiner.add(flight.toString());
}
return joiner.toString();
}Task 1 — implement void save(String filename), propagating any exceptions rather than handling them:
public void save(String filename) throws IOException {
BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
writer.write(this.toString());
writer.close();
}Task 2 — implement static Schedule load(String filename), using two private helpers (readFlight, readAirport) and throwing BadScheduleException on a failed numeric parse or an unknown airport code:
public static Schedule load(String filename) throws IOException, BadScheduleException {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String airportsLine = reader.readLine();
String flightsLine = reader.readLine();
if (airportsLine == null || flightsLine == null) {
throw new BadScheduleException();
}
int numAirports, numFlights;
try {
numAirports = Integer.parseInt(airportsLine);
numFlights = Integer.parseInt(flightsLine);
} catch (NumberFormatException e) {
throw new BadScheduleException();
}
List<Airport> airports = new ArrayList<>();
for (int i = 0; i < numAirports; ++i) {
airports.add(readAirport(reader));
}
List<Flight> flights = new ArrayList<>();
for (int i = 0; i < numFlights; ++i) {
flights.add(readFlight(reader, airports));
}
return new Schedule(flights);
}
private static Flight readFlight(BufferedReader reader, List<Airport> airports)
throws IOException, BadScheduleException {
String line = reader.readLine();
if (line == null) {
throw new BadScheduleException();
}
String[] lineParts = line.split("\\|");
if (lineParts.length != 4) {
throw new BadScheduleException();
}
int flightNumber;
try {
flightNumber = Integer.parseInt(lineParts[0]);
} catch (NumberFormatException e) {
throw new BadScheduleException();
}
Airport origin = null, destination = null;
for (Airport airport : airports) {
if (airport.getCode().equals(lineParts[1])) {
origin = airport;
}
if (airport.getCode().equals(lineParts[2])) {
destination = airport;
}
}
if (origin == null || destination == null) {
throw new BadScheduleException();
}
DayOfWeek dayOperating = DayOfWeek.valueOf(lineParts[3]);
return new Flight(flightNumber, origin, destination, dayOperating);
}
private static Airport readAirport(BufferedReader reader) throws IOException, BadScheduleException {
String line = reader.readLine();
if (line == null) {
throw new BadScheduleException();
}
String[] lineParts = line.split("\\|");
if (lineParts.length != 2) {
throw new BadScheduleException();
}
return new Airport(lineParts[0], lineParts[1]);
}Note readFlight/readAirport both throw BadScheduleException for a malformed line — this is a form of defensive programming (see java-specification) protecting the Schedule created by load() from corrupt input, rather than letting a NullPointerException/ArrayIndexOutOfBoundsException leak out from deeper in the parsing logic.
Maze — designing a file format
Maze maps (row, column) positions to tile types (start, end, wall, empty). Unlike Schedule, there’s no existing format to follow — the task is to design one that’s sufficient to save and reload a Maze’s full state.
Task 3 — design a file format for Maze. (Hint: the class’s invariants may let you store less than a full grid dump.)
Several reasonable designs, each with different tradeoffs:
- Visual dump — store the same characters
render()would print (e.g.#for wall,S/Efor start/end). Human-readable, but more complex to parse back than necessary. - Linear encoding of every tile — e.g.
#S#Efor a 2×2 grid means wall at (0,0), start at (0,1), wall at (1,0), end at (1,1) (with or without an outer border wall included). - One line per map entry — e.g.
1,2,#means “wall at (1, 2)”. Simple, but verbose for large mazes with many walls. - Only relevant tiles — the first two stored positions are always start/end, and every position after that is implicitly a wall; positions can be
(row, column)pairs or a single linear index (rows * row + column). - Start/end positions + a bitmap — store start and end explicitly, then a bit per remaining tile (
1= wall,0= empty). - Start/end positions + wall runs — store start and end, then walls as (start, end) position pairs, so a run of adjacent wall tiles can be stored as a single entry instead of one entry per tile.
Options 4-6 exploit the fact that most of a maze’s tiles are either walls or empty (i.e. an invariant/regularity in the data), letting the format avoid storing every single tile individually.