Free Ebook cover Java for Beginners: A Complete Introduction to Programming with Java

Java for Beginners: A Complete Introduction to Programming with Java

New course

10 pages

Putting It Together: Building a Small Java Console Application

Capítulo 10

Estimated reading time: 9 minutes

+ Exercise

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 Task class representing one task (description + done status).
  • A TaskListApp class 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 App

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 Java

Step 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 Java

Test 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 after nextInt().
  • Problem: IndexOutOfBoundsException when marking/removing. Fix: Validate the converted index (number - 1) with index < 0 || index >= tasks.size() before accessing the list.
  • Problem: Tasks show as object references like Task@1a2b3c. Fix: Implement toString() in Task (already included) and print the task object directly.
  • Problem: Menu loop exits unexpectedly. Fix: Confirm only choice 0 sets running = 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.

Now answer the exercise about the content:

In a console Task List app that stores tasks in an ArrayList and lets users choose tasks by number (shown as 1-based), what is the correct way to safely handle a user-entered task number before accessing the list to mark it done or remove it?

You are right! Congratulations, now go to the next page

You missed! Try again.

The menu shows tasks using 1-based numbering, but an ArrayList uses 0-based indexes. Subtract 1 to convert the number to an index, then check bounds (index < 0 or index >= size) to avoid invalid access.

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.