Java Exercises: Basic to Advanced Practice Programs (with Solutions)

I’ve watched a lot of developers “learn Java” by reading docs, skimming tutorials, and memorizing syntax—then freeze the first time they need to write a real program under time pressure. The fastest fix is boring in the best way: exercises. Small, focused programs force you to make decisions—input parsing, data types, edge cases, time/space costs—and those decisions add up to practical skill.

When you practice the right way, you’re not just writing loops. You’re building habits: validating assumptions, choosing the simplest correct approach, and proving correctness with tests. You’ll start with tiny programs (printing a message, adding numbers), then gradually stack complexity: patterns (loop reasoning), arrays and matrices (index discipline), strings (immutability and performance), and object-oriented design (modeling problems cleanly).

Below is the set of Java exercises I give to juniors and also revisit myself when I want to sharpen fundamentals. Every exercise comes with a runnable solution and notes on common mistakes and modern workflows you should adopt in 2026—like writing quick JUnit tests, using a fast input reader when needed, and asking an AI assistant to generate edge-case checklists (while you still own the final correctness).

A Modern Setup That Makes Practice Actually Stick

If your environment is annoying, you’ll “practice” less. I keep two modes: a zero-config mode for one-file programs, and a project mode for exercises that deserve tests.

Mode A: Single-file Java (fast feedback)

  • Create a file like HelloWorld.java.
  • Compile and run: javac HelloWorld.java then java HelloWorld.

Mode B: Small project with tests (better habits)

  • Use Gradle or Maven.
  • Add JUnit 5.
  • Run ./gradlew test or mvn test after each exercise.

Here’s a quick comparison of the two ways I see people practice:

Classic habit

2026 habit I recommend

Why it matters —

— Print outputs and eyeball them

Write a tiny test or at least a deterministic check

You catch regressions instantly Use Scanner everywhere

Use BufferedReader / fast scanner when input is big

Your solution doesn’t time out Ignore edge cases

Ask “what breaks this?” and prove behavior

Correctness is a skill Rewrite from scratch

Refactor in small steps

You learn design, not just syntax

I’ll show Scanner in a couple beginner programs for clarity, then switch to a fast input helper you can reuse.

Micro-Exercises: I/O, Types, and Control Flow

These are small, but they build muscle memory: reading input, producing output, and staying precise with types.

1) Hello World

Goal: confirm your toolchain and the simplest Java structure.

public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello World!");

}

}

Common mistakes I still see:

  • Wrong class name vs file name (when using javac directly): HelloWorld must match HelloWorld.java.
  • Forgetting the semicolon or the static keyword.

2) Add Two Numbers

Goal: parse numbers, handle whitespace, print result.

import java.util.Scanner;

public class AddTwoNumbers {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

long a = sc.nextLong();

long b = sc.nextLong();

System.out.println(a + b);

}

}

Notes:

  • I used long instead of int because it reduces overflow surprises in practice.
  • For competitive-programming-style input sizes, Scanner can be slow; later I’ll show a fast reader.

3) Swap Two Numbers (With and Without a Temp Variable)

Goal: understand assignment order.

With a temporary variable (clear and safe):

import java.util.Scanner;

public class SwapTwoNumbers {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int a = sc.nextInt();

int b = sc.nextInt();

int tmp = a;

a = b;

b = tmp;

System.out.println("a=" + a + " b=" + b);

}

}

Without a temp (arithmetic):

import java.util.Scanner;

public class SwapTwoNumbersNoTemp {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

long a = sc.nextLong();

long b = sc.nextLong();

// Beware overflow for large values.

a = a + b;

b = a – b;

a = a – b;

System.out.println("a=" + a + " b=" + b);

}

}

In real systems, I prefer the temp variable. It’s readable and avoids overflow.

Practice upgrade:

  • Add a third method using XOR swap for int and explain why you still shouldn’t use it in production (clarity beats clever).

4) Convert Decimal Integer to Binary

Goal: practice loops and bit reasoning.

Approach A: Use built-in conversion (fine in real apps):

import java.util.Scanner;

public class DecimalToBinaryBuiltIn {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

System.out.println(Integer.toBinaryString(n));

}

}

Approach B: Implement conversion (better for learning):

import java.util.Scanner;

public class DecimalToBinaryManual {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

if (n == 0) {

System.out.println("0");

return;

}

StringBuilder sb = new StringBuilder();

int x = n;

while (x > 0) {

sb.append(x % 2);

x /= 2;

}

// Digits were appended in reverse.

System.out.println(sb.reverse().toString());

}

}

Common mistakes:

  • Forgetting the n == 0 case.
  • Building strings with + inside a loop (slow due to repeated allocations); StringBuilder is the right tool.

Practice upgrades:

  • Support negative input by printing a sign + magnitude (or explain two’s complement vs “human-readable” binary).
  • Add a version that prints exactly 32 bits using bit masks.

5) Factorial of a Number (Handling Overflow)

Goal: loops, recursion tradeoffs, and big numbers.

Factorial grows very fast. 20! already exceeds long. So I show both long (with a limit) and BigInteger.

import java.math.BigInteger;

import java.util.Scanner;

public class FactorialBig {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

if (n < 0) {

System.out.println("Factorial is undefined for negative numbers");

return;

}

BigInteger result = BigInteger.ONE;

for (int i = 2; i <= n; i++) {

result = result.multiply(BigInteger.valueOf(i));

}

System.out.println(result);

}

}

I avoid recursion here because it adds stack depth for no gain.

Practice upgrades:

  • Implement a factorialLong(int n) that throws an exception when overflow would occur.
  • Write tests for 0!, 1!, and a mid-size value like 10!.

6) Add Two Complex Numbers (Your First Tiny Model Type)

Goal: represent a domain object and implement an operation.

public class ComplexAddition {

static final class Complex {

final int real;

final int imag;

Complex(int real, int imag) {

this.real = real;

this.imag = imag;

}

Complex add(Complex other) {

return new Complex(this.real + other.real, this.imag + other.imag);

}

@Override

public String toString() {

return real + "+" + imag + "i";

}

}

public static void main(String[] args) {

// Example: (1 + 2i) + (4 + 5i) = 5 + 7i

Complex a = new Complex(1, 2);

Complex b = new Complex(4, 5);

System.out.println(a.add(b));

}

}

Practice upgrade: parse complex numbers from input like 1+2i. Parsing is where bugs live.

Extra realism tip:

  • For parsing, define exactly what formats you support: a+bi, a-bi, spaces allowed or not, i required or not. Writing that spec first is a professional habit.

7) Simple Interest

Goal: formula implementation + type choice.

Simple interest is typically P R T / 100.

import java.util.Scanner;

public class SimpleInterest {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

double p = sc.nextDouble();

double r = sc.nextDouble();

double t = sc.nextDouble();

double si = (p r t) / 100.0;

// If you need currency-safe behavior, use BigDecimal.

System.out.println(si);

}

}

Common mistake: using integer math accidentally (dropping decimals).

Practice upgrades:

  • Re-implement with BigDecimal and rounding (this forces you to learn where floating-point is safe vs not safe).
  • Format output with 2 decimal places.

8) Sum of Fibonacci Numbers at Even Indexes

Goal: sequence generation, indexing clarity, and boundary definitions.

I’m defining the Fibonacci sequence as:

  • F0 = 0, F1 = 1
  • F2 = 1, F3 = 2, F4 = 3, ...

If the input is n, compute:

  • F0 + F2 + F4 + ... + F(2n)

Example n = 4:

  • even indexes: 0, 2, 4, 6, 8
  • values: 0, 1, 3, 8, 21
  • sum = 33

import java.util.Scanner;

public class SumFibonacciEvenIndexes {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

if (n < 0) {

System.out.println(0);

return;

}

// We need up to index 2n.

int maxIndex = 2 * n;

long a = 0; // F0

long b = 1; // F1

long sum = 0;

for (int i = 0; i <= maxIndex; i++) {

if (i % 2 == 0) {

sum += a;

}

long next = a + b;

a = b;

b = next;

}

System.out.println(sum);

}

}

Two good follow-ups:

  • Add overflow protection (switch to BigInteger after a threshold).
  • Replace the loop with a fast-doubling Fibonacci method if n can be very large.

Pattern Exercises: Loops You Can’t Fake

Pattern problems look like “printing stars,” but they’re really about controlled iteration, invariants, and off-by-one correctness. In interviews and real work, that translates to writing correct loops over grids, logs, and time windows.

I’ll show a few patterns and focus on how I reason about them.

9) Pascal’s Triangle

Key idea: each value is sum of two values above it.

For N = 5, rows (0-indexed) are:

  • 1
  • 1 1
  • 1 2 1
  • 1 3 3 1
  • 1 4 6 4 1

import java.util.Scanner;

public class PascalsTriangle {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

for (int row = 0; row < n; row++) {

long val = 1;

for (int col = 0; col <= row; col++) {

System.out.print(val);

if (col < row) System.out.print(" ");

// Compute next value in row using combinatorics:

// C(row, col+1) = C(row, col) * (row – col) / (col + 1)

val = val * (row – col) / (col + 1);

}

System.out.println();

}

}

}

Common mistake: computing factorials for combinations (slow, overflow-prone). The incremental formula is cleaner.

Practice upgrades:

  • Print the triangle centered with leading spaces.
  • Switch to BigInteger for larger n.

10) Pyramid Number Pattern

A typical variant prints centered rows increasing to a peak then decreasing. For N = 5, one common output is:

  • 1
  • 1 2 1
  • 1 2 3 2 1
  • 1 2 3 4 3 2 1
  • 1 2 3 4 5 4 3 2 1

import java.util.Scanner;

public class PyramidNumberPattern {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

for (int row = 1; row <= n; row++) {

// Leading spaces for centering

for (int s = 0; s < n – row; s++) {

System.out.print(" ");

}

// Increasing numbers

for (int x = 1; x <= row; x++) {

System.out.print(x);

System.out.print(" ");

}

// Decreasing numbers

for (int x = row – 1; x >= 1; x–) {

System.out.print(x);

if (x > 1) System.out.print(" ");

}

System.out.println();

}

}

}

Tip: decide whether your “unit” is one char wide or two (number plus space). Inconsistent spacing is where patterns fall apart.

11) Square Star Pattern

For N = 4, output:

import java.util.Scanner;

public class SquareStarPattern {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

for (int i = 0; i < n; i++) {

for (int j = 0; j < n; j++) {

System.out.print(‘*‘);

}

System.out.println();

}

}

}

12) Star Triangle (Centered)

This one varies by definition. A popular version prints stars in a centered triangle (sometimes with spaces between stars). Here’s a simple centered triangle for N = 5:

import java.util.Scanner;

public class StarTriangle {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

for (int row = 1; row <= n; row++) {

for (int s = 0; s < n – row; s++) System.out.print(" ");

for (int k = 0; k < (2 row – 1); k++) System.out.print(‘‘);

System.out.println();

}

}

}

13) Diamond Star Pattern

For N = 4, output (top half + bottom half):

  • *
  • *
  • *
  • *
  • *
  • *
  • *

import java.util.Scanner;

public class DiamondStarPattern {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

int n = sc.nextInt();

// Top half

for (int row = 1; row <= n; row++) {

for (int s = 0; s < n – row; s++) System.out.print(" ");

for (int k = 0; k < 2 row – 1; k++) System.out.print(‘‘);

System.out.println();

}

// Bottom half

for (int row = n – 1; row >= 1; row–) {

for (int s = 0; s < n – row; s++) System.out.print(" ");

for (int k = 0; k < 2 row – 1; k++) System.out.print(‘‘);

System.out.println();

}

}

}

Pattern exercises are where I recommend an AI assistant: ask it to generate test cases like N=0, N=1, N=2, and to describe expected outputs. You still should verify visually once, but the assistant helps you think about boundaries.

Array and Matrix Exercises: Index Discipline and Performance

Arrays are simple until they’re not. The moment you’re off by one, your program “works” for most inputs and fails in production. These exercises build the habit of making index ranges explicit.

A Reusable Fast Input Helper (Worth Learning)

When inputs get large, I switch from Scanner to a fast reader. I also usually pair it with a StringBuilder (or StringBuffer if I truly need thread safety) for output.

Here’s a compact FastScanner that reads from stdin. It’s intentionally strict: if input ends, it returns a sentinel so you can decide what to do.

import java.io.BufferedInputStream;

import java.io.IOException;

final class FastScanner {

private final BufferedInputStream in = new BufferedInputStream(System.in);

private final byte[] buffer = new byte[1 << 16];

private int ptr = 0, len = 0;

private int readByte() throws IOException {

if (ptr >= len) {

len = in.read(buffer);

ptr = 0;

if (len <= 0) return -1;

}

return buffer[ptr++];

}

long nextLong() throws IOException {

int c;

do {

c = readByte();

if (c == -1) return Long.MIN_VALUE;

} while (c <= ' ');

int sign = 1;

if (c == ‘-‘) {

sign = -1;

c = readByte();

}

long val = 0;

while (c > ‘ ‘) {

val = val * 10 + (c – ‘0‘);

c = readByte();

}

return val * sign;

}

int nextInt() throws IOException {

long x = nextLong();

return (int) x;

}

String next() throws IOException {

int c;

do {

c = readByte();

if (c == -1) return null;

} while (c <= ' ');

StringBuilder sb = new StringBuilder();

while (c > ‘ ‘) {

sb.append((char) c);

c = readByte();

}

return sb.toString();

}

}

Two important practical notes:

  • For huge outputs (printing lots of lines), build output using StringBuilder and print once.
  • For production code, I often prefer BufferedReader + parsing, because it’s more readable. The fast scanner is mainly for exercises with heavy input.

14) Reverse an Array

Goal: basic indexing, two-pointer thinking.

Input: n then n integers. Output: the array reversed.

import java.io.IOException;

public class ReverseArray {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int[] a = new int[n];

for (int i = 0; i < n; i++) a[i] = fs.nextInt();

int l = 0, r = n – 1;

while (l < r) {

int tmp = a[l];

a[l] = a[r];

a[r] = tmp;

l++;

r–;

}

StringBuilder out = new StringBuilder();

for (int i = 0; i < n; i++) {

if (i > 0) out.append(‘ ‘);

out.append(a[i]);

}

System.out.println(out);

}

}

Common mistakes:

  • Swapping until l <= r (harmless but redundant) vs l < r (clean).
  • Forgetting to handle n = 0 (your loops should naturally handle it).

Practice upgrades:

  • Implement “reverse in range” (reverse a subarray [L, R]). This forces you to define inclusive/exclusive boundaries.

15) Rotate an Array by K

Goal: modular arithmetic, boundary definitions.

There are two classic approaches:

  • Extra array (simple, O(n) space)
  • Reverse trick (in-place, O(1) extra space)

Here’s the in-place reverse trick for right rotation by k:

public class RotateArrayRight {

static void reverse(int[] a, int l, int r) {

while (l < r) {

int tmp = a[l];

a[l] = a[r];

a[r] = tmp;

l++; r–;

}

}

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int k = fs.nextInt();

int[] a = new int[n];

for (int i = 0; i < n; i++) a[i] = fs.nextInt();

if (n == 0) {

System.out.println();

return;

}

k %= n;

if (k < 0) k += n; // if you allow negative rotations

reverse(a, 0, n – 1);

reverse(a, 0, k – 1);

reverse(a, k, n – 1);

StringBuilder out = new StringBuilder();

for (int i = 0; i < n; i++) {

if (i > 0) out.append(‘ ‘);

out.append(a[i]);

}

System.out.println(out);

}

}

Common mistakes:

  • Forgetting k %= n.
  • Rotating left when asked to rotate right.
  • Breaking when k = 0 (should be a no-op).

16) Two Sum (Return Indices)

Goal: use HashMap, handle duplicates.

Given array a and target t, find indices i, j where a[i] + a[j] = t.

import java.util.HashMap;

import java.util.Map;

public class TwoSumIndices {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

long target = fs.nextLong();

long[] a = new long[n];

for (int i = 0; i < n; i++) a[i] = fs.nextLong();

Map firstIndex = new HashMap();

int ansI = -1, ansJ = -1;

for (int i = 0; i < n; i++) {

long need = target – a[i];

Integer j = firstIndex.get(need);

if (j != null) {

ansI = j;

ansJ = i;

break;

}

// store only the first occurrence to keep output stable

firstIndex.putIfAbsent(a[i], i);

}

System.out.println(ansI + " " + ansJ);

}

}

Practice upgrades:

  • Return 1-based indices.
  • Return all pairs (harder: avoid duplicates, think about frequency maps).

17) Prefix Sums (Fast Range Sum Queries)

Goal: trade time for precomputation.

You’ll see this pattern everywhere: analytics, logs, scoring systems, and anything with repeated queries.

Problem: given n, array a, then q queries [l, r] (inclusive), output sum a[l] + ... + a[r].

public class RangeSumQueries {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int q = fs.nextInt();

long[] pref = new long[n + 1];

for (int i = 0; i < n; i++) {

long x = fs.nextLong();

pref[i + 1] = pref[i] + x;

}

StringBuilder out = new StringBuilder();

for (int qi = 0; qi < q; qi++) {

int l = fs.nextInt();

int r = fs.nextInt();

long sum = pref[r + 1] – pref[l];

out.append(sum).append(‘\n‘);

}

System.out.print(out);

}

}

Common mistakes:

  • Off-by-one in prefix arrays. I like the convention pref[0] = 0, pref[i+1] = sum of first i+1 elements.
  • Mixing inclusive/exclusive ranges. Decide once and stick to it.

18) Matrix Transpose

Goal: nested loops, careful dimensions.

Input: r c then r*c integers. Output transposed matrix of size c x r.

public class MatrixTranspose {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int r = fs.nextInt();

int c = fs.nextInt();

int[][] m = new int[r][c];

for (int i = 0; i < r; i++) {

for (int j = 0; j < c; j++) {

m[i][j] = fs.nextInt();

}

}

StringBuilder out = new StringBuilder();

for (int j = 0; j < c; j++) {

for (int i = 0; i < r; i++) {

if (i > 0) out.append(‘ ‘);

out.append(m[i][j]);

}

out.append(‘\n‘);

}

System.out.print(out);

}

}

Practice upgrades:

  • Transpose in-place for a square matrix.
  • Implement matrix addition and multiplication (start small; validate dimensions).

19) Spiral Order Traversal

Goal: boundaries that shrink; loop invariants.

This is a classic “looks easy, is not” exercise. The trick is to define four boundaries and shrink them.

public class SpiralTraversal {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int r = fs.nextInt();

int c = fs.nextInt();

int[][] m = new int[r][c];

for (int i = 0; i < r; i++) for (int j = 0; j < c; j++) m[i][j] = fs.nextInt();

int top = 0, bottom = r – 1;

int left = 0, right = c – 1;

StringBuilder out = new StringBuilder();

while (top <= bottom && left <= right) {

for (int j = left; j <= right; j++) out.append(m[top][j]).append(' ');

top++;

for (int i = top; i <= bottom; i++) out.append(m[i][right]).append(' ');

right–;

if (top <= bottom) {

for (int j = right; j >= left; j–) out.append(m[bottom][j]).append(‘ ‘);

bottom–;

}

if (left <= right) {

for (int i = bottom; i >= top; i–) out.append(m[i][left]).append(‘ ‘);

left++;

}

}

// trim trailing space for nicer output

String s = out.toString().trim();

System.out.println(s);

}

}

Common mistakes:

  • Forgetting the boundary checks before the third and fourth passes.
  • Printing an element twice when dimensions are odd.

String Exercises: Immutability, Parsing, and Performance

Strings are where beginner code becomes slow or buggy, because you’re juggling immutability, indexing, Unicode surprises, and parsing rules.

20) Reverse Words in a Sentence

Goal: split safely, handle extra spaces.

Input: one line. Output: words reversed, single spaces between words.

import java.io.BufferedReader;

import java.io.InputStreamReader;

public class ReverseWords {

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String line = br.readLine();

if (line == null) return;

line = line.trim();

if (line.isEmpty()) {

System.out.println("");

return;

}

String[] parts = line.split("\\s+");

StringBuilder out = new StringBuilder();

for (int i = parts.length – 1; i >= 0; i–) {

if (out.length() > 0) out.append(‘ ‘);

out.append(parts[i]);

}

System.out.println(out);

}

}

Practical note:

  • For very large text, splitting creates many objects. Then you’d consider manual scanning. For exercises, split("\\s+") is fine.

21) Check Palindrome (Ignoring Non-Alphanumeric)

Goal: two pointers, input cleanup.

import java.io.BufferedReader;

import java.io.InputStreamReader;

public class PalindromeClean {

static boolean isAlphaNum(char ch) {

return Character.isLetterOrDigit(ch);

}

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

if (s == null) return;

int l = 0, r = s.length() – 1;

while (l < r) {

char cl = s.charAt(l);

char cr = s.charAt(r);

if (!isAlphaNum(cl)) { l++; continue; }

if (!isAlphaNum(cr)) { r–; continue; }

cl = Character.toLowerCase(cl);

cr = Character.toLowerCase(cr);

if (cl != cr) {

System.out.println("false");

return;

}

l++; r–;

}

System.out.println("true");

}

}

Practice upgrades:

  • Add tests for "A man, a plan, a canal: Panama".
  • Discuss Unicode: toLowerCase and “letter” behavior can be tricky across locales.

22) First Non-Repeating Character

Goal: counting, order tracking.

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.util.LinkedHashMap;

import java.util.Map;

public class FirstNonRepeating {

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

if (s == null) return;

Map freq = new LinkedHashMap();

for (int i = 0; i < s.length(); i++) {

char ch = s.charAt(i);

freq.put(ch, freq.getOrDefault(ch, 0) + 1);

}

for (Map.Entry e : freq.entrySet()) {

if (e.getValue() == 1) {

System.out.println(e.getKey());

return;

}

}

System.out.println("-");

}

}

Common mistakes:

  • Using HashMap and then expecting “first” to mean “first in the original string.” If order matters, use LinkedHashMap or a two-pass approach.

23) Parse and Validate an Email-Like String

Goal: real-world parsing and validation.

This is a great exercise because it looks trivial, then you realize you need a spec. Keep the spec intentionally simple:

  • exactly one @
  • non-empty local part and domain
  • domain contains at least one .
  • no spaces

import java.io.BufferedReader;

import java.io.InputStreamReader;

public class SimpleEmailValidator {

static boolean isValid(String s) {

if (s == null) return false;

if (s.contains(" ")) return false;

int at = s.indexOf(‘@‘);

if (at <= 0) return false;

if (s.indexOf(‘@‘, at + 1) != -1) return false;

if (at >= s.length() – 1) return false;

String domain = s.substring(at + 1);

if (!domain.contains(".")) return false;

if (domain.startsWith(".")) return false;

if (domain.endsWith(".")) return false;

return true;

}

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

System.out.println(isValid(s));

}

}

Practical value:

  • In real systems, you’d use a mature library or accept a broad range of valid emails. For practice, your mini-spec is the point.

Collections and Hashing: The Workhorse Exercises

If you can fluently use ArrayList, HashMap, HashSet, and Deque, you can build a lot of real software.

24) Frequency Counter (Top K)

Goal: frequency maps, sorting by value.

import java.util.*;

public class TopKFrequent {

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int k = fs.nextInt();

Map freq = new HashMap();

for (int i = 0; i < n; i++) {

int x = fs.nextInt();

freq.put(x, freq.getOrDefault(x, 0) + 1);

}

List<Map.Entry> list = new ArrayList(freq.entrySet());

list.sort((a, b) -> {

int byCount = Integer.compare(b.getValue(), a.getValue());

if (byCount != 0) return byCount;

return Integer.compare(a.getKey(), b.getKey());

});

StringBuilder out = new StringBuilder();

for (int i = 0; i < Math.min(k, list.size()); i++) {

out.append(list.get(i).getKey()).append(‘\n‘);

}

System.out.print(out);

}

}

Practice upgrades:

  • Implement with a min-heap of size k (better for large unique counts).
  • Add tie-breaking rules and test them.

25) Balanced Parentheses

Goal: stack discipline.

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.util.ArrayDeque;

import java.util.Deque;

public class BalancedParentheses {

static boolean matches(char open, char close) {

return (open == ‘(‘ && close == ‘)‘) ||

(open == ‘[‘ && close == ‘]‘) ||

(open == ‘{‘ && close == ‘}‘);

}

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

if (s == null) return;

Deque st = new ArrayDeque();

for (int i = 0; i < s.length(); i++) {

char ch = s.charAt(i);

if (ch == ‘(‘ |

ch == ‘[‘

ch == ‘{‘) {

st.push(ch);

} else if (ch == ‘)‘ |

ch == ‘]‘

ch == ‘}‘) {

if (st.isEmpty() || !matches(st.pop(), ch)) {

System.out.println("false");

return;

}

}

}

System.out.println(st.isEmpty() ? "true" : "false");

}

}

Common mistakes:

  • Using Stack (legacy) instead of ArrayDeque.
  • Not checking isEmpty() before popping.

Recursion and Backtracking: Controlled Exhaustive Search

These exercises teach you to write code that explores possibilities without getting lost.

26) Generate All Subsets

Goal: recursion, base cases, output formatting.

public class Subsets {

static void dfs(int[] a, int idx, StringBuilder chosen, StringBuilder out) {

if (idx == a.length) {

out.append(chosen.toString().trim()).append(‘\n‘);

return;

}

// Exclude a[idx]

dfs(a, idx + 1, chosen, out);

// Include a[idx]

int before = chosen.length();

chosen.append(a[idx]).append(‘ ‘);

dfs(a, idx + 1, chosen, out);

chosen.setLength(before);

}

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int[] a = new int[n];

for (int i = 0; i < n; i++) a[i] = fs.nextInt();

StringBuilder out = new StringBuilder();

dfs(a, 0, new StringBuilder(), out);

System.out.print(out);

}

}

Practical value:

  • This teaches you how to avoid expensive copying by mutating and rolling back (setLength(before)). That pattern is a big deal in backtracking.

27) Permutations (With Duplicates Handling)

Goal: backtracking + sorting + skip duplicates.

import java.util.Arrays;

public class UniquePermutations {

static void backtrack(int[] a, boolean[] used, int[] cur, int depth, StringBuilder out) {

if (depth == a.length) {

for (int i = 0; i < cur.length; i++) {

if (i > 0) out.append(‘ ‘);

out.append(cur[i]);

}

out.append(‘\n‘);

return;

}

for (int i = 0; i < a.length; i++) {

if (used[i]) continue;

if (i > 0 && a[i] == a[i – 1] && !used[i – 1]) continue; // skip duplicates

used[i] = true;

cur[depth] = a[i];

backtrack(a, used, cur, depth + 1, out);

used[i] = false;

}

}

public static void main(String[] args) throws Exception {

FastScanner fs = new FastScanner();

int n = fs.nextInt();

int[] a = new int[n];

for (int i = 0; i < n; i++) a[i] = fs.nextInt();

Arrays.sort(a);

StringBuilder out = new StringBuilder();

backtrack(a, new boolean[n], new int[n], 0, out);

System.out.print(out);

}

}

Common mistakes:

  • Attempting to remove duplicates using a Set<List> after generating everything (works, but wastes time and memory).
  • Getting the “skip duplicates” rule wrong. The !used[i - 1] condition is the key.

OOP Exercises: Modeling, Encapsulation, and Clean APIs

This is where practice stops being “toy problems” and starts looking like how you write maintainable Java.

28) A Small Bank Account Model

Goal: encapsulation, validation, clear method contracts.

public class BankAccountDemo {

static final class BankAccount {

private long balanceCents;

BankAccount(long initialCents) {

if (initialCents < 0) throw new IllegalArgumentException("negative initial balance");

this.balanceCents = initialCents;

}

long getBalanceCents() {

return balanceCents;

}

void deposit(long cents) {

if (cents <= 0) throw new IllegalArgumentException("deposit must be positive");

balanceCents += cents;

}

void withdraw(long cents) {

if (cents <= 0) throw new IllegalArgumentException("withdraw must be positive");

if (cents > balanceCents) throw new IllegalStateException("insufficient funds");

balanceCents -= cents;

}

}

public static void main(String[] args) {

BankAccount acc = new BankAccount(10_00); // $10.00

acc.deposit(2_50);

acc.withdraw(1_25);

System.out.println(acc.getBalanceCents());

}

}

Why this matters:

  • You’re practicing the habit of pushing rules into the model instead of scattering checks across the codebase.

Practice upgrades:

  • Add a transferTo(BankAccount other, long cents) method that is atomic in intent (either the whole transfer happens or it doesn’t).
  • Add unit tests for invalid operations.

29) Implement a Simple LRU Cache

Goal: data structures + API design.

In Java, the simplest correct LRU cache uses LinkedHashMap in access-order mode.

import java.util.LinkedHashMap;

import java.util.Map;

public class LruCacheDemo {

static final class LruCache {

private final int capacity;

private final LinkedHashMap map;

LruCache(int capacity) {

if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");

this.capacity = capacity;

this.map = new LinkedHashMap(16, 0.75f, true) {

@Override

protected boolean removeEldestEntry(Map.Entry eldest) {

return size() > LruCache.this.capacity;

}

};

}

V get(K key) {

return map.get(key);

}

void put(K key, V value) {

map.put(key, value);

}

int size() {

return map.size();

}

}

public static void main(String[] args) {

LruCache cache = new LruCache(2);

cache.put(1, "a");

cache.put(2, "b");

cache.get(1);

cache.put(3, "c");

System.out.println(cache.get(2)); // null (evicted)

}

}

Practice upgrades:

  • Write a version without LinkedHashMap (harder): combine a HashMap with a doubly-linked list.
  • Add tests that verify eviction order.

Exception Handling and Input Validation (Exercises That Feel Like Real Work)

A lot of beginner code either:

  • never validates anything, or
  • validates everything with random if statements scattered everywhere.

Practice these as exercises because they translate directly to production.

30) Robust Integer Parsing

Goal: cleanly handle invalid input, avoid crashes.

import java.io.BufferedReader;

import java.io.InputStreamReader;

public class RobustParseInt {

static Integer tryParseInt(String s) {

try {

return Integer.valueOf(s.trim());

} catch (Exception e) {

return null;

}

}

public static void main(String[] args) throws Exception {

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

Integer x = tryParseInt(s);

System.out.println(x == null ? "invalid" : x);

}

}

Practice upgrades:

  • Reject leading + or allow it (pick one, document it, test it).
  • Implement manual parsing and detect overflow.

File I/O Exercises: From Strings to Files

A surprising number of developers avoid file I/O until they have to. These exercises make it less scary.

31) Count Lines, Words, Characters

Goal: buffered reading, text processing.

import java.io.BufferedReader;

import java.io.FileReader;

public class FileStats {

public static void main(String[] args) throws Exception {

if (args.length != 1) {

System.out.println("Usage: java FileStats ");

return;

}

long lines = 0;

long words = 0;

long chars = 0;

try (BufferedReader br = new BufferedReader(new FileReader(args[0]))) {

String line;

while ((line = br.readLine()) != null) {

lines++;

chars += line.length() + 1; // +1 for newline (approx)

String trimmed = line.trim();

if (!trimmed.isEmpty()) {

words += trimmed.split("\\s+").length;

}

}

}

System.out.println("lines=" + lines);

System.out.println("words=" + words);

System.out.println("chars=" + chars);

}

}

Practical note:

  • Counting characters “exactly” depends on how you treat newlines and encodings. This is a great moment to learn that specifications matter.

Concurrency and Performance: The Advanced Muscle

Not every Java learner needs concurrency early. But if you want “advanced” practice programs, this is where you get real leverage.

32) Producer-Consumer with a BlockingQueue

Goal: thread coordination without manual locking.

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

public class ProducerConsumerDemo {

public static void main(String[] args) throws Exception {

BlockingQueue q = new ArrayBlockingQueue(5);

Thread producer = new Thread(() -> {

try {

for (int i = 1; i <= 10; i++) {

q.put(i);

}

q.put(-1); // sentinel to stop

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

});

Thread consumer = new Thread(() -> {

try {

while (true) {

int x = q.take();

if (x == -1) break;

// pretend work

System.out.println("consumed " + x);

}

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

});

producer.start();

consumer.start();

producer.join();

consumer.join();

}

}

Practice upgrades:

  • Use multiple producers/consumers.
  • Replace the sentinel with a separate stop mechanism and discuss tradeoffs.

Performance mindset:

  • Don’t optimize blindly. First make it correct, then measure. For practice, you can still reason about hotspots: string concatenation in loops, per-item I/O, and unnecessary allocations.

Capstone Mini-Projects (Where Exercises Become “Real Programs”)

If you only do micro-exercises, you’ll get good at snippets but still struggle to assemble systems. These capstones are small enough to finish, but big enough to force design.

Capstone A: CLI Todo App (In-Memory + File Save)

Skills you’ll practice:

  • Parsing commands like add, list, done , remove
  • Modeling data: Task { id, title, done }
  • Persisting tasks to a file (CSV or JSON)
  • Writing tests for parsing and state transitions

Suggested milestones:

1) Implement in-memory list

2) Add persistence

3) Add unit tests for parsing

4) Add a “search” command

Capstone B: Log Analyzer

Skills you’ll practice:

  • Reading a file line-by-line
  • Parsing timestamps
  • Grouping by keys (HashMap)
  • Producing aggregated output

Example tasks:

  • Count events per minute
  • Top N error messages
  • Detect gaps in sequences

Capstone C: Simple HTTP Client

Skills you’ll practice:

  • URL parsing
  • Timeouts
  • Handling response codes
  • Reading response body safely

I like this one because it forces you to handle the boring-but-real details: exceptions, encoding, and resource closing.

A Deliberate Practice Loop (How I Actually Use These)

The difference between “I solved it once” and “I can solve it under pressure” is repetition with reflection.

Here’s the loop I follow:

1) Solve it cleanly first. No micro-optimizations, just correctness.

2) List edge cases. I often ask an AI assistant: “What are the edge cases for this problem?” Then I manually pick the important ones.

3) Write 3–8 tests. Not 100 tests. Enough to protect the logic.

4) Refactor once. Make names clearer, extract helpers, reduce duplication.

5) Do a timed re-solve a week later. This is how you build recall.

JUnit Habit: A Minimal Test Template

If you practice with tests, you internalize correctness. Even for these exercises, a tiny test suite is worth it.

A minimal JUnit 5 test often looks like:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class ExampleTest {

@Test

void addWorks() {

assertEquals(5, 2 + 3);

}

}

In practice, I’ll pull logic out of main into a method that can be tested, like solve(...) or isValid(...). That’s not just for tests—it’s the shape of maintainable code.

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling

If you want, tell me what level you’re aiming for (beginner, interview prep, backend dev, or competitive programming), and I’ll reorder these exercises into a 2-week or 4-week schedule with daily goals and test checkpoints.

Scroll to Top