Java
Article

Building a Study Guide App with Java Hashmap

By Lincoln Daniel

These days, the idea a physical dictionary is pretty old-fashioned. Consulting a physical book to learn the meaning of a word or phrase makes no sense when a simple Google search will give you all the information you need.

But a dictionary is a very useful metaphor for an important programming concept: key-value pairs. Dictionaries operate by linking a word (a “key”) to its meaning (a “value”). This key-value pair is a fundamental data model in programming.

Most programming languages use hashes to ensure the uniqueness of keys and make storage and retrieval of values more efficient. A HashMap in Java puts all of that together to allow programmers to store key-value pairs by hashing their keys upon insertion and retrieval.

In this tutorial, we’ll learn how to build a study guide program by storing Java concepts and their corresponding meanings in a HashMap to be retrieved when it’s time to study. The source code for this tutorial can be found here

HashMap

Java maps make programs more efficient by storing key-value pairs in a single data structure. Alternatively, the same could be accomplished with two manually synced data structures — one holding keys and the other holding values — but this isn’t the best approach to mapping keys to values; that’s what a Map is for. The HashMap is arguably one of the most powerful of them all. All keys of the map must be of the same type, as must its values.

In the background, Java’s HashMap class stores the pairs in arrays, but we don’t have to worry much about the implementation because much of the heavy lifting is encapsulated.

The Map interface provides methods for putting pairs in the map, replacing the value of pairs, removing pairs by key, checking for the existence of a pair by key, and clearing the map. We’ll make use of almost all of those functions today.

The Application

Our app will be a simple study guide app. We’ll build our HashMap to store our concepts and use Java control-flow statements to loop until we have provided the correct meaning for each concept. Let’s build!

Classes

We’ll need two classes to run our study guide application. The first class will be our main class to run our program and the second will be another class to model the meaning of Java concepts and encapsulate a couple of fields and methods to simplify the study guide interface.

The first thing we need to do is create a new Java project with a main class. Once we have that set up, let’s go ahead and build our Meaning class that will be used in our Main.

study

Meaning Class

Create a new class called “Meaning”. We won’t be diving into this class, but let’s go over the importance of it. This class will model the meaning of Java concepts to be studied by holding their meaning and tokens of that meaning. Here are its important parts:

public class Meaning {
    /*if a user's guess at the meaning matches at least 65% of the 
    actual meaning of a Meaning, the user is correct.*/
    private static final double CORRECTNESS_THRESHOLD_PERCENTAGE = .65;

    //the string representation of this meaning
    private String meaning;
    //whether or not the user's input matches this meaning
    private boolean gotCorrect;
    //holds the tokens of the meaning
    private String[] meaningTokens;

    //the constructor
    public Meaning(String meaning) {
        this.meaning = meaning;
        tokenizeMeaning();
    }

    //omitted ...

    /**
     * This is a naive lexical analyzer
     * that counts how many words match
     * between the user input and this meaning.
     *
     * There are many ways to improve this.
     * @param userInput the user's guess at what this concept means
     * @return true if the user's input matches 
     * this meaning.
     */
    public boolean userInputMatches(String userInput) {
        //make sure the user's input is not empty
        if(userInput != null && !userInput.isEmpty()) {
            int numMatchedTokens = 0;
            userInput = removeAllPunctuation(userInput);
            String[] userInputTokens = userInput.split(" ");

            //total tokens. The greater of the two
            int totalTokens = Math.max(userInputTokens.length, 
            meaningTokens.length);

            /*get the number of matched tokens 
            between the two sets of tokens.*/
            numMatchedTokens = getNumMatchedTokens(userInputTokens);

            //cast to a double to get floating point result
            double matchPercentage = ((double)numMatchedTokens / totalTokens);
            //return whether or not the matched percentage meets the threshold
            return matchPercentage >= CORRECTNESS_THRESHOLD_PERCENTAGE;
        }

        //return false because the user's input is empty 
        return false;
    }

    //omitted ...
}

To construct a new Meaning instance, we pass the meaning of a concept to the constructor. From there, the meaning will be tokenized and stored in an array for us to use when checking if the user’s input matches the meaning of the concept.

When the program runs, the user will be asked to input a guess at the meaning of a concept, and we’ll use that input to check if the user is correct by calling the userInputMatches() instance method of Meaning. This method will tokenize the user’s input and loop through the meaning’s tokens and the user’s input tokens to count their matches in order. We’ll return true if 65% of the meaning’s tokens are matched by the user’s input tokens; this means the user answer is correct and so they get a point.

That’s all we need Meaning for, but you can view the full class in the supporting code for this tutorial.

Main Class

Our study guide will run in the main class. Feel free to call this class whatever you want, but I’ll call mine “StudyGuide”.

public class StudyGuide {
    //omitted ...

    //initialize a HashMap to hold String-Meaning (key-value) pairs.
    private static final HashMap<String, Meaning> studyGuide = new HashMap<>();
    /*The total possible points the user can obtain. 
    Equal to the number of concepts in the study guide.*/
    private static int possiblePoints;

    //our main method to start our app
    public static void main(String[] args) {
        populateStudyGuide();
        printInstructions();       
        study();
    }

    //omitted ...
}

Our StudyGuide class begins pretty humbly. We start by initializing a final static HashMap called studyGuide as our study guide. All the keys of studyGuide, representing our concepts, will be of the String type while the values, the meanings of our concepts, will be of our custom Meaning type.

We begin our app by calling populateStudyGuide to populate studyGuide with concept-meaning pairs:

/**
 * Populates the study guide with Java concepts.
 */
private static void populateStudyGuide() {
    //add a bunch of 
    studyGuide.put("Variable", new Meaning(
            "Used to store a single value for later use."));
    studyGuide.put("String", new Meaning(
            "A class for representing character strings."));
    studyGuide.put("double", new Meaning(
            "A primitive datatype for representing floating point numbers."));
    studyGuide.put("Double", new Meaning(
            "A class for wrapping a double in an Object with convenient methods."));
    studyGuide.put("zero", new Meaning(
            "The number zero. The first index in arrays and the first position of lists."));

    //set the possible points
    updatePossiblePoints();
}

There, we call the put() instance method of our HashMap five times, putting a concept and its meaning in studyGuide each time. This method ends by calling updatePossiblePoints() to set the possible points, static possiblePoints, obtainable by a user:

private static void updatePossiblePoints() {
        possiblePoints = studyGuide.size();
        System.out.println("There are " 
        + possiblePoints + " concepts to study.");
}

Note thatpossiblePoints should always be equal to the number of elements in studyGuide which we can gather by calling its size() instance method.

Next, we call printInstructions() to print our app’s instructions to the console for the user. After that, it’s time to study:

/**
 * Starts the study session
 */
private static void study() {
    //for scanning the user's keyboard for input
    Scanner userInputScanner = new Scanner(System.in);
    //to store the user's input
    String userInput;

    /*the points obtained by the user 
    for getting the meanings of concepts correct.*/
    int userPoints = 0;
    /*how many rounds the user takes 
    to get all of the concept meanings correct*/
    int rounds = 1;

    //set the default user input because the while loop needs to check it
    userInput = "startInConsole";

    /*We'll let the user quit at any point in the app,
    so we must make sure they haven't quit yet*/
    while(userPoints < possiblePoints && !userQuit(userInput, userPoints)) {
        //store the keys of the study guide in a immutable Set
        final Set<String> concepts = studyGuide.keySet();

        for (String concept : concepts) {
            Meaning currentConceptMeaning = studyGuide.get(concept);
            //make sure currentConceptMeaning is not null
            if(currentConceptMeaning == null) {
                studyGuide.remove(concept);
                //move to next iteration
                continue;
            }

            if(!currentConceptMeaning.userGotCorrect()) {
                System.out.printf("\n\t\t" + OUTPUT_SEPERATOR_LINE 
                        + "\n\n> What is %s?\n\n\n", concept);

                if(userInputScanner.hasNextLine()) {
                    userInput = userInputScanner.nextLine();
                }

                if (!userQuit(userInput, userPoints)) {
                    if (currentConceptMeaning.userInputMatches(userInput)) {
                        currentConceptMeaning.markUserGotCorrect();
                        userPoints++;
                        System.out.printf("\n> CORRECT! %s means: %s",
                                concept, currentConceptMeaning.getMeaning());
                    } else {
                        System.out.printf("\n> WRONG! %s means: %s",
                                concept, currentConceptMeaning.getMeaning());
                    }
                }
            }
        }

        System.out.println(OUTPUT_SEPERATOR_LINE);
        System.out.printf("\n> You have %d of %d possible points at the end of round %d. ", 
                userPoints, possiblePoints, rounds);

        //make sure the user hasn't scored all of the possible points
        if(userPoints < possiblePoints) {
            System.out.println("\n> Type anything to continue "
                    + "OR remove to remove a concept \"x\" or \"quit\" to quit?");
            if(userInputScanner.hasNextLine()) {
                userInput = userInputScanner.nextLine();
                if(userInput.toLowerCase().equals("remove")) {
                    System.out.println("\n> Remove which concept?");
                    if(userInputScanner.hasNextLine()) {
                        removeConcept(userInputScanner.nextLine());
                    }
                }
            }
        } else break; //break out of the loop because the user is done
    }

    System.out.println(OUTPUT_SEPERATOR_LINE);
    System.out.println("Congrats! You got all the meanings correct.");
}

We start studying by calling the study() method. This method declares a couple of variables to hold information about the user’s study session. We initialize a Scanner as userInputScanner to capture the user’s input, which will be stored in userInput. The user’s accumulated points will be stored in userPoints and incremented by 1 each time the user provides the right meaning for a concept.

We use a while loop to continue prompting the user with concepts so long as they haven’t obtained the possible points and they do not enter “x” or “quit” to quit.

At the top of the while loop, we must store the Set of keys in studyGuide in a constant by calling its keySet() instance method so that we can iterate over it. For each key, we get its value by calling the get() instance method of studyGuide and passing it the key. The value returned and stored in currentConceptMeaning is a Meaning instance representing the meaning of a concept. So long as the currentConceptMeaning is not null, we let the user take a guess at what they think it is. If the userInputMatches() instance method of currentConceptMeaning returns true, we call its markUserGotCorrect() instance method to set its gotCorrect instance field to true and increment userPoints because the user provided the correct meaning for the concept.

At the end of each round, we print out how many points the user has accumulated and we allow them to remove a concept they give up on studying if they haven’t finished the study guide; we do this by calling our removeConcept() method:

/**
  * This allows the user to remove a concept they give up on from the study guide. 
  * Removes the pair from the HashMap with a key that matches the concept.
  * @param concept 
  */
 private static void removeConcept(String concept) {
     Meaning conceptMeaning = studyGuide.get(concept);
     if(conceptMeaning != null) {
         //make sure the user hasn't already gotten the meaning correct
         if(!conceptMeaning.userGotCorrect()){
             //remove the concept's key-value pair from the study guide
             studyGuide.remove(concept);
             System.out.println("Removed \"" + concept + "\" from your study guide.");

             /*update the possible points so that it matches 
             the number of concepts left in the study guide*/
             updatePossiblePoints();
         } else {
             //don't let the user remove a concept they already know
             System.out.println("You know \"" + concept + "\", silly. Can't remove.");
         }
     } else {
         System.out.println("\"" + concept + "\" isn't in your study guide.");
     }
 }

When we call removeConcept(), it gets the Meaning value corresponding to the concept in studyGuide as conceptMeaning. If the concept is found, we call the userGotCorrect() instance method of conceptMeaning to check if the user got the meaning of the concept correct, and if this returns false, we call the remove() instance method of studyGuide() to remove the key-value pair from the HashMap by the concept as the key. We don’t let the user remove a concept that they already correctly defined because they already received points for it.

Our userQuit() method checks if the user asked to quit at any point to end the program and print out the user’s points. On the other hand, if the user makes it to the end of the study guide, we call break to break out of the while loop. At that point, the app will congratulate the user on a successful study session and the program will end.

Conclusion

Well look at that, we’re all done! We now have a study guide built with a single HashMap to help us study our Java concepts. We also now know how to use the HashMap for efficiently storing and manipulating key-value pairs in Java. You’re already on your way to be a master of the maps! Play around with the source code and see how you can make the study guide better. Have questions? Leave a comment and I’ll do my best to answer.

References:
Oracle Documentation on Maps
Oracle Documentation on HashMaps

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Java, once a week, for free.