Title: Java Part 3
To ensure that thread T2 runs after T1 and thread T3 runs after T2, you can use the join()
method. This method allows one thread to wait for the completion of another.
public class ThreadExecutionOrder {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("Thread T1 is running");
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // T2 waits for T1 to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread T2 is running");
});
Thread t3 = new Thread(() -> {
try {
t2.join(); // T3 waits for T2 to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread T3 is running");
});
t1.start();
t2.start();
t3.start();
}
}
You can set an UncaughtExceptionHandler
for a thread to handle uncaught exceptions.
public class UnhandledException {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
throw new RuntimeException("An unhandled exception");
});
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Caught exception from thread: " + t.getName() + " - " + e.getMessage());
});
thread.start();
}
}
List<? extends T>
and List <? super T>
?List<? extends T>
: A list of objects that are instances of T or its subclasses. You can read from the list, but you cannot add to it (except null
).
List<? super T>
: A list of objects that are instances of T or its superclasses. You can add instances of T or its subclasses to the list, but reading from it only returns Object
.
List<String>
to a method which accepts List<Object>
?No, you cannot pass List<String>
to a method that accepts List<Object>
because of type safety. Generics are invariant in Java.
public class GenericExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
// methodAcceptingObjectList(stringList); // This will cause a compile-time error
}
public static void methodAcceptingObjectList(List<Object> list) {
// Implementation
}
}
List<?>
and List<Object>
in Java?List<?>
: A list of unknown type. You can only read from the list, but cannot add to it (except null
).
List<Object>
: A list that can hold any type of objects. You can both read from and add to the list.
You cannot create generic arrays directly in Java due to type erasure. However, you can use List
or other generic collections instead.
// This will cause a compile-time error
// T[] array = new T[10];
// You can use ArrayList instead
List<T> list = new ArrayList<>();
Sealed classes and interfaces restrict which classes can extend or implement them. Introduced in Java 15 as a preview feature and standardized in Java 17.
sealed class Shape permits Circle, Square {
}
final class Circle extends Shape {
}
final class Square extends Shape {
}
public class SealedClassExample {
public static void main(String[] args) {
Shape shape1 = new Circle();
Shape shape2 = new Square();
System.out.println(shape1 instanceof Circle); // true
System.out.println(shape2 instanceof Square); // true
}
}
In this example, only Circle
and Square
are permitted to extend Shape
. Any attempt to extend Shape
by another class will result in a compilation error.
To provide answers to the questions along with Java code snippets, let’s go through each question from the provided image and offer a detailed explanation and corresponding code.
A Thread
in Java is a lightweight process. It is the smallest unit of a process that can run concurrently with other threads.
public class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
A Process
is an instance of a program that runs independently and isolated from other processes, while a Thread
is a subset of the process that runs in shared memory space.
// Example code not required as this is a conceptual explanation
Threads can be implemented by either extending the Thread
class or implementing the Runnable
interface.
// Extending Thread class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
}
public class TestThread {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
// Implementing Runnable interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running...");
}
}
public class TestRunnable {
public static void main(String[] args) {
Thread t2 = new Thread(new MyRunnable());
t2.start();
}
}
Use Runnable
when you want to share the same task across multiple threads or when your class already extends another class.
// Runnable example provided above
The start()
method creates a new thread and executes the run()
method in that new thread, while run()
method just executes in the current thread.
class MyThread extends Thread {
public void run() {
System.out.println("Running in thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.run(); // This will run in the main thread
t1.start(); // This will run in a new thread
}
}
Runnable
is a functional interface that returns void, while Callable
returns a result and can throw a checked exception.
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public class TestCallable {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get()); // Output will be 123
}
}
CountDownLatch
is used for a single event while CyclicBarrier
can be used for multiple events.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
public class TestSynchronization {
public static void main(String[] args) throws InterruptedException {
// CountDownLatch example
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
latch.countDown();
System.out.println("Counted down");
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await();
System.out.println("All tasks completed");
// CyclicBarrier example
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Barrier reached"));
Runnable barrierTask = () -> {
try {
barrier.await();
System.out.println("Barrier released");
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(barrierTask).start();
new Thread(barrierTask).start();
new Thread(barrierTask).start();
}
}
The Java Memory Model (JMM) defines how threads interact through memory and what behaviors are allowed in concurrent executions.
// Conceptual explanation; code example not required
A volatile
variable ensures visibility of changes to variables across threads.
public class VolatileExample {
private volatile boolean flag = true;
public void run() {
while (flag) {
// Busy-wait loop
}
System.out.println("Flag changed");
}
public void stop() {
flag = false;
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
new Thread(example::run).start();
Thread.sleep(1000);
example.stop();
}
}
Thread-safety means that a class or method can be used by multiple threads concurrently without causing problems. Yes, Vector
is a thread-safe class.
// Vector is synchronized, making it thread-safe
import java.util.Vector;
public class TestVector {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
System.out.println("Vector: " + vector);
}
}
A race condition occurs when two or more threads can access shared data and they try to change it at the same time.
public class RaceConditionExample {
private int count = 0;
public void increment() {
count++;
}
public static void main(String[] args) {
RaceConditionExample example = new RaceConditionExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.count); // Expected 2000 but result may vary
}
}
You can stop a thread in Java using a boolean flag.
public class StopThreadExample {
private volatile boolean running = true;
public void run() {
while (running) {
System.out.println("Running");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void stop() {
running = false;
}
public static void main(String[] args) throws InterruptedException {
StopThreadExample example = new StopThreadExample();
Thread t = new Thread(example::run);
t.start();
Thread.sleep(500);
example.stop();
}
}
When an exception occurs in a thread, it will terminate unless the exception is caught and handled.
public class ExceptionThreadExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
throw new RuntimeException("Thread exception");
} catch (Exception e) {
System.out.println("Caught exception: " + e.getMessage());
}
});
t.start();
}
}
You can share data between threads using shared objects or data structures.
public class SharedDataExample {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public static void main(String[] args) {
SharedDataExample example = new SharedDataExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter: " + example.counter); // Should be 2000
}
}
notify
wakes up one waiting thread, while notifyAll
wakes up all waiting threads.
public class NotifyExample {
private final Object lock = new Object();
public void doWait() {
synchronized (lock) {
try {
System.out.println("Waiting");
lock.wait();
System.out.println("Woken up");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void doNotify
() {
synchronized (lock) {
System.out.println("Notifying one");
lock.notify(); // or lock.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
NotifyExample example = new NotifyExample();
Thread t1 = new Thread(example::doWait);
Thread t2 = new Thread(example::doWait);
t1.start();
t2.start();
Thread.sleep(1000);
example.doNotify();
}
}
wait
, notify
, and notifyAll
are part of the Object
class because they are meant to be used on shared objects.
A ThreadLocal
variable provides thread-local variables where each thread accessing such a variable has its own independently initialized copy.
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
System.out.println("Initial value: " + value);
threadLocal.set(value + 1);
System.out.println("Updated value: " + threadLocal.get());
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
FutureTask
represents a cancellable asynchronous computation. It can be used to wrap Callable
or Runnable
objects.
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class FutureTaskExample {
public static void main(String[] args) throws Exception {
Callable<Integer> callable = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
System.out.println("Result: " + futureTask.get()); // Outputs 42 after 1 second
}
}
interrupted()
checks the interrupt status of the current thread and clears it, while isInterrupted()
checks the interrupt status of any thread without clearing it.
public class InterruptExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Running");
}
System.out.println("Thread interrupted status: " + Thread.interrupted());
});
t.start();
try {
Thread.sleep(1000);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
wait
and notify
must be called from within a synchronized block to ensure that the current thread holds the lock on the object before calling these methods.
// Conceptual explanation; code example included in NotifyExample
To avoid spurious wakeups, it’s essential to check the waiting condition in a loop.
public class SpuriousWakeupExample {
private final Object lock = new Object();
private boolean condition = false;
public void waitForCondition() {
synchronized (lock) {
while (!condition) { // Check in a loop
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Condition met");
}
}
public void setCondition() {
synchronized (lock) {
condition = true;
lock.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
SpuriousWakeupExample example = new SpuriousWakeupExample();
new Thread(example::waitForCondition).start();
Thread.sleep(1000);
example.setCondition();
}
}
Synchronized collections are thread-safe but have a single lock for the entire collection, leading to contention. Concurrent collections, like ConcurrentHashMap
, use finer-grained locking for better scalability.
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CollectionExample {
public static void main(String[] args) {
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put("key1", "value1");
syncMap.put("key2", "value2");
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", "value1");
concurrentMap.put("key2", "value2");
System.out.println("Synchronized Map: " + syncMap);
System.out.println("Concurrent Map: " + concurrentMap);
}
}
The stack is used for static memory allocation and method execution, while the heap is used for dynamic memory allocation for objects at runtime.
// Conceptual explanation; code example not required
A thread pool manages a set of reusable threads (pool of worker threads ) for executing tasks. It improves performance by reducing the overhead associated with creating and destroying threads.
ExecutorService
:import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a thread pool with 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit 5 tasks to the thread pool
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Thread name: " + threadName);
try {
// Simulate some work with Thread.sleep
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Completed: " + threadName);
});
}
// Shut down the executor service
executor.shutdown();
}
}
Deadlocks occur when two or more threads are blocked forever, waiting for each other. To avoid deadlock, you can follow several strategies:
Avoid acquiring multiple locks if possible. If you must, always acquire the locks in the same order.
class A {
synchronized void methodA(B b) {
System.out.println("Thread 1 starts execution of methodA");
b.last();
}
synchronized void last() {
System.out.println("Inside A.last()");
}
}
class B {
synchronized void methodB(A a) {
System.out.println("Thread 2 starts execution of methodB");
a.last();
}
synchronized void last() {
System.out.println("Inside B.last()");
}
}
public class AvoidDeadlock implements Runnable {
A a = new A();
B b = new B();
AvoidDeadlock() {
Thread t = new Thread(this);
t.start();
a.methodA(b); // main thread
}
public void run() {
b.methodB(a); // child thread
}
public static void main(String[] args) {
new AvoidDeadlock();
}
}
tryLock
with TimeoutUse ReentrantLock
with tryLock
to avoid waiting indefinitely.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AvoidDeadlockWithTryLock {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
try {
if (lock1.tryLock() && lock2.tryLock()) {
// Critical section
}
} finally {
lock1.unlock();
lock2.unlock();
}
}
public void method2() {
try {
if (lock2.tryLock() && lock1.tryLock()) {
// Critical section
}
} finally {
lock2.unlock();
lock1.unlock();
}
}
}
The Thread
class does not provide a direct method to check if a thread holds a lock. However, you can use ReentrantLock
to check if the current thread holds the lock.
import java.util.concurrent.locks.ReentrantLock;
public class CheckLock {
private final ReentrantLock lock = new ReentrantLock();
public void checkLock() {
lock.lock();
try {
// Check if current thread holds the lock
if (lock.isHeldByCurrentThread()) {
System.out.println("Current thread holds the lock");
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
CheckLock example = new CheckLock();
example.checkLock();
}
}
A thread dump provides a snapshot of all the threads running in a JVM. You can take a thread dump in several ways:
jstack
CommandUse the jstack
command-line utility to get a thread dump.
jstack <pid>
Replace <pid>
with the process ID of your Java application.
jvisualvm
You can also use the jvisualvm
tool, which provides a graphical interface to take a thread dump.
You can use ThreadMXBean
to take a thread dump programmatically.
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadDump {
public static void main(String[] args) {
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMxBean.dumpAllThreads(true, true);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.toString());
}
}
}
You can control the stack size of a thread using the -Xss
JVM parameter.
java -Xss512k MyClass
This sets the stack size of each thread to 512 kilobytes.
public class SynchronizedExample {
public synchronized void syncMethod() {
// Critical section
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
}
}
java.lang.IllegalThreadStateException
.hasNext() : hasNext() method returns true if iterator have more elements.
next() : next() method returns the next element and also moves cursor pointer to the next element.
super()
- calls the base class constructor whereas
this()
- calls current class constructor.
Both this() and super() are constructor calls.
Constructor call must always be the first statement. So you either have super() or this() as first statement.
When you set a method as final
it means: “I don’t want any class override it.” But according to the Java Language Specification: JLS 8.8 - “Constructor declarations are not members. They are never inherited and therefore are not subject to hiding or overriding.”
When you set a method as static
it means: “This method belongs to the class, not a particular object.” But the constructor is implicitly called to initialize an object, so there is no purpose in having a static constructor.
When you set a method as abstract
it means: “This method doesn’t have a body and it should be implemented in a child class.” But the constructor is called implicitly when the new keyword is used so it can’t lack a body.
Constructors aren’t inherited so can’t be overridden so no use to have final
constructor
Constructor is called automatically when an instance of the class is created, it has access to instance fields of the class. so no use to have static
constructor.
Constructor can’t be overridden so no use to have an abstract
constructor.
Let’s go through each question and provide a Java code snippet where appropriate to illustrate the concepts.
Pros:
Cons:
Java Code Snippet:
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
System.out.println(Thread.currentThread().getName() + " initial value: " + value);
threadLocal.set(value + 1);
System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocal.get());
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
CompletableFuture is part of the Java java.util.concurrent
package and provides a way to handle asynchronous programming. It represents a future result of an asynchronous computation and provides methods to handle the result once it’s available, chain multiple computations, and handle errors.
Java Code Snippet:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Hello, World!";
});
// Attach a callback to be executed when the future is completed
future.thenAccept(result -> {
System.out.println("Result: " + result);
});
// Wait for the future to complete
future.get();
}
}
Streams in Java are lazy because they do not process data until a terminal operation is invoked. Intermediate operations (like map
, filter
, etc.) are only executed when a terminal operation (like collect
, forEach
, etc.) is called.
Java Code Snippet:
import java.util.Arrays;
import java.util.List;
public class StreamLazinessExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Intermediate operations are not executed yet
names.stream()
.filter(name -> {
System.out.println("Filtering: " + name);
return name.startsWith("A");
})
.map(name -> {
System.out.println("Mapping: " + name);
return name.toUpperCase();
});
System.out.println("No terminal operation yet.");
// Terminal operation triggers processing
names.stream()
.filter(name -> {
System.out.println("Filtering: " + name);
return name.startsWith("A");
})
.map(name -> {
System.out.println("Mapping: " + name);
return name.toUpperCase();
})
.forEach(name -> System.out.println("Final Result: " + name));
}
}
The peek()
method in Java Streams is an intermediate operation that allows you to perform a side-effect action (such as logging) on each element as it is processed. It is primarily used for debugging purposes to see the contents of the stream at various points.
Java Code Snippet:
import java.util.Arrays;
import java.util.List;
public class StreamPeekExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("Filtered: " + name))
.map(String::toUpperCase)
.peek(name -> System.out.println("Mapped: " + name))
.forEach(name -> System.out.println("Final Result: " + name));
}
}
When to use peek()
:
peek()
when you want to inspect the elements of the stream at a certain point in the pipeline, usually for debugging purposes.peek()
for anything that alters the state or relies on side-effects as it goes against the functional programming paradigm.ArrayList
is 10.ArrayList
needs to grow, it increases its size by approximately 50%.n
, it will be increased to n + (n >> 1)
(which is equivalent to 1.5 times the current capacity).HashMap
is 16.HashMap
is resized, its capacity is doubled.HashMap
will resize when the number of entries exceeds 75% of the current capacity.n
, it will be increased to 2 * n
.HashSet
is 16.HashMap
, a HashSet
doubles its capacity when it exceeds the load factor threshold.n
, it will be increased to 2 * n
.LinkedList
does not have a predefined capacity or resizing mechanism as it is a doubly linked list. It grows dynamically with each addition.Vector
is 10.Vector
needs to grow, it doubles its size by default.n
, it will be increased to 2 * n
.Stack
extends Vector
and hence inherits its resizing behavior.Collection Type | Initial Capacity | Resize Increase Percentage |
---|---|---|
ArrayList | 10 | 50% |
HashMap | 16 | 100% |
HashSet | 16 | 100% |
LinkedList | Dynamic | N/A |
Vector | 10 | 100% |
Stack | 10 | 100% |
TreeMap | N/A | N/A |
TreeSet | N/A | N/A |
Note: The actual resizing behavior can be influenced by custom constructors or initial capacity settings provided during instantiation.
Collection Type | Default Load Factor | Resize Percentage |
---|---|---|
ArrayList | N/A | 50% (1.5x) |
HashMap | 0.75 | 100% (2x) |
HashSet | 0.75 | 100% (2x) |
LinkedList | N/A | N/A |
Vector | N/A | 100% (2x) |
Stack | N/A | 100% (2x) |
TreeMap | N/A | N/A |
TreeSet | N/A | N/A |
Optional
ClassThe Optional
class in Java, introduced in Java 8, is a container that may or may not hold a non-null value. It is used to represent the presence or absence of a value and to avoid NullPointerException
. Optional
is often used to handle null values in a more explicit and readable way.
Optional
Creation
Optional.of(value)
: Creates an Optional
with the given non-null value.Optional.ofNullable(value)
: Creates an Optional
that can hold a null value.Optional.empty()
: Creates an empty Optional
.Checking Value Presence
isPresent()
: Returns true
if the value is present, false
otherwise.ifPresent(Consumer<? super T> action)
: Executes the given action if a value is present.Retrieving the Value
get()
: Returns the value if present; otherwise, throws NoSuchElementException
.orElse(T other)
: Returns the value if present; otherwise, returns the provided default value.orElseGet(Supplier<? extends T> other)
: Returns the value if present; otherwise, invokes the provided supplier and returns the result.orElseThrow(Supplier<? extends X> exceptionSupplier)
: Returns the value if present; otherwise, throws an exception created by the provided supplier.Transforming the Value
map(Function<? super T, ? extends U> mapper)
: Applies the provided function to the value if present and returns an Optional
with the result.flatMap(Function<? super T, Optional<U>> mapper)
: Applies the provided function to the value if present and returns the result.import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
// Creating Optional objects
Optional<String> optionalValue = Optional.of("Hello, World!");
Optional<String> emptyOptional = Optional.empty();
Optional<String> nullableOptional = Optional.ofNullable(null);
// Checking value presence
if (optionalValue.isPresent()) {
System.out.println(optionalValue.get());
}
// Using ifPresent to avoid null checks
optionalValue.ifPresent(System.out::println);
// Providing a default value
System.out.println(nullableOptional.orElse("Default Value"));
// Throwing an exception if value is not present
try {
emptyOptional.orElseThrow(() -> new IllegalStateException("Value is missing!"));
} catch (Exception e) {
System.out.println(e.getMessage());
}
// Transforming the value
Optional<Integer> length = optionalValue.map(String::length);
length.ifPresent(System.out::println);
}
}
Functional interfaces are interfaces with a single abstract method. They provide target types for lambda expressions and method references. Java 8 introduced several built-in functional interfaces in the java.util.function
package.
**Consumer
void accept(T t)
**Supplier
T get()
Function<T, R>: Represents a function that takes one argument and produces a result.
R apply(T t)
**Predicate
boolean test(T t)
**UnaryOperator
T apply(T t)
**BinaryOperator
T apply(T t1, T t2)
import java.util.function.*;
public class FunctionalInterfaceExample {
public static void main(String[] args) {
// Using a Consumer
Consumer<String> consumer = System.out::println;
consumer.accept("Hello, Consumer!");
// Using a Supplier
Supplier<String> supplier = () -> "Hello, Supplier!";
System.out.println(supplier.get());
// Using a Function
Function<String, Integer> function = String::length;
System.out.println(function.apply("Hello, Function!"));
// Using a Predicate
Predicate<String> predicate = s -> s.isEmpty();
System.out.println(predicate.test("Hello, Predicate!"));
// Using a UnaryOperator
UnaryOperator<String> unaryOperator = s -> s.toUpperCase();
System.out.println(unaryOperator.apply("Hello, UnaryOperator!"));
// Using a BinaryOperator
BinaryOperator<String> binaryOperator = (s1, s2) -> s1 + s2;
System.out.println(binaryOperator.apply("Hello, ", "BinaryOperator!"));
}
}
Optional
and Functional InterfacesA practical use case of combining Optional
with functional interfaces is in streamlining operations that might return null values. For example, fetching a user’s email address and then sending a welcome email if the address is present:
import java.util.Optional;
import java.util.function.Consumer;
public class OptionalWithFunctionalInterface {
public static void main(String[] args) {
User user = new User("john.doe@example.com");
sendWelcomeEmail(user);
}
static void sendWelcomeEmail(User user) {
Optional.ofNullable(user.getEmail())
.ifPresent(sendEmail);
}
static Consumer<String> sendEmail = email -> {
// Simulate sending email
System.out.println("Sending welcome email to " + email);
};
static class User {
private String email;
User(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
}
}
Using a functional interface instead of a normal interface in Java has several advantages, particularly in the context of functional programming introduced in Java 8. Here are some key reasons why functional interfaces are preferred in many scenarios:
Functional interfaces are designed to be implemented using lambda expressions and method references, which provide a more concise and readable way to express instances of single-method interfaces.
With a normal interface:
interface Printer {
void print(String message);
}
public class NormalInterfaceExample {
public static void main(String[] args) {
Printer printer = new Printer() {
@Override
public void print(String message) {
System.out.println(message);
}
};
printer.print("Hello, World!");
}
}
With a functional interface:
@FunctionalInterface
interface Printer {
void print(String message);
}
public class FunctionalInterfaceExample {
public static void main(String[] args) {
Printer printer = message -> System.out.println(message);
printer.print("Hello, World!");
}
}
The functional interface example is more concise and easier to read.
Java 8 introduced a set of standard functional interfaces in the java.util.function
package, which are widely used throughout the JDK and third-party libraries. These standard interfaces improve code reusability and interoperability.
Using a Consumer
from java.util.function
:
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> printer = message -> System.out.println(message);
printer.accept("Hello, World!");
}
}
Functional interfaces are essential for using the Stream API, which provides a powerful and expressive way to work with collections and other data sources.
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
}
}
In this example, filter
and forEach
use functional interfaces Predicate
and Consumer
, respectively.
Functional interfaces enable simplified syntax for implementing methods, especially when the implementation is straightforward.
Without lambda (using anonymous inner class):
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
new Thread(runnable).start();
With lambda:
Runnable runnable = () -> System.out.println("Running");
new Thread(runnable).start();
The use of lambda expressions and functional interfaces often results in code that is more readable and maintainable by reducing boilerplate code and making the intent clearer.
Using the new
Keyword:
MyClass obj = new MyClass();
This is the most common way to create an object. It allocates memory for the new object and initializes it by calling the constructor.
Using Class.forName() Method:
MyClass obj = (MyClass) Class.forName("com.example.MyClass").newInstance();
This method is useful when you need to create an instance of a class dynamically at runtime. It requires handling ClassNotFoundException
, InstantiationException
, and IllegalAccessException
.
Using Clone Method:
MyClass obj1 = new MyClass();
MyClass obj2 = (MyClass) obj1.clone();
This requires the class to implement the Cloneable
interface and override the clone()
method. It creates a copy of an existing object.
Using Deserialization:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("file.ser"));
MyClass obj = (MyClass) in.readObject();
This method is used to create an object from its serialized form. It requires the class to implement the Serializable
interface.
Using Factory Methods:
MyClass obj = MyClass.createInstance();
A factory method is a static method that returns an instance of the class. It can encapsulate the logic of instance creation and provide flexibility.
Using Builder Pattern:
MyClass obj = new MyClass.Builder().setProperty1(value1).setProperty2(value2).build();
The Builder pattern is useful for creating complex objects step by step and provides better readability and control over the object creation process.
Using Dependency Injection:
@Inject
MyClass obj;
Dependency Injection (DI) frameworks like Spring or Google Guice can manage object creation and dependency resolution. Objects are injected into the class by the framework.
Using Method Handles (Java 7 and above):
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle constructor = lookup.findConstructor(MyClass.class, MethodType.methodType(void.class));
MyClass obj = (MyClass) constructor.invoke();
Method handles provide a way to invoke methods and constructors dynamically.
Using Reflection with Constructor Class:
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass obj = constructor.newInstance();
Similar to Class.forName()
, this method uses reflection to invoke the constructor and create an instance.
Using Lambda Expressions (Java 8 and above):
Supplier<MyClass> supplier = MyClass::new;
MyClass obj = supplier.get();
A functional interface like Supplier
can be used to create an instance using a lambda expression or method reference.
In Java, volatile
and transient
are two keywords with distinct purposes related to the memory management and serialization aspects of the language. Here are the key differences between them:
volatile
Purpose:
volatile
keyword is used to indicate that a variable’s value will be modified by different threads.Concurrency Control:
Use Case:
Example:
private volatile boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
Behavior:
++
(increment) are not atomic even if the variable is volatile
.transient
Purpose:
transient
keyword is used in the context of serialization. It indicates that a field should not be serialized when an object is converted to a byte stream.Serialization Control:
Use Case:
Example:
public class User implements Serializable {
private String username;
private transient String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
// getters and setters
}
Behavior:
transient
fields are initialized to their default values (e.g., null
for objects, 0
for numeric types, false
for booleans).Feature | volatile |
transient |
---|---|---|
Purpose | Indicate variable modified by multiple threads | Prevent field from being serialized |
Context | Concurrency | Serialization |
Effect | Ensures visibility of changes across threads | Excludes field from serialization process |
Use Case | Flags, state variables shared across threads | Sensitive or temporary data within serialized objects |
Behavior | Guarantees visibility but not atomicity | Field is initialized to default value upon deserialization |
LinkedHashSet
and ConcurrentHashMap
are two distinct classes in Java Collections Framework that serve different purposes and have different characteristics. Here’s a detailed comparison between them:
LinkedHashSet
Definition:
LinkedHashSet
is a hash table and linked list implementation of the Set
interface. It maintains a doubly-linked list running through all its entries, thus preserving the insertion order.Order:
Synchronization:
LinkedHashSet
is not synchronized. If multiple threads access a LinkedHashSet
concurrently and at least one of the threads modifies the set, it must be externally synchronized.Performance:
LinkedHashSet
provides constant time performance for basic operations like add
, remove
, and contains
, assuming the hash function disperses elements properly among the buckets.Usage:
Example:
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("A");
linkedHashSet.add("B");
linkedHashSet.add("C");
ConcurrentHashMap
Definition:
ConcurrentHashMap
is a thread-safe implementation of the Map
interface. It allows concurrent access to its elements, meaning multiple threads can read and write concurrently without causing inconsistencies.Order:
Synchronization:
Performance:
Usage:
Example:
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("A", 1);
concurrentHashMap.put("B", 2);
concurrentHashMap.put("C", 3);
Feature | LinkedHashSet |
ConcurrentHashMap |
---|---|---|
Data Structure | Hash table with a linked list | Hash table |
Order | Maintains insertion order | Does not maintain any order |
Duplicates | Does not allow duplicates | Keys must be unique, values can be duplicate |
Thread Safety | Not synchronized (requires external synchronization) | Thread-safe (designed for concurrent access) |
Performance | Constant time for basic operations | Efficient concurrent operations with high throughput |
Usage | When a set with insertion order is needed | When a thread-safe map with high concurrency is needed |
LinkedHashSet
when you need a collection that does not allow duplicates and preserves the insertion order.ConcurrentHashMap
when you need a thread-safe map that can handle high levels of concurrency efficiently.In Java, iterators can be categorized into two types based on their behavior when the underlying collection is modified during iteration: fail-fast and fail-safe iterators. Understanding the difference between these two types of iterators is crucial for developing robust concurrent applications. Here’s a detailed explanation:
Definition:
ConcurrentModificationException
if they detect any structural modification (addition, removal, or update) to the collection while iterating, except through the iterator’s own methods.Collections:
java.util
package, such as ArrayList
, HashSet
, and HashMap
.Mechanism:
ConcurrentModificationException
.Usage:
Example:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
list.add("D"); // This line will throw ConcurrentModificationException
}
Drawback:
Definition:
ConcurrentModificationException
if the collection is modified during iteration. Instead, they work on a clone of the collection’s data, ensuring safe iteration.Collections:
java.util.concurrent
package, such as ConcurrentHashMap
, CopyOnWriteArrayList
, and CopyOnWriteArraySet
.Mechanism:
ConcurrentHashMap
iterators use the internal data structures that are thread-safe and updated in a way that does not interfere with iteration.Usage:
Example:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
map.put("D", 4); // No ConcurrentModificationException will be thrown
}
Drawback:
Feature | Fail-Fast Iterator | Fail-Safe Iterator |
---|---|---|
Behavior | Throws ConcurrentModificationException on modification |
Does not throw exceptions, operates on a snapshot |
Collections | ArrayList , HashSet , HashMap , etc. |
ConcurrentHashMap , CopyOnWriteArrayList , etc. |
Mechanism | Checks modification count (modCount) | Iterates over a copy or snapshot of the collection |
Thread Safety | Not thread-safe, requires external synchronization | Thread-safe, designed for concurrent modifications |
Reflects Changes | Reflects changes immediately | May not reflect the most recent changes |
Use Case | Single-threaded or manually synchronized environments | Concurrent environments where modifications during iteration are allowed |
ConcurrentHashMap
and Hashtable
are both implementations of the Map
interface in Java that support thread-safe operations. However, they have different design philosophies and performance characteristics. Here’s a detailed comparison:
Hashtable
Definition:
Hashtable
is a synchronized implementation of the Map
interface that was part of the original version of Java.Synchronization:
Hashtable
are synchronized. This means that only one thread can access a method of a Hashtable
object at a time.Concurrency:
Hashtable
is less efficient in terms of concurrency. It locks the entire table for most operations, leading to contention and reduced performance in multi-threaded environments.Null Values:
Hashtable
does not allow null keys or values. Attempting to insert a null key or value will result in a NullPointerException
.Iteration:
Hashtable
’s methods are fail-fast. If the Hashtable
is structurally modified at any time after the iterator is created, except through the iterator’s own remove
method, the iterator will throw a ConcurrentModificationException
.Legacy:
Hashtable
is considered a legacy class. It is generally recommended to use ConcurrentHashMap
or other modern concurrent collections instead.Example:
Map<String, Integer> hashtable = new Hashtable<>();
hashtable.put("A", 1);
hashtable.put("B", 2);
ConcurrentHashMap
Definition:
ConcurrentHashMap
is a highly concurrent implementation of the Map
interface, introduced in Java 5 as part of the java.util.concurrent
package.Synchronization:
ConcurrentHashMap
uses a more sophisticated locking mechanism called lock stripping. It divides the map into segments and locks only the segment being accessed by a thread, allowing greater concurrency and throughput.Concurrency:
Null Values:
Hashtable
, ConcurrentHashMap
does not allow null keys or values. Attempting to insert null will result in a NullPointerException
.Iteration:
ConcurrentHashMap
are fail-safe. They do not throw ConcurrentModificationException
because they operate on a snapshot of the map’s state at the time the iterator was created.Modern Usage:
ConcurrentHashMap
is the preferred choice for thread-safe maps in modern Java applications, particularly when high concurrency and performance are required.Example:
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("A", 1);
concurrentHashMap.put("B", 2);
Feature | Hashtable |
ConcurrentHashMap |
---|---|---|
Synchronization | Synchronized on all methods | Segment-based locking (lock stripping) |
Concurrency | Low, locks the entire table for operations | High, allows concurrent reads and segmented writes |
Null Keys/Values | Not allowed | Not allowed |
Iteration | Fail-fast (throws ConcurrentModificationException ) |
Fail-safe (no ConcurrentModificationException ) |
Performance | Generally lower due to full table locks | Generally higher due to fine-grained locking |
Usage | Legacy, not recommended for new applications | Recommended for concurrent applications |
Use ConcurrentHashMap
:
Avoid using Hashtable
in new applications. It is considered a legacy class, and ConcurrentHashMap
is a better alternative for almost all use cases that require thread-safe map implementations.
The StringJoiner
class in Java is a utility class introduced in Java 8 that provides an easy way to construct a sequence of characters separated by a delimiter. It can also optionally include a prefix and a suffix. This class is particularly useful for building strings from a collection of elements, especially when you want to insert a delimiter between each element.
StringJoiner
Delimiter:
StringJoiner
. For example, a comma (“,”) can be used as a delimiter to create comma-separated values.Prefix and Suffix:
Mutable:
StringJoiner
is mutable, meaning you can add elements to it and modify the sequence as needed.With Delimiter Only:
public StringJoiner(CharSequence delimiter)
StringJoiner
with a specified delimiter.With Delimiter, Prefix, and Suffix:
public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner
with a specified delimiter, prefix, and suffix.add(CharSequence newElement):
StringJoiner
.length():
setEmptyValue(CharSequence emptyValue):
merge(StringJoiner other):
StringJoiner
into the current one.toString():
StringJoiner
.Example 1: Basic Usage with Delimiter
import java.util.StringJoiner;
public class StringJoinerExample {
public static void main(String[] args) {
StringJoiner joiner = new StringJoiner(", ");
joiner.add("Apple");
joiner.add("Banana");
joiner.add("Cherry");
System.out.println(joiner.toString()); // Output: Apple, Banana, Cherry
}
}
Example 2: Usage with Delimiter, Prefix, and Suffix
import java.util.StringJoiner;
public class StringJoinerExample {
public static void main(String[] args) {
StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("Apple");
joiner.add("Banana");
joiner.add("Cherry");
System.out.println(joiner.toString()); // Output: [Apple, Banana, Cherry]
}
}
Example 3: Setting an Empty Value
import java.util.StringJoiner;
public class StringJoinerExample {
public static void main(String[] args) {
StringJoiner joiner = new StringJoiner(", ");
joiner.setEmptyValue("No fruits");
System.out.println(joiner.toString()); // Output: No fruits
joiner.add("Apple");
joiner.add("Banana");
System.out.println(joiner.toString()); // Output: Apple, Banana
}
}
StringJoiner
is perfect for creating comma-separated values from a list of elements.In Java 11, the Metaspace is a memory space that replaces the Permanent Generation (PermGen) from earlier versions of Java. It is part of the Java Virtual Machine (JVM) and is used to store metadata about the classes that the JVM loads. Here’s an in-depth look at Metaspace:
Definition:
Replacement for PermGen:
Native Memory:
Automatic Management:
OutOfMemoryError
if it was exhausted.Configuration Parameters:
-XX:MetaspaceSize
: Initial size of Metaspace.-XX:MaxMetaspaceSize
: Maximum size of Metaspace. If not set, Metaspace can grow until the native memory is exhausted.-XX:MinMetaspaceFreeRatio
and -XX:MaxMetaspaceFreeRatio
: These parameters control the space available for class metadata allocation before and after garbage collection.Garbage Collection:
Improved Performance and Stability:
-XX:MetaspaceSize:
-XX:MetaspaceSize=128M
-XX:MaxMetaspaceSize:
-XX:MaxMetaspaceSize=512M
-XX:MinMetaspaceFreeRatio:
-XX:MaxMetaspaceFreeRatio:
java -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=512M MyApplication
You can monitor Metaspace usage using various tools:
JVisualVM:
JConsole:
Garbage Collection Logs:
-Xlog:gc*
jstat:
jstat
tool can provide real-time statistics about the JVM, including Metaspace.OutOfMemoryError
related to class metadata.The Stream
API and Collection
API in Java are both part of the Java Collections Framework, but they serve different purposes and have different characteristics. Here’s a comparison of the two:
The Collection
API is part of the Java Collections Framework and provides a way to store and manage groups of objects. Some common types of collections are List
, Set
, and Map
.
Key Characteristics:
Examples:
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("apple");
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // Duplicate element will not be added
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("apple", 3); // Key "apple" will be updated to 3
The Stream
API, introduced in Java 8, is used for processing sequences of elements. It allows operations on data in a declarative manner (similar to SQL).
Key Characteristics:
Examples:
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> stream = list.stream();
filter
, map
, and sorted
.
Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"));
Stream<String> mappedStream = stream.map(String::toUpperCase);
collect
, forEach
, and reduce
.
List<String> result = stream.filter(s -> s.startsWith("a"))
.map(String::toUpperCase)
.collect(Collectors.toList());
Feature | Collection API | Stream API |
---|---|---|
Primary Purpose | Data storage and manipulation | Data processing and computation |
Mutability | Mutable | Immutable |
Type | Concrete data structures (List, Set, Map) | Abstract processing pipeline |
Operations | Basic CRUD operations (add, remove, update) | Functional operations (filter, map, reduce) |
Traversal | Iterators, for-each loops | Internal iteration using functional style |
Evaluation | Eager (operations executed immediately) | Lazy (operations executed when terminal operation is invoked) |
Suitability | Direct manipulation of data | Complex data processing pipelines |
Collection API:
Stream API: