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.javathenjava HelloWorld.
Mode B: Small project with tests (better habits)
- Use Gradle or Maven.
- Add JUnit 5.
- Run
./gradlew testormvn testafter each exercise.
Here’s a quick comparison of the two ways I see people practice:
2026 habit I recommend
—
Write a tiny test or at least a deterministic check
Scanner everywhere Use BufferedReader / fast scanner when input is big
Ask “what breaks this?” and prove behavior
Refactor in small steps
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
javacdirectly):HelloWorldmust matchHelloWorld.java. - Forgetting the semicolon or the
statickeyword.
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
longinstead ofintbecause it reduces overflow surprises in practice. - For competitive-programming-style input sizes,
Scannercan 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
intand 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 == 0case. - Building strings with
+inside a loop (slow due to repeated allocations);StringBuilderis 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 like10!.
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,irequired 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
BigDecimaland 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 = 1F2 = 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
BigIntegerafter a threshold). - Replace the loop with a fast-doubling Fibonacci method if
ncan 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
BigIntegerfor largern.
10) Pyramid Number Pattern
A typical variant prints centered rows increasing to a peak then decreasing. For N = 5, one common output is:
11 2 11 2 3 2 11 2 3 4 3 2 11 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
StringBuilderand 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) vsl < 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:
toLowerCaseand “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
HashMapand then expecting “first” to mean “first in the original string.” If order matters, useLinkedHashMapor 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 == ‘{‘) {
st.push(ch);
} else if (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 ofArrayDeque. - 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 aHashMapwith 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
ifstatements 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.


