Java Fundamentals Practice (Interactive)
Java — Fundamentals Practice
10 auto-graded practice problems covering Java fundamentals. Select an answer, submit, and review the explanation.
Worked Examples
Example 1: Type Promotion and Casting
public class TypePromotion \{
public static void main(String[] args) \{
// Byte arithmetic promotes to int
byte b = 10;
// b = b + 1; // ERROR: int cannot be assigned to byte
b += 1; // OK: compound assignment includes implicit cast
System.out.println("b = " + b); // b = 11
// Widening conversion (implicit)
int i = 42;
long l = i; // int -> long (widening)
double d = i; // int -> double (widening)
System.out.println("l = " + l + ", d = " + d);
// Narrowing conversion (explicit cast required)
double pi = 3.14159;
int truncated = (int) pi; // double -> int (narrowing, truncates)
System.out.println("truncated = " + truncated); // 3
// Overflow behavior
int max = Integer.MAX_VALUE;
int overflow = max + 1; // Wraps to Integer.MIN_VALUE
System.out.println("overflow = " + overflow); // -2147483648
\}
\}
Output:
b = 11
l = 42, d = 42.0
truncated = 3
overflow = -2147483648
Key insight: Java promotes byte and short to int for arithmetic operations. Use compound assignments (+=) for implicit casting. Narrowing conversions require explicit casts and may lose data.
Example 2: Streams and Functional Operations
import java.util.*;
import java.util.stream.*;
public class StreamExample \{
public static void main(String[] args) \{
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
// Filter and collect
List<String> shortNames = names.stream()
.filter(name -> name.length() <= 3)
.collect(Collectors.toList());
System.out.println("Short names: " + shortNames); // [Bob, Eve]
// Map and reduce
int totalLength = names.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println("Total length: " + totalLength); // 19
// Chaining operations
String result = names.stream()
.filter(name -> name.startsWith("C") || name.startsWith("D"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", "));
System.out.println("Result: " + result); // CHARLIE, DAVID
// Grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("By length: " + byLength);
// Parallel stream
long count = names.parallelStream()
.filter(name -> name.length() > 3)
.count();
System.out.println("Long names: " + count); // 3
\}
\}
Output:
Short names: [Bob, Eve]
Total length: 19
Result: CHARLIE, DAVID
By length: \{3=[Bob, Eve], 5=[Alice], 7=[Charlie, David]\}
Long names: 3
Key insight: Streams provide a declarative way to process collections. Intermediate operations (filter, map) are lazy. Terminal operations (collect, reduce, count) trigger processing.
Example 3: Generics and Wildcards
import java.util.*;
public class GenericsExample \{
// Bounded type parameter
public static <T extends Comparable<T>> T findMax(List<T> list) \{
T max = list.get(0);
for (T item : list) \{
if (item.compareTo(max) > 0) \{
max = item;
\}
\}
return max;
\}
// Upper bounded wildcard (read-only)
public static double sum(List<? extends Number> list) \{
double total = 0;
for (Number num : list) \{
total += num.doubleValue();
\}
return total;
\}
// Lower bounded wildcard (write-only)
public static void addNumbers(List<? super Integer> list) \{
for (int i = 1; i <= 5; i++) \{
list.add(i);
\}
\}
public static void main(String[] args) \{
List<Integer> ints = List.of(3, 1, 4, 1, 5, 9);
System.out.println("Max: " + findMax(ints)); // 9
List<Double> doubles = List.of(1.1, 2.2, 3.3);
System.out.println("Sum: " + sum(doubles)); // 6.6
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println("Numbers: " + numbers); // [1, 2, 3, 4, 5]
\}
\}
Output:
Max: 9
Sum: 6.6
Numbers: [1, 2, 3, 4, 5]
Key insight: Use ? extends T for read-only access (covariance), ? super T for write-only access (contravariance), and <T extends Bound> for type parameters with constraints.
Example 4: Concurrency with ExecutorService
import java.util.concurrent.*;
import java.util.*;
public class ConcurrencyExample \{
public static void main(String[] args) throws Exception \{
// Create a thread pool
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit tasks
List<Future<String>> futures = new ArrayList<>();
for (int i = 1; i <= 5; i++) \{
final int taskId = i;
futures.add(executor.submit(() -> \{
Thread.sleep(100 * taskId);
return "Task " + taskId + " completed";
\}));
\}
// Collect results
for (Future<String> future : futures) \{
System.out.println(future.get()); // Blocks until result is ready
\}
// Shutdown executor
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
// CompletableFuture for composition
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " " + b);
System.out.println(combined.get()); // Hello World
\}
\}
Output:
Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
Task 5 completed
Hello World
Key insight: ExecutorService manages thread pools for concurrent task execution. Future represents a pending result. CompletableFuture enables chaining and composition of async operations.
Types and Variables
Access Modifiers
Collections
Streams
Concurrency
Generics
Garbage Collection
JIT Optimization (Hard)
Class Loading Order (Hard)
Intuition
Java is a strongly typed, object-oriented language: Every variable has a type, every object is an instance of a class, and the compiler catches type errors before runtime. This structure makes Java code explicit and maintainable.
Why it matters: Java’s principles (encapsulation, inheritance, polymorphism) are foundational to software engineering and transfer to many other languages.
The key insight: Java’s checked exceptions force you to handle error cases explicitly, this is controversial but leads to more robust code when done well.
Common Mistakes
Confusing == with .equals(): == compares object references (memory addresses). .equals() compares object content. For String comparison, always use .equals() or Objects.equals(). Using == for Strings gives wrong results because of string interning.
Forgetting that arrays are objects: Java arrays are objects with a fixed length. array.length is a field, not a method (no parentheses). Confusing length (field) with length() (String method) causes compilation errors.
Ignoring checked exceptions: Java distinguishes checked (must be caught or declared) and unchecked (runtime) exceptions. Forgetting to handle or declare checked exceptions causes compilation errors. Use try-catch or add throws declarations.
Cross-References
- Site Home: Main landing page for Java notes.
- Java Basics: Fundamental Java concepts including types and control flow.
- Concurrency: Multithreading and concurrent programming.
- Practice: Practice problems for revision.