Iterating Complex Map Structures
Have you ever encountered yourself with nested data structures mixed with generics syntax? Even worse, have you or have you been creating this confuse statements in your code?
Let me show you an example of what I mean and what I had found in some interactions with colleague's programming code.
In our example application we need to store and show the enrollment data for a university quatrimester. The data is structured in a hierarchical fashion where we have as first node the year, then the quatrimesters, the courses and at the bottom the students enlisted in those courses.
Year
++ Quatrimester
+++ Course
++++ Student
Seems like we could sit down with some paper and a pencil to start drawing some nice class diagrams, well let me show you what can happen when someone wants to skip some designing time:
// Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter = new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>(); //
Why bother our application adding more files to represent new classes when you can have it all in one line? That is the implicit approach when someone decides to create this level of complexity, that for him could be understandable but poor of the guy that needs to maintain that later.
I'm going to develop more the code taking this approach to show the increasing complexity we get by not isolating the distinct components of the problem we want to solve.
The Student class is pretty much simple with just the fullName attribute:
public class Student { this.fullName = fullName; } return fullName; } this.fullName = fullName; } }
Now we add an "EnrollmentProcessVr1" class with the already presented scary variable and two methods:
+ public static void startEnrollment(int year, int quarter){...} // To fill our enrollment data.
+ public static void showEnrollment() {...} // Show the data contained in the data structure.
Let's see the complex of the code to add/show data from this structure:
import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; public class EnrollmentProcessVr1 { private static Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter = new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>(); public static void startEnrollment(int year, int quarter) { // Create a Year. // Create a quatrimester. // Create courses. // Create List of Students. List<Student> students = new ArrayList<Student>(); students.add(new Student("Cristobal Colón")); students.add(new Student("Juan SantaMaría")); // Create a Map of Courses/Students. // Add Students to courses. studentsByCourses.put(course1, students); studentsByCourses.put(course2, students); // Create a Map of Quarter/Courses. new HashMap<Integer, Map<String,List<Student>>>(); coursesQuarterMap.put(quatrimester, studentsByCourses); // Add quarter to year. studentsByQuarter.put(currentYear, coursesQuarterMap); } public static void showEnrollment() { while (studentsByQuarterIterator.hasNext()) { Map<Integer, Map<String, List<Student>>> quarterCoursesMap = (Map<Integer, Map<String, List<Student>>>) studentsByQuarterPairs.getValue(); while (quarterCoursesIterator.hasNext()) { Map<String, List<Student>> coursesStudentsMap = (Map<String, List<Student>>) coursesByQuarterPairs.getValue(); while (coursesStudentsIterator.hasNext()) { List<Student> studentsByCourse = (List<Student>) coursesStudentsPairs.getValue(); for(Student student : studentsByCourse) { } } } } } EnrollmentProcessVr1.startEnrollment(2009, 3); EnrollmentProcessVr1.showEnrollment(); } }
Output from main method:

My focus in this post will be how to solve the intrinsic complexity of iterating map structures, not necessary the refactoring of bad designed data structures, but this will help to demonstrate that even well designed classes can have some problems it they have internal map structures. Having said that let's create some classes:

EnrollmentTrackVr1:
import java.util.LinkedHashMap; import java.util.Map; public class EnrollmentTrackVr1 { private Map<Integer, QuatrimestersVr1> quatrimesters; public EnrollmentTrackVr1() { quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr1>(); } public Map<Integer, QuatrimestersVr1> getQuatrimesters() { return quatrimesters; } if (!quatrimesters.containsKey(year)) { quatrimesters.put(year, new QuatrimestersVr1()); } quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student); } }
QuatrimestersVr1:
import java.util.HashMap; import java.util.Map; public class QuatrimestersVr1 { private Map<Integer, CoursesVr1> courses; public QuatrimestersVr1() { courses = new HashMap<Integer, CoursesVr1>(); } if (!courses.containsKey(quatrimesterNumber)) { courses.put(quatrimesterNumber, new CoursesVr1()); } courses.get(quatrimesterNumber).addStudent(courseName, student); } public Map<Integer, CoursesVr1> getCourses() { return courses; } }
CoursesVr1:
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class CoursesVr1 { private Map<String, List<Student>> students; public CoursesVr1() { students = new HashMap<String, List<Student>>(); } if (!students.containsKey(courseName)) { students.put(courseName, new ArrayList<Student>()); } students.get(courseName).add(student); } public Map<String, List<Student>> getStudents() { return students; } }
The new classes simplify the code to add data but as we can see the iteration logic still remains a pain, we still have to use the iterator of the internal map structures of every single class:
import java.util.Iterator; import java.util.List; import java.util.Map; public class EnrollmentProcessVr2 { private static EnrollmentTrackVr1 enrollmentTrackVr1; public static void startEnrollment(int year, int quarter) { Student student1 = new Student("Cristobal Colón"); Student student2 = new Student("Juan Santamaría"); enrollmentTrackVr1 = new EnrollmentTrackVr1(); enrollmentTrackVr1.addStudent(student1, year, quarter, course1); enrollmentTrackVr1.addStudent(student2, year, quarter, course1); enrollmentTrackVr1.addStudent(student1, year, quarter, course2); enrollmentTrackVr1.addStudent(student2, year, quarter, course2); } public static void showEnrollment() { Iterator enrollmentIterator = enrollmentTrackVr1.getQuatrimesters().entrySet().iterator(); while (enrollmentIterator.hasNext()) { QuatrimestersVr1 quatrimesters = (QuatrimestersVr1) enrollmentPairs.getValue(); Iterator quatrimestersIterator = quatrimesters.getCourses().entrySet().iterator(); while (quatrimestersIterator.hasNext()) { CoursesVr1 courses = (CoursesVr1) quatrimestersPairs.getValue(); Iterator coursesIterator = courses.getStudents().entrySet().iterator(); while (coursesIterator.hasNext()) { List<Student> studentsByCourse = (List<Student>) coursesPairs.getValue(); for(Student student : studentsByCourse) { } } } } } EnrollmentProcessVr2.startEnrollment(2009, 3); EnrollmentProcessVr2.showEnrollment(); } }
Wouldn't be nice if we could iterate our classes in the same way we do with any list from java collections?
List<String> fooList = new ArrayList<String>(); ... }
It is possible to do that if we learn how to use the Iterable & Iterator interfaces. In the following design I use my own interface which extends from these two and additional, a new method that will be helpful to return the key of the object that is being iterated in the loop (I will explain it more).
This is how the new design looks:

My custom interface receives a new generic element "K" that will be the type/class of the map's key.
import java.util.Iterator; public abstract interface MapIterable<K, V> extends Iterable<V>, Iterator<V>{ public K currentKey(); }
Let's implement the new interface and explain further which methods we need to implement for our classes to be iterable:
+ public boolean hasNext() : this method indicates if the class can advance more in the structure. In my implementation I added a private variable (currentKeyYearIndex)
to keep record of the current array's index. This variable is incremented with the next() method.
+ public V next(): returns the next object of the iteration. In my implementation I get the keyset from the internal map and then I convert it to an array to get the element
from the current index location.
+ public void remove(): removes the current object. Here I didn't want to bother too much and I just throw an UnsupportedOperationException;
+ public Iterator iterator(): gets the iterator. Simply return the class.
+ public K currentKey(): my added method to get the current key of the iteration.
EnrollmentTrackVr2:
import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; public class EnrollmentTrackVr2 implements MapIterable<Integer, QuatrimestersVr2> { private Map<Integer, QuatrimestersVr2> quatrimesters; private int currentKeyYearIndex; public EnrollmentTrackVr2() { quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr2>(); currentKeyYearIndex = 0; } public Map<Integer, QuatrimestersVr2> getQuatrimesters() { return quatrimesters; } if (!quatrimesters.containsKey(year)) { quatrimesters.put(year, new QuatrimestersVr2()); } quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student); } public boolean hasNext() { if (quatrimesters.keySet().toArray().length > currentKeyYearIndex) { return true; } return false; } public QuatrimestersVr2 next() { quatrimesters.keySet().toArray()[currentKeyYearIndex++]; return quatrimesters.get(currentKeyYear); } public void remove() { } public Iterator<QuatrimestersVr2> iterator() { return this; } quatrimesters.keySet().toArray()[currentKeyYearIndex-1]; } }
QuatrimestersVr2:
import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class QuatrimestersVr2 implements MapIterable<Integer, CoursesVr2> { private Map<Integer, CoursesVr2> courses; private int currentQuatriIndex; private int year; public QuatrimestersVr2() { courses = new HashMap<Integer, CoursesVr2>(); currentQuatriIndex = 0; } public QuatrimestersVr2(int year) { this(); this.year = year; } if (!courses.containsKey(quatrimesterNumber)) { courses.put(quatrimesterNumber, new CoursesVr2()); } courses.get(quatrimesterNumber).addStudent(courseName, student); } public Map<Integer, CoursesVr2> getCourses() { return courses; } public boolean hasNext() { if (courses.keySet().toArray().length > currentQuatriIndex) { return true; } return false; } public CoursesVr2 next() { courses.keySet().toArray()[currentQuatriIndex++]; return courses.get(currentQuatri); } public void remove() { } public Iterator<CoursesVr2> iterator() { return this; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } courses.keySet().toArray()[currentQuatriIndex-1]; } }
CoursesVr2:
import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; public class CoursesVr2 implements MapIterable<String, List<Student>> { private Map<String, List<Student>> students; private int currentCourseIndex; private int quatrimester; public CoursesVr2() { students = new HashMap<String, List<Student>>(); currentCourseIndex = 0; } public CoursesVr2(int quatrimester) { this(); this.quatrimester = quatrimester; } if (!students.containsKey(courseName)) { students.put(courseName, new ArrayList<Student>()); } students.get(courseName).add(student); } public Map<String, List<Student>> getStudents() { return students; } public boolean hasNext() { if (students.keySet().toArray().length > currentCourseIndex) { return true; } return false; } public List<Student> next() { String currentCourse = students.keySet().toArray()[currentCourseIndex++].toString(); return students.get(currentCourse); } public void remove() { } public Iterator<List<Student>> iterator() { return this; } public int getQuatrimester() { return quatrimester; } public void setQuatrimester(int quatrimester) { this.quatrimester = quatrimester; } return students. keySet().toArray()[currentCourseIndex-1].toString(); } }
Finally let's see how it ends up the iteration process:
EnrollmentProcessVr3:
import java.util.List; public class EnrollmentProcessVr3 { private static EnrollmentTrackVr2 enrollmentTrackVr2; public static void startEnrollment(int year, int quarter) { Student student1 = new Student("Cristobal Colón"); Student student2 = new Student("Juan Santamaría"); enrollmentTrackVr2 = new EnrollmentTrackVr2(); enrollmentTrackVr2.addStudent(student1, year, quarter, course1); enrollmentTrackVr2.addStudent(student2, year, quarter, course1); enrollmentTrackVr2.addStudent(student1, year, quarter, course2); enrollmentTrackVr2.addStudent(student2, year, quarter, course2); } public static void showEnrollment() { for(QuatrimestersVr2 quatrimesters : enrollmentTrackVr2) { for (CoursesVr2 courses : quatrimesters) { for (List<Student> students : courses) { for (Student student : students) { } } } } } EnrollmentProcessVr3.startEnrollment(2009, 3); EnrollmentProcessVr3.showEnrollment(); } }
The code may have some bugs, I think I forgot to reset the current key at some point, but this was done only to demostrate the power of using the Iterable interface.
I may be reinventing the wheel or doing more complex than it is so you are invited to give me any suggestions you have. Thanks!
- Gabriel Solano
- gsolano's blog
- Login or register to post comments
