• 15 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/28/24

Move from a list to a dictionary to manage task completion

In this chapter, we are going to modify your TodoList class to implement one functionality: track whether a task has been completed or not.

Refactor your TodoList class with a dictionary

You'll be using a dictionary to implement this new functionality!

Why are we using a dictionary? 🤔

As you have seen in the first part of this course, a dictionary is a data structure that stores data as a key/value pair. There can be many tasks on a to-do list. For each of them, you want to know whether they have been completed or not. For this, you can use the tasks and their completion states as a pair. Now, what do you think you should use as the key? 

1. If you use the completion state, you only have two possible values for the key (completed or not).
2. If you use the name of the task, each task must only come once.

The first choice is more generic, but harder to implement. So, use the name of the task as the key for the remainder of this chapter.

Let's see how to implement your dictionary using the HashMap Java collection.

Replace the ArrayList with a HashMap in your TodoList class

There are several classes in Java that implement dictionaries. They all implement the  Map  interface that defines all the operations necessary to manipulate the dictionary. A full discussion on how to pick the right one is available on the official Java Tutorials website.

In this example, we will use the most commonly used map implementation: the HashMap.  You can find the Javadoc that describes all the available methods you can use. 

Let's now this how this translates in your TodoList class:

/** TodoList models a Todo List 
 * and the operation we can perform on it
 */
public class TodoList {
	private String topic;
	private HashMap<String,Boolean> tasks;
	// remaining of the implementation goes here
}

You can see that:

  1. The name of the class is the same.

  2. The topic field remains identical, as an instance of a String class.

  3. The tasks field is now an instance of a HashMap.

The HashMap is parametrized using the <String, Boolean> syntax. This means the key has to be a  String (the task name) and the value a Boolean  (whether the task is complete or not).

As with an ArrayList, a HashMap must be initialized before using. This initialization is performed in the constructor of the class. Here is the implementation of the class augmented with a constructor:

/** TodoList models a Todo List 
 * and the operation we can perform on it
 */
public class TodoList {
	String topic;
	HashMap<String,Boolean> tasks;

	/** TodoList instantiates a TodoList with the provided string as a topic for the list
	 * and allocates the memory for the list of tasks
	 * @param topic The topic for which the list is created for
	 */
	public TodoList(String topic) {
		this.topic=topic;
		this.tasks=new HashMap<String,Boolean>();
	}

//... methods go here
}

As you can see, the constructor:

  1. Sets the value for the topic field with the provided argument.

  2. Initializes the memory for the tasks field.

This constructor is called in the main method when you create the object:

TodoList myTodoList = new TodoList("My morning routine");

 See how this constructor makes sure your object is ready to use? Since you cannot create an object without going through a constructor, you are guaranteed that each instance of your TodoList  class:

  1. Will have a value set for the topic field.

  2. Will have a properly initialized tasks field.

Try this

Let's refactor each of your methods to deal with the tasks field being a HashMap instead of an ArrayList.

Refactor the add method

When adding a new task, you know that it has not been completed yet. You can, therefore, set the value for the task field as false. You just need to provide its name as an argument to the method. Here is the code:

/** addTask adds a task to the map with the name as the key and false as the completion status
 * @param taskName The description of the task to be added
 */
public void addTask(String taskName) {
	this.tasks.put(taskName,false);	
}

Did you notice a change in the signature of the method, compared to the addTask method from the previous chapter? Let's refresh your memory with the implementation from the previous chapter:

/** addTask appends the provided String at the end of the list
 *  @param task The description of the task to be added
 */
public void addTask(String taskName) {
	this.tasks.add(taskName);
}

You see that:

  1. The signature is the same.

  2. The implementation has changed in the body of the method.

Success! ✌️Any object using the class will still be using it in the same way. You have refactored the class without breaking its interface (the set of public methods accessible to the world). Hurray for object-oriented programming! 🌟

Let's verify that with an implementation in the main method:

myTodoList.addTask("Wake up");
myTodoList.addTask("Shower");
myTodoList.addTask("Have breakfast");
myTodoList.addTask("Go to work");
myTodoList.display();

Which gets you the following output:

Here is our Todo List for My morning routine:

Task Wake up is not done
Task Go to work is not done
Task Shower is not done
Task Have breakfast is not done

As you can see, the description and completion status for each task are displayed nicely.

Wait! Why are the tasks displayed in that order? 😯

Let's check your implementation of the display method.

Refactor the display method to account for the completion state

 In the previous chapter, you implemented the display method by looping through the ArrayList. Let's see how to modify code loop over the HashMap and display the completion status along the way:

/** display displays the topic of the list
 * and each task description and completion status 
 */
public void display() {
	if(this.tasks.size()==0){
		System.out.println("Our Todo List for " + topic + "is currently empty!");
	}else {
		System.out.println("Here is our Todo List for " + topic);
		for(Entry<String, Boolean> task : this.tasks.entrySet()) {
			if (task.getValue()) {
				System.out.println("Task " + task.getKey() + " is complete");
			}else {
				System.out.println("Task " + task.getKey() + " is not done");
			}
		}
	}
}

The logic of the method remains the same:

  • If there is no task, print the topic and warn the user that the to-do list is currently empty.

  • If there is at least one task, display the key and the value for each task.

In the last chapter, you used an enumerated loop, as you relied on the order of the tasks. Since a hashMap has no order, you need to use a loop that does not rely on one either. The Java Tutorials refers to this form as the enhanced for statement. Here is the code:

for(Entry<String, Boolean> task : this.tasks.entrySet()) {

Let's break this down:

  • this.tasks.entrySet() returns the set of all key/values entries in the map. Each entry is an instance of a Map.Entry<String, Boolean> class.

  • For each iteration, one of the entries will be assigned to the task variable of type  Map.Entry<String, Boolean>

  • Then use the getKey and getValue method defined in the Map.Entry class, as you can see in the associated Javadoc

First check the value of the task and display an appropriate message if it has been completed:

if (task.getValue()) {
    System.out.println("Task " + task.getKey() + " is complete");

If not, display the opposite message:

}else {
	System.out.println("Task " + task.getKey() + " is not done");
	}
}

As you can see, the tasks are not displayed in a particular order when you use a HashMap. You have successfully refactored your method because its signature has not changed, but you have broken the ordering. Based on how the to-do list class is used, this can or not be a problem. The next chapter will show a solution that provides the task completion tracking functionality while keeping the ordering.

For now, since you are well awake, it's time to tell it to the world - or at least the program! Let's implement the markAsDone method to make it happen.

Implement the markAsDone method

To mark a task as completed, you need to:

  1. Access the task you want to mark. 

  2. Change the value of the completion status from false to true.

Here is the code:

/** markAsDone marks the task completion status as true
 * @param taskName
 */
public void markAsDone(String taskName) {
	System.out.println("Marking " + taskName + " as completed");
	if (this.tasks.containsKey(taskName)){
		this.tasks.put(taskName, true);
	}else {
		System.out.println("No such task!");
	}		
}

Since you have no task ordering to rely on, define the String taskName as a parameter. That string should match a key in the HashMap for the operation to succeed.

The containsKey method of the HashMap class does the work of checking if it does match, as you can see in the Javadoc documentation.

By using the result of the this.tasks.containsKey(taskName) expression inside an if statement, you can either do the actual marking or alert the user if there is no such key.

Here is the code that performs the marking if the key is found:

if (this.tasks.containsKey(taskName)){
	this.tasks.put(taskName, true);

Notice that we used the put method like in the addTask method.

If the key is not found, print an informative message:

}else {
	System.out.println("No such task!");
}	

That's it! Use the method in your main function and let's see how it works out:

myTodoList.markAsDone("Wake up");
myTodoList.display();
myTodoList.markAsDone("My work for the day");
myTodoList.display();

The result is unsurprising:

Marking Wake up as completed
Marking My working day as completed
No such task!
Here is our Todo List for My morning routine:

Task Wake up is complete
Task Go to work is not done
Task Shower is not done
Task Have breakfast is not done

As you can see, you have not been able to mark all your work for the day as done, since the task was not set to begin with.

Let's now move on to the removeTask method.

Implement task removal using a key

In the last chapter, you learned the following signature for the removeTask method:

/** remove removes a task at the provided index 
 * @param i The index of the task to remove
 */
public void removeTask(int i) {

You accessed it by relying on the index of your task. With no ordering available with your HashMap, you need to define another strategy. The same strategy as with the markAsDone method applies:  use the key as the access mechanism. Here is the code:

/** remove removes a task at the provided index 
 * @param nameName The name of the task to remove
 */
public void removeTask(String taskName) {
	if (this.tasks.containsKey(taskName)){
	   	System.out.println("Removing " + taskName);
		this.tasks.remove(taskName);
	}else {
		System.out.println("No such task!");
	}
}

The body of the method makes use of the remove method provided by the HashMap class.

Let's see this in action with the implementation in the main method:

myTodoList.removeTask("Have Breakfast");
myTodoList.display();

The output is:

Removing Have breakfast
Here is our Todo List for My morning routine:

Task Wake up is complete
Task Go to work is not done
Task Shower is not done

However, relying on the key makes the method sensitive to small mistakes, like typos. What happens if you try to remove the Go To Work method?

myTodoList.removeTask("Go To Work");
myTodoList.display();

The result is:

Removing Go To Work
No such task

Did you notice the difference between the Go to work task and the Go To Work string sent to the method? Java did! That's case sensitivity (T is not t) at its finest. 😉

We are almost done! Let's wrap up with the remaining method,  rename.

Implement task renaming using a key

As with removal, you no longer have access to an index due to the lack of task ordering. Here is the Java code for an implementation that relies on the task name:

/** rename renames the task at the provided index
 * 
 * @param oldTask The name of the task to rename
 * @param newTask The new description for the task
 */
	public void rename(String oldTask, String newTask) {
	    System.out.println("Renaming " + oldTask + " to " + newTask);
		if (this.tasks.containsKey(oldTask)){
			this.tasks.put(newTask, this.tasks.get(oldTask));
			this.tasks.remove(oldTask);
		}else {
			System.out.println("No such task!");
		}
	}

Since the task name is the key, renaming consists of two steps:

1. Adding a new key with the value from the old one 
2. Removing the old key

 In order to keep the completion value intact, use the result of the call to this.tasks.get(oldTask)) as the second argument to the this.tasks.put method call.

Let's see the result in action. Here is the associated code in the main method:

myTodoList.rename("Shower", "Take bath");
myTodoList.rename("Have breakfast", "Go for a run");
myTodoList.display();

This code results in:

Renaming Shower to Take bath
Renaming Have breakfast to Go for a run
No such task!

Here is our Todo List for My morning routine:

Task Wake up is complete
Task Take bath is not done
Task Go to work is not done

It works! ✨You have replaced a shower with a bath, and were prevented from renaming a task that no longer exists, since you removed it before.

Here is the final workflow:

/** MorningRoutine implements our logic workflow
 * This version adds the capability to track whether a task is completed
 */
public class MorningRoutine {

	public static void main(String[] args) {
		TodoList myTodoList = new TodoList("My morning routine");
		myTodoList.addTask("Wake up");
		myTodoList.addTask("Shower");
		myTodoList.addTask("Have breakfast");
		myTodoList.addTask("Go to work");
		myTodoList.display();
		myTodoList.markAsDone("Wake up");
		myTodoList.markAsDone("My working day");
		myTodoList.display();
		myTodoList.removeTask("Have Breakfast");
		myTodoList.display();
		myTodoList.rename("Shower", "Take bath");
		myTodoList.display();
	}

}

Compared to last chapter:

  1. You have added a  markAsDone method to mark a task as done.

  2. You are no longer relying on the order of a task to remove/rename a task. You are now sending a String to the removeTask and the rename methods. 

Nice work! ✌️

Summary

By changing the class of the field used to record the state of the list, you have been able to add a new capability: marking a task as done!  Here are a few observations about the work in this chapter:

  1. Interestingly, the way you actually use the object has changed very little. Besides the new capability of marking a task as done, you are calling the same other methods. Using the name of the task instead of the order is something you could have also done using a list.

  2. Each time you change the body of a public method without changing its signature, you use encapsulation. Encapsulation allows you to mark attributes and methods as private, so you can refactor them without impacting the way your class is used.

  3. In this example, however, when you switched from a list to a dictionary, you lost the ordering of your to-do list.. This means that operations that rely on that order no longer work in the same way.

Why can't you have it both ways? Actually, you can, and much more. Let's discuss this in the next chapter!

Example of certificate of achievement
Example of certificate of achievement