Introduction
Modern enterprise software engineering is based on Core Java, which is used for developing high-performance back-end systems, Android applications, and cloud microservices. Nevertheless, crafting good Java code is a difficult task because one has to deal with problems of thread-racing conditions and object immutability, memory allocation, and optimization of collections. To cope with such challenges, one needs to go deeper than knowing the basics of the Java language and study the architecture of the Java Virtual Machine (JVM), as well as object-oriented design patterns. It is only through mastering such challenges that the programmer becomes a highly efficient enterprise software engineer. Interested in building the base of your enterprise programming skills? Have a look at our Core Java course syllabus.
Core Java Coding Challenges and Solutions for Freshers
Newbies when writing in Core Java may hit problems of language mechanics that lead to runtime errors or performance issues. Dealing with these basic pitfalls in programming will help build the necessary muscle memory required for professional programming.
1. String Comparison Pitfall (== vs .equals())
The Challenge: String comparison using == operator instead of checking for object equality. In Java, while the == operator compares two references to see if they point to the same location in memory on the heap, the .equals() method compares the characters within the string.
While new developers may achieve success through misleading results due to String Pooling with the == operator for string literals, they face logical errors at runtime when the strings are dynamically created.
Solution: Use .equals() or .equalsIgnoreCase() method when comparing the content of Strings. Write defensive code by always having the hardcoded literal on the left-hand side of the equals check operation (e.g., “ADMIN”.equals(userRole)).
Code Exmaple:
// ❌ Incorrect: Checks memory reference; fails for dynamically generated strings
String userInput = new String(“ADMIN”);
if (userInput == “ADMIN”) {
// Fails to execute even though content matches
}
// ✅ Correct: Compares character content safely against null
if (“ADMIN”.equals(userInput)) {
// Executes reliably without throwing NullPointerException
}
2. ConcurrentModificationException During Iteration
The Challenge: Modification of a collection (insertion/deletion of items) while iterating over it in a for-each loop. In enhanced for loop, iterator is used internally. Direct modification of the collection makes the internal modification count invalid and causes an exception at runtime.
The Solution: Use Iterator and its native iterator.remove() function rather than direct removal from the list. Another way would be removal in Java 8 using removeIf(item -> item.isExpired()) or collecting items into another list for removal outside the loop.
Code Example:
List<String> users = new ArrayList<>(List.of(“Alice”, “Bob”, “Charlie”));
// ❌ Incorrect: Modifying list during for-each throws ConcurrentModificationException
for (String user : users) {
if (user.startsWith(“A”)) {
users.remove(user);
}
}
// ✅ Correct: Use removeIf() (Java 8+) to mutate the collection safely
users.removeIf(user -> user.startsWith(“A”));
3. Unchecked NullPointerException (NPE)
The Challenge: Trying to invoke methods, access fields, or unboxing the wrapper class on null object references. Novice developers tend to think that method arguments and database responses will have valid objects always, leading to application crashes immediately.
The Solution: Ensure null checks before invoking any object’s methods. Employing the use of java.util.Optional<T> for return types that may have no values and require clients to deal with empty scenarios. Use Objects.requireNonNull() for checking non-null method arguments.
Code Example:
// ❌ Incorrect: Risks NPE if user object or getName() returns null
public String getUpperName(User user) {
return user.getName().toUpperCase();
}
// ✅ Correct: Use Optional to safely handle absent values
public String getUpperName(User user) {
return Optional.ofNullable(user)
.map(User::getName)
.map(String::toUpperCase)
.orElse(“GUEST”);
}
4. Resource Leaks with Unclosed Streams
The Challenge: Failing to close file streams (FileInputStream, BufferedReader) or network connections (Socket, HttpURLConnection) in all cases when working with them. In case of exceptions being thrown before the method .close() is called explicitly, resources won’t be released properly.
The Solution: Wrapping any class implementing AutoCloseable interface into try-with-resources statement (try (BufferedReader reader = new BufferedReader(…)) { … }). It will guarantee automatic closing of all open streams on exiting the block even in case of exceptions being thrown.
Code Example:
// ❌ Incorrect: If readLine() throws an exception, close() is never called
BufferedReader reader = new BufferedReader(new FileReader(“file.txt”));
String line = reader.readLine();
reader.close();
// ✅ Correct: Try-with-resources guarantees cleanup on exit
try (BufferedReader reader = new BufferedReader(new FileReader(“file.txt”))) {
String line = reader.readLine();
} catch (IOException e) {
// Handle file exception
}
5. Inefficient String Concatenation in Loops
The Challenge: The use of + operator within loops to create large string texts. Strings in Java are immutable and every time the + operator is used, an intermediate StringBuilder object is created, along with a new string object being created on the heap.
The Solution: Create only one StringBuilder object outside the loop and keep appending characters (builder.append(data)) to the builder. Only convert the builder into a string after completing the process.
Code Example:
// ❌ Incorrect: Re-allocates a new String on the heap during every iteration
String result = “”;
for (int i = 0; i < 1000; i++) {
result += i;
}
// ✅ Correct: Uses a single mutable buffer to minimize heap allocations
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
Practice more in our Core Java Course in Chennai.
Core Java Coding Challenges and Solutions for Experienced Candidates
1. ThreadLocal Memory Leaks in Pooled Threads
The Challenge: Application server uses threads taken from pools to process requests. Keeping contextual information using ThreadLocal without cleaning up the state would retain references in Thread.threadLocals (which is ThreadLocalMap).
The thread does not end its life, so the reference, along with the classloader that loaded the object, cannot be collected, resulting in Metaspace and heap leak.
The Solution: Always execute .remove() inside a finally block to clear the state before the thread returns to the pool, or migrate to Java 21+ Scoped Values (ScopedValue.where(…)).
Code Example:
// ❌ Leaks memory in thread pools
public void process(UserContext context) {
USER_CONTEXT.set(context);
doWork(); // Missing remove() leaves reference attached to pooled thread }
// ✅ Clears context safely across all execution paths
public void process(UserContext context) {
try {
USER_CONTEXT.set(context);
doWork();
} finally {
USER_CONTEXT.remove();
}
}
2. Broken Double-Checked Locking Without volatile
The Challenge: If there is no volatile modifier, the CPU and compiler may reorder instructions so that the object reference is published to other threads even though the constructor of the target object hasn’t completed yet.
The Solution: Make the reference variable volatile in order to define a happens-before memory barrier, or use the Initialization-on-demand holder idiom.
Code Example:
// ❌ Unsafe: Reordering can expose partially constructed object
private static Helper instance;
public static Helper getInstance() {
if (instance == null) {
synchronized (Helper.class) {
if (instance == null) instance = new Helper();
}
}
return instance;
}
// ✅ Safe: volatile forces completion of writes before reference publication
private static volatile Helper instance;
public static Helper getInstance() {
Helper result = instance;
if (result == null) {
synchronized (Helper.class) {
result = instance;
if (result == null) instance = result = new Helper();
}
}
return result;
}
3. Common ForkJoinPool Exhaustion via Blocking I/O
The Challenge: Calling blocking network or file operations from parallelStream() will consume worker threads from ForkJoinPool.commonPool(). This leaves no worker threads for any other parallel streams running in the same JVM.
The Solution: Running blocking tasks in their own thread pool or performing I/O in Virtual Threads (using Executors.newVirtualThreadPerTaskExecutor()).
Code Example:
// ❌ Starves global shared common pool during network delays
urls.parallelStream().map(url -> fetchHttp(url)).toList();
// ✅ Uses Virtual Threads to handle blocking I/O efficiently
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetchHttp(url), executor))
.toList();
List<String> results = futures.stream().map(CompletableFuture::join).toList();
}
4. Silent Exception Swallowing in CompletableFuture Chains
The Challenge: Uncaught exceptions in the async pipeline chain are saved within the CompletableFuture object. If no exception handlers or terminal methods such as .join() are used in the async pipeline, exceptions occur quietly.
The Solution: Add an exception handler .exceptionally() or .handle() to the async pipeline stage itself.
Code Example:
// ❌ Fails silently; runtime exception is lost unless join() is called
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException(“Database Timeout”);
});
// ✅ Traps exception, logs error, and recovers gracefully
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException(“Database Timeout”);
}).exceptionally(ex -> {
logger.error(“Async operation failed”, ex);
return “Fallback Value”;
});
5. Heap Overhead from Mass AtomicReference Instantiations
The Challenge: Creating large numbers of AtomicReference wrapper objects for lock-free data structures leads to high memory usage (16 bytes of object header plus reference overhead per node), increasing Garbage Collection load.
The Solution: Make use of AtomicReferenceFieldUpdater to implement lock-free compareAndSet on regular volatile fields without creating any wrappers.
Code Example:
// ❌ High memory overhead: Creates an extra wrapper object per node
class ConcurrentNode {
final AtomicReference<ConcurrentNode> next = new AtomicReference<>();
}
// ✅ Zero extra allocations: Mutates volatile field directly
class ConcurrentNode {
volatile ConcurrentNode next;
private static final AtomicReferenceFieldUpdater<ConcurrentNode, ConcurrentNode> NEXT_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ConcurrentNode.class, ConcurrentNode.class, “next”);
public boolean casNext(ConcurrentNode expect, ConcurrentNode update) {
return NEXT_UPDATER.compareAndSet(this, expect, update);
}
}
Conclusion
Tackling the Core Java problems like memory management, thread safety, performance optimization in terms of concurrent threads, and JVM is important in developing applications at an enterprise level. Moving from simple concepts to design patterns which have been used by experts in the industry can take your software engineering skills to the next level.
Are you looking to build a strong base in software engineering? Enroll yourself in our software training institute in Chennai right away! Our Core Java course will provide you with the hands-on training needed to succeed in global IT companies.