Project Overview and Goal
In this chapter you will build a small but complete Java console application by combining skills you have already learned. The goal is to practice how pieces fit together: a clear program goal, a simple menu, repeated actions in a loop, decisions with conditionals, and code organization with methods and a class.
Program goal: build a Task List console app that lets a user add tasks, list tasks, mark a task as done, and remove a task. The app runs until the user chooses to exit.
Checkpoint: What you will have at the end
- A
Taskclass representing one task (description + done status). - A
TaskListAppclass with a menu loop. - Methods that keep the code readable: printing the menu, handling each action, and small helper methods.
- A short testing routine with sample runs and common fixes.
Step 1: Define the Data Model (Task Class)
Start by defining the smallest “unit” of your program: a task. Keeping task-related data and behavior in a class makes the rest of the program simpler and avoids scattered variables.
public class Task { private String description; private boolean done; public Task(String description) { this.description = description; this.done = false; } public String getDescription() { return description; } public boolean isDone() { return done; } public void markDone() { this.done = true; } @Override public String toString() { String status = done ? "[x]" : "[ ]"; return status + " " + description; } }Checkpoint: Quick sanity check
Before building the full app, confirm the class compiles. If you create a Task object and print it, you should see something like [ ] Buy milk, and after calling markDone() you should see [x] Buy milk.
Step 2: Plan the User Flow (Menu Actions)
Your console app will follow a repeating pattern:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
- Show a menu
- Read the user’s choice
- Run the chosen action
- Return to the menu until the user exits
Here is the menu we will implement:
- 1) Add a task
- 2) List tasks
- 3) Mark task as done
- 4) Remove a task
- 0) Exit
Checkpoint: Decide how tasks are stored
Use an ArrayList<Task> to store tasks in memory while the program runs. Each task will be referenced by its number in the list (1-based for user friendliness).
Step 3: Build the Application Skeleton (Loop + Menu)
Create a main application class that owns the list of tasks and coordinates the menu loop. Keep the loop in main small by delegating work to methods.
import java.util.ArrayList; import java.util.Scanner; public class TaskListApp { private static final Scanner scanner = new Scanner(System.in); private static final ArrayList<Task> tasks = new ArrayList<>(); public static void main(String[] args) { boolean running = true; while (running) { printMenu(); int choice = readInt("Choose an option: "); switch (choice) { case 1: addTask(); break; case 2: listTasks(); break; case 3: markTaskDone(); break; case 4: removeTask(); break; case 0: running = false; break; default: System.out.println("Unknown option. Try again."); } } System.out.println("Goodbye!"); } private static void printMenu() { System.out.println(); System.out.println("=== Task List ==="); System.out.println("1) Add a task"); System.out.println("2) List tasks"); System.out.println("3) Mark task as done"); System.out.println("4) Remove a task"); System.out.println("0) Exit"); } private static int readInt(String prompt) { System.out.print(prompt); while (!scanner.hasNextInt()) { scanner.next(); System.out.print("Please enter a number: "); } int value = scanner.nextInt(); scanner.nextLine(); return value; } private static String readLine(String prompt) { System.out.print(prompt); return scanner.nextLine(); } }Checkpoint: Run the skeleton
At this point the program should compile and run, showing the menu repeatedly. Options will not do much yet until you implement the action methods.
Step 4: Implement “Add Task” (Gather Input)
Adding a task is a straightforward input-to-object step: read a description, create a Task, and store it.
private static void addTask() { String description = readLine("Enter task description: "); if (description.trim().isEmpty()) { System.out.println("Task description cannot be empty."); return; } tasks.add(new Task(description.trim())); System.out.println("Task added."); }Checkpoint: Add and list count
After adding, you should see “Task added.” If you add multiple tasks, the list size should grow.
Step 5: Implement “List Tasks” (Loop Through Data)
Listing tasks demonstrates looping through a collection and formatting output for the user. Use 1-based numbering so the user can refer to tasks naturally.
private static void listTasks() { if (tasks.isEmpty()) { System.out.println("No tasks yet."); return; } System.out.println("Your tasks:"); for (int i = 0; i < tasks.size(); i++) { System.out.println((i + 1) + ") " + tasks.get(i)); } }Checkpoint: Verify formatting
Expected output should look like:
Your tasks: 1) [ ] Buy milk 2) [ ] Study JavaStep 6: Implement “Mark Task as Done” (Conditionals + Index Handling)
This feature combines input, validation, and updating an object. The user enters a task number; your code converts it to a 0-based index and checks bounds.
private static void markTaskDone() { if (tasks.isEmpty()) { System.out.println("No tasks to mark."); return; } listTasks(); int number = readInt("Enter task number to mark as done: "); int index = number - 1; if (index < 0 || index >= tasks.size()) { System.out.println("That task number does not exist."); return; } Task task = tasks.get(index); if (task.isDone()) { System.out.println("That task is already marked as done."); } else { task.markDone(); System.out.println("Task marked as done."); } }Checkpoint: Test both branches
- Mark a valid task: it should change from
[ ]to[x]. - Mark the same task again: you should see the “already marked” message.
- Enter an invalid number (0, negative, too large): you should see the bounds error message.
Step 7: Implement “Remove Task” (Mutating the List)
Removing tasks is similar to marking done: read a number, validate it, then remove from the list. After removal, the list shrinks and the remaining tasks shift positions.
private static void removeTask() { if (tasks.isEmpty()) { System.out.println("No tasks to remove."); return; } listTasks(); int number = readInt("Enter task number to remove: "); int index = number - 1; if (index < 0 || index >= tasks.size()) { System.out.println("That task number does not exist."); return; } Task removed = tasks.remove(index); System.out.println("Removed: " + removed.getDescription()); }Checkpoint: Confirm shifting behavior
If you remove task 1, the old task 2 becomes task 1 when you list again. This is expected behavior for an indexed list.
Testing the Application (Sample Runs and Verification)
Testing a console app means running it with realistic inputs and checking that outputs match your expectations. Use the following scripted runs to verify core behavior.
Test 1: Happy path
=== Task List === 1) Add a task 2) List tasks 3) Mark task as done 4) Remove a task 0) Exit Choose an option: 1 Enter task description: Buy milk Task added. Choose an option: 1 Enter task description: Study Java Task added. Choose an option: 2 Your tasks: 1) [ ] Buy milk 2) [ ] Study Java Choose an option: 3 Your tasks: 1) [ ] Buy milk 2) [ ] Study Java Enter task number to mark as done: 2 Task marked as done. Choose an option: 2 Your tasks: 1) [ ] Buy milk 2) [x] Study Java Choose an option: 4 Your tasks: 1) [ ] Buy milk 2) [x] Study Java Enter task number to remove: 1 Removed: Buy milk Choose an option: 2 Your tasks: 1) [x] Study JavaTest 2: Edge cases
Choose an option: 2 No tasks yet. Choose an option: 3 No tasks to mark. Choose an option: 4 No tasks to remove. Choose an option: 1 Enter task description: Task description cannot be empty. Choose an option: 9 Unknown option. Try again.Common issues and fixes
- Problem: After entering a number, the next text input is skipped. Fix: Ensure you consume the newline after reading an integer. In the provided
readInt,scanner.nextLine()is called afternextInt(). - Problem:
IndexOutOfBoundsExceptionwhen marking/removing. Fix: Validate the converted index (number - 1) withindex < 0 || index >= tasks.size()before accessing the list. - Problem: Tasks show as object references like
Task@1a2b3c. Fix: ImplementtoString()inTask(already included) and print the task object directly. - Problem: Menu loop exits unexpectedly. Fix: Confirm only choice
0setsrunning = false, and that other cases do not modify it. - Problem: Empty tasks are added. Fix: Trim the input and reject empty descriptions (as in
addTask).
Optional Enhancements (Next Steps)
Add stronger validation and friendlier prompts
- When listing tasks, show a hint: “Use the task number to mark done or remove.”
- When the user enters a non-number, keep prompting until a valid number is provided (already handled in
readInt). - Reject very short descriptions (for example, fewer than 2 characters).
Refactor repeated “choose a task number” logic
Both “mark done” and “remove” do the same steps: list tasks, read a number, validate, convert to index. Extract that into a helper method that returns an index or -1 if invalid.
private static int chooseTaskIndex(String prompt) { if (tasks.isEmpty()) { return -1; } listTasks(); int number = readInt(prompt); int index = number - 1; if (index < 0 || index >= tasks.size()) { System.out.println("That task number does not exist."); return -1; } return index; }Then reuse it:
private static void markTaskDone() { if (tasks.isEmpty()) { System.out.println("No tasks to mark."); return; } int index = chooseTaskIndex("Enter task number to mark as done: "); if (index == -1) return; Task task = tasks.get(index); if (task.isDone()) { System.out.println("That task is already marked as done."); } else { task.markDone(); System.out.println("Task marked as done."); } } private static void removeTask() { if (tasks.isEmpty()) { System.out.println("No tasks to remove."); return; } int index = chooseTaskIndex("Enter task number to remove: "); if (index == -1) return; Task removed = tasks.remove(index); System.out.println("Removed: " + removed.getDescription()); }Add a “Clear completed tasks” feature
Add a menu option that removes all tasks where isDone() is true. This is good practice for iterating while removing items.
private static void clearCompleted() { if (tasks.isEmpty()) { System.out.println("No tasks to clear."); return; } int removedCount = 0; for (int i = tasks.size() - 1; i >= 0; i--) { if (tasks.get(i).isDone()) { tasks.remove(i); removedCount++; } } System.out.println("Cleared " + removedCount + " completed task(s)."); }Improve the Task class with small upgrades
- Add a creation timestamp or priority field.
- Add an
unmarkDone()method and a menu option to toggle status. - Normalize descriptions (trim, collapse repeated spaces) in the constructor.