Interview Java ‐ JSE - Yash-777/MyWorld GitHub Wiki

Type casting:

  • Primitive casting (primitive ↔ primitive)
    • Widening (int → double) : Automatic Converting smaller data type → larger data type. No data loss
    • Narrowing (double → int) : Manual (explicit cast required) as Converting larger data type → smaller data type. Possible data loss
    byteshortintlongfloatdoublechar
  • Reference type casting (Object → Object) | (reference ↔ reference)
    • Upcasting (Child → Parent) (automatic & safe)
    • Downcasting (Parent → Child)

Type Conversion:

  • Boxing: Auto Converting a primitive type → wrapper object
  • Unboxing: Converting a wrapper object → primitive type
🔹 Array vs List – Most Used Methods
Purpose Arrays are fixed-size
Arrays support primitives + objects
Lists are dynamic
Lists support objects only
Size / Length arr.length (property) list.size() (method)
Access element arr[index] list.get(index)
Update element arr[index] = value list.set(index, value)
Search element Arrays.binarySearch(arr, key)
    → array must be sorted
list.indexOf(value)
    → works on unsorted list
Sort Ascending sort: Arrays.sort(arr)
Arrays.parallelSort(arr)
Descending sort: Arrays.sort(arrObj, Comparator.reverseOrder())
Ascending sort: Collections.sort(list)
Descending sort: `
Convert to String Arrays.toString(arr) list.toString()
Compare equality Arrays.equals(arr1, arr2) list1.equals(list2)
Copy Arrays.copyOf(arr, newLength)
is internally used when ArrayList grows
new ArrayList<>(list)
Add element ❌ Not possible list.add(value)
Remove element ❌ Not possible list.remove(index / value)
Check empty ❌ N/A list.isEmpty()
Contains element ❌ N/A list.contains(value)
Iterate for / enhanced for for / enhanced for / iterator
Array{int[], Integer[]} → List
Arrays.toString(arr)
List → Array{int[], Integer[]}
Object.toString()
// Premitive Array
int[] arr = {10, 40, 30, 20};
// Arrays.binarySearch( sortedArr ) If it is not sorted, the results are undefined.
System.out.println("Convert to String:"+Arrays.toString(arr));
System.out.println("Index Position of Key → array must be sorted:"+Arrays.binarySearch(arr, 30)); // -2
Arrays.sort(arr);         // Java 1.2 → Single-threaded ascending
System.out.println("Convert to String:"+Arrays.toString(arr));
System.out.println("Index Position of Key → array must be sorted:"+Arrays.binarySearch(arr, 40));
//arr[1]; // → second smallest with ascending order
//arr[arr.length - 2]; // → second highest with ascending order
Arrays.parallelSort(arr); // Java 8 → multiple threads (ForkJoinPool)
// Arrays.sort(arr, Comparator.reverseOrder()); // ❌ compile error for int[]
//
//Correct way to print primitive array - Convert to String
System.out.println("int[] contents: " + Arrays.toString(arr));
//Enhanced for-loop (Java 5)
for (int i : arr) {
    System.out.println(i);
}
// ❌ Wrong way (very common mistake) use Manual loop
//List<int[]> listWorngWay = Arrays.asList(arr); // NOT what you want
// Java 8 Streams (BEST)
List<Integer> listBoxed =  Arrays.stream(arr) // (or) IntStream.of(arr)
								.boxed()
								.collect(Collectors.toList());
System.out.println("Premitive[] → Boxed → Wrapper → List<Class-Object>:"+ listBoxed);
//
Integer[] arrObj = {10, 40, 30, 20};
Arrays.parallelSort(arrObj, Comparator.reverseOrder()); // descending - Objects
List<Integer> listObj = Arrays.asList(arrObj);
System.out.println("Wrapper[] → List<Class-Object>:"+ listObj);
    
// List<Integer> → Integer[] (Wrapper Array)
List<Integer> list = 
	Arrays.asList(10, 40, 30, 20); // Using Arrays.asList() (Java 1.2) – fixed-size
	List.of(10, 40, 30, 20);     // Java 9+ : Create immutable lists
	new ArrayList<>(Arrays.asList(10, 40, 20)); //Using new ArrayList<>(...) – mutable (recommended)
	// (Java 8) Functional style
	Stream.of(10, 40, 30, 20).collect(Collectors.toList()); // Using Stream.of()
	IntStream.of(10, 40, 30, 20).boxed().collect(Collectors.toList());// Using IntStream + boxed()
//
Integer[] arrObj = 
	list.toArray(new Integer[0]); // Java 5+ new Array
	list.toArray(Integer[]::new); // Java 8+: Using method reference (Java 8)
System.out.println("Integer[] → Object.toString():"+arrObj.toString()); // prints memory reference
//Correct way to print array contents
System.out.println("Integer[] contents: " + Arrays.toString(arrObj));
//
// List<Integer> → int[] (Primitive Array)
//Java 8+: Convert List to primitive int[]
int[] arr = list.stream()
                .mapToInt(Integer::intValue)
                .toArray();
//Correct way to print array contents - Convert to String
System.out.println("int[] contents: " + Arrays.toString(arr));
      

What is the difference between Java Streams and Collections?

Think of Collections as containers for data, and Streams as pipelines for processing that data.

  • Collections are used to store a group of objects as a single unit.
    List<String> names = new ArrayList<>();
    names.add("Alice");
    names.add("Bob");
    Here, names is a single collection object holding multiple String objects.
  • Streams are used to process data from a source, typically a collection, in a declarative and functional programming style.

🔥 Java 8 introduced functional programming via lambdas, streams, Optional, and a modern Date-Time API. 👇

🔹 Optional is a Container object. Example to avoid NullPointerException?
Optional : Container object Example
public final class Optional<T> {
    private static final Optional<?> EMPTY = new Optional<>(null);
    private final T value;
    private Optional(T value) {
        this.value = value;
    }
    // static
    public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
    }
    public static <T> T requireNonNull(T obj) {
        if (obj == null) throw new NullPointerException();
        return obj;
    }
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? (Optional<T>) EMPTY
                             : new Optional<>(value);
    }
    // instance
    public boolean isPresent() {
        return value != null;
    }
    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }
    public T orElse(T other) {
        return value != null ? value : other;
    }
    // @since Java 11+
    public boolean isEmpty() {
        return value == null;
    }
}
      
//A container object that may or may not contain a value.
Optional<Integer> opt = 
    Optional.of(10); // Runtime Object created
    Optional.ofNullable(null);
    Optional.empty();
opt = Optional.ofNullable(opt).orElse( Optional.of(10) );
System.err.println("Optional.of(a) :"+ opt);
System.err.println("Optional.empty():"+ opt.isEmpty());
System.err.println("Optional isPresent():"+ opt.isPresent());
// System.err.println("Optional isPresent() get():"+ opt.get()); // NoSuchElementException: No value present
//
if (!opt.isEmpty() && opt.isPresent()) {
    System.err.println("Correct way :" + opt.get());
} else {
    System.err.println("A container object does not contain a value.");
}
      
Name What / Why Example
Lambda Expression Write anonymous functions concisely
reduces boilerplate
(parameters) -> expression;
(or)
(parameters) -> { statements };
Functional Interface Single abstract method; enables lambdas @FunctionalInterface
interface Comparator<T>
-> compare(T o1, T o2):int
Default Method To add new methods to existing interfaces without breaking all the implementing classes.
Before Java 8, adding a method to an interface meant: ❌ Every implementing class had to implement it
Default methods solve this by providing a method body.
default Comparator<T> thenComparing(Comparator<?> other)
default Comparator<T> reversed()
Accessed via object / implementing class
Static Methods in Interface To group utility/helper methods logically with the interface itself.
Before Java 8: Utility methods were in separate *Utils classes
Java 8: Utilities live inside the interface
int sum = MathUtil.add(2, 3);
static Comparator<T> comparing(Function<?, ?> keyExtractor)
Accessed only via interface name
Stream API Functional data processing; readable & parallel list.stream().filter(x -> x > 5)
Method Reference Shorthand for lambda System.out::println
Optional Avoids NullPointerException Optional.ofNullable(emp).orElse(Collections.emptyList())
forEach Internal iteration list.forEach(System.out::println)
Predicate Boolean condition e -> e.getAge() > 30
Function Transform data e -> e.getName()
Consumer Performs action System.out::println
Supplier Supplies value without input () -> LocalDate.now()
Parallel Stream Multi-threaded processing list.parallelStream()
Date & Time API Immutable, thread-safe date handling LocalDate.now()
Nashorn JavaScript Engine Run JS code inside JVM jjs script.js
Base64 API Built-in Base64 encoding/decoding Base64.getEncoder().encodeToString(data)
Improved Map APIs Cleaner map operations map.putIfAbsent(k,v)
Parallel Array Sorting Faster sorting for large arrays Arrays.parallelSort(arr)
Java 8 → multiple threads (ForkJoinPool)
Stream Collectors Enhancements More powerful collectors Collectors.joining()

In Java, streams are a powerful API introduced in Java 8 that enable functional-style operations on sequences of elements, such as collections or arrays, without modifying the original data source.

  • Stream = single processing pipeline, Uses one thread.
  • Parallel Stream = multiple pipelines running at the same time using multiple threads (ForkJoinPool). JVM decides the number of threads based on available CPU cores

Stream Pipeline Stages:

Type of Stream
stream() / parallelStream() creates a stream from the collection
Intermediate operations → lazy, return a Stream
filter() keeps only elements matching a predicate
map() transforms each element
Terminal operations
→ trigger execution, return result/value or void
collect() gathers the results into a collection
→ Provides data (collection, array, I/O, etc.) to create a stream.
myList       // Source
.stream()  // Sequential Stream
      
→ Execution mode can be modified by the BaseStream.sequential() or BaseStream.parallel() methods
.stream().parallel()
(or)
.parallelStream().sequential()
      
→ Take the stream of data from the pipeline (lazy - not executed immediately)
→ Perform intermediate processing (e.g., filter, map)
→ Return a new stream to the pipeline
 .filter(n -> n > 10)  // Intermediate operation
 .map(n -> n * 2)      // Intermediate operation
      
→ Predicate - Boolean Values Function → used in filter(...)
→ Function - function on apply transforms T → R → used in map(...)
Predicate<Integer> greaterThan10 = n -> n > 10;
Function<Integer, Integer> doubleValue = n -> n * 2;
  .filter(greaterThan10)
  .map(doubleValue)
      
→ Triggers execution
→ Produces the final result (print, collect, reduce)
.forEach(System.out::println);
  (or)
.forEach( (e) -> { System.out.println(e); } );
      
    Stream pipelines may execute either sequentially(one thread) or in parallel(multiple threads).
  • → Streams are created with an initial choice of sequential or parallel execution.
      For example
    • Collection.stream() creates a sequential stream
    • Collection.parallelStream() creates a parallel one.
  • → This choice of execution mode may be modified by the BaseStream.sequential() or BaseStream.parallel() methods, and may be queried with the BaseStream.isParallel() method.

Arrays.asList() → Java 5+ List.of() → Java 9+
// int[] or Integer[]
Integer[] numbersArr = {5, 10, 15, 20, 7, 2}; // {} syntax → array
// Collection
List<Integer> numbersList = 
	Arrays.asList(5, 10, 15, 20, 7, 2); // Java 5+ : Arrays.asList() → List
Integer[] numbersListArr = numbersList.toArray( new Integer[0] );
      
    List.of() creates an immutable list with fixed elements and does not allow null values.
  • ✅ Immutable list (cannot be modified)
  • add(), remove(), set() → throw UnsupportedOperationException
  • ❌ Does not allow null values
List<Integer> immutableList = List.of(5, 10, 15, 20, 7, 2); // Java 9+ 
immutableList.add(25);   // ❌ Runtime exception
//
List<Integer> mutableList = new ArrayList<>( immutableList );
mutableList.add(25); // ✅ works
      
Stream
    Stream works with objects (wrapper / class types)
  • 👉 Stream<Character>, Stream<Integer>, Stream<String>
Example
Primitives are handled by special streams:
IntStream / LongStream / DoubleStream


Stream<Integer> → for wrapper classes
//Primitive values
IntStream    ofInt    = IntStream.of(1, 2, 3);
IntStream    ofChar   = IntStream.of('Y', 'a', 's', 'h');
LongStream   ofLong   = LongStream.of(1L, 2L);
DoubleStream ofDouble = DoubleStream.of(1.0, 2.0);
      
    Character / String → Stream
  • String (words)
     → Arrays.stream(str.split(",")) : Stream<String>
  • String (characters)str.chars() : IntStream
  • String → Character
     → str.chars().mapToObj(c -> (char) c) : Stream<Character>
     Convert primitive → object using mapToObj()

String orElseStr = Optional.ofNullable(str).orElse("");
This avoids NullPointerException cleanly 👍
 
String str = "Yash";
//
// String → IntStream (mapToObj → convert each int to Character) → Stream<Character>
// Stream from char[] (str.toCharArray())
char[] charArray = str.toCharArray();
Stream<Character> stream =
        IntStream.range(0, charArray.length)
                 .mapToObj(i -> charArray[i]);
// Cleaner & recommended (from String directly)
IntStream chars = str.chars();
Stream<Character> streamIntChars = chars.mapToObj(c -> (char) c);
//
// String → Stream (using split)
String[] splitStr = str.split(",");
Stream<String> streamStrSplit = Arrays.stream( splitStr );
      
    Array → Stream
    • Array (Object) → Arrays.stream(arr) : Stream<T>
    • Array (Primitive) → Arrays.stream(intArr) : IntStream
String[] splitStr = str.split(",");
Stream<String> streamStrSplit 
	= Arrays.stream( splitStr );
	//or
	= Stream.of( splitStr );
//
int[] numbersArr = {5, 10, 15, 20, 7, 2}; // {} syntax → array
IntStream ofIntArr = Arrays.stream(numbersArr);
      
    Collection → Stream
    • list.stream() : Stream<T>
    • set.stream() : Stream<T>
    • queue.stream() : Stream<T>
List<String> list = null; // or maybe empty
Collection<String> safeList =
        Optional.ofNullable(list).orElse(Collections.emptyList());
safeList.stream().forEach(System.out::println);
//
Set<Integer> set = null; // or maybe empty
Collection<Integer> safeSet =
        Optional.ofNullable(set).orElse(Collections.emptySet());
safeSet.stream().forEach(System.out::println);
      
    Map → Stream
    • Map (entries) map.entrySet().stream() : Stream<Map.Entry<K,V>>
    • Map (keys) map.keySet().stream() : Stream<K>
    • Map (values) map.values().stream() : Stream<V>
Map<Integer, String> map = Map.of(
        1, "Apple",
        2, "Banana",
        3, "Cherry"
);
      
//✅ Map entries → Stream>
map.entrySet()
   .stream()
   .forEach(e ->
        System.out.println(e.getKey() + " = " + e.getValue()));
//✅ Map keys → Stream
map.keySet()
   .stream()
   .forEach(System.out::println);
//✅ Map values → Stream
map.values()
   .stream()
   .forEach(System.out::println);
      

Runnable: class vs anonymous class vs lambda.

Using a concrete class (Task) Using an anonymous inner class Using a lambda expression
We create a separate class that implements `Runnable` and override `run()`. An object of this class is passed to `Thread`. This approach is verbose but reusable and suitable when the logic is complex or used in multiple places.
public class Task implements Runnable {
  @Override public void run() {
    System.out.println("Running...")
  }
}
// Usage
Thread t = new Thread( new Task() );
t.start()
      
We implement `Runnable` inline without creating a named class. This reduces the number of classes but still has boilerplate code. Commonly used before Java 8 for one-time implementations.
Runnable anonymusFun = new Runnable() {
  @Override public void run() {
	System.out.println("Running...")
  }
};
// Usage
Thread t2 = new Thread( anonymusFun );
t2.start();
      
Since `Runnable` is a functional interface (only one abstract method), we can use a lambda. It is the most concise, readable, and preferred approach in Java 8+ for simple tasks.
Runnable lambda = () -> System.out.println("Running...");
// Usage
Thread t3 = new Thread( lambda );
t3.srart();
//
// ### Runnable with stream (start threads)
List&lt;Runnable&gt; tasks = List.of(
        () -> System.out.println("Task 1"),
        () -> System.out.println("Task 2")
);
tasks.stream()
     .forEach(r -> new Thread(r).start());
      
class Task implements Callable<Integer> {
    public Integer call() throws Exception {
        return 100;
    }
}
// Usage
ExecutorService ex = Executors.newSingleThreadExecutor();
Future<Integer> f = ex.submit( new Task() );
System.out.println(f.get());
ex.shutdown();
      
Callable<Integer> callableTask = new Callable<Integer>() {
    public Integer call() {
        return 200;
    }
}
// Usage
ExecutorService ex = Executors.newSingleThreadExecutor();
Future<Integer> f = ex.submit( callableTask );
System.out.println(f.get());
ex.shutdown();
      
Callable<Integer> callableTask = () -> 300;
// Usage
ExecutorService ex = Executors.newSingleThreadExecutor();
Future<Integer> f = ex.submit( callableTask );
System.out.println(f.get());
ex.shutdown();
//
// ### Callable with stream (submit tasks)
List<Callable<Integer>> tasks = List.of(
        () -> 10,
        () -> 20,
        () -> 30
);
ExecutorService ex = Executors.newFixedThreadPool(3);
List<Future<Integer>> results =
        tasks.stream()
             .map(ex::submit)
             .toList();
for (Future<Integer> f : results) {
    System.out.println(f.get());
}
ex.shutdown();
      
class Task implements Supplier {
    public Integer get() {
        return 100;
    }
}
//
CompletableFuture<Integer> cf =
        CompletableFuture.supplyAsync(new Task());
//
System.out.println(cf.join());
      
Supplier taskSupplier = new Supplier() {
            public Integer get() {
                return 200;
            }
        };
//
CompletableFuture<Integer> cf = 
    CompletableFuture.supplyAsync( taskSupplier );
System.out.println(cf.join());
      
Supplier taskSupplier = () -> 300;
//
CompletableFuture<Integer> cf = 
    CompletableFuture.supplyAsync( taskSupplier );
System.out.println(cf.join());
//
// ### CompletableFuture with Stream (launch async tasks)
Function<Integer, Supplier<Integer>> taskSupplier = n -> () -> n * 2;
List<CompletableFuture<Integer>> futures =
        List.of(10, 20, 30).stream()
            //.map(n -> CompletableFuture.supplyAsync( () -> n * 2 ))
            .map(n -> CompletableFuture.supplyAsync( taskSupplier.apply(n) ))
            .toList();
List<Integer> results =
        futures.stream()
               .map(CompletableFuture::join)
               .toList();
System.out.println(results); // [20, 40, 60]
      
Task with Runnable → Callable → CompletableFuture → parallelStream
Aspect Runnable
──► fire-and-forget (no result)
Callable
──► background task (returns result)
CompletableFuture
──► async + chaining + non-blocking
parallelStream
──► data-parallel processing
Purpose Run task Run task + result Async workflow Parallel data processing
Returns value
Exception handling ✅ (checked) ✅ (exceptionally / handle) ⚠️ Limited
Blocking ✅ get() ❌ optional join() ❌ (terminal op blocks)
Chaining ✅ (thenApply, thenCompose)
Thread control Manual ExecutorService Custom Executor ForkJoinPool
Best for Fire-and-forget Background task Microservices / async I/O CPU-bound collections
Avoid when Need result Need chaining Simple tasks I/O or blocking calls

Using CompletableFuture (BEST choice) 🏆 : Complex async workflow REST / DB / I/O calls

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userClient.getUser(orderId));

CompletableFuture<Inventory> inventoryFuture =
        CompletableFuture.supplyAsync(() -> inventoryClient.getInventory(orderId));

CompletableFuture<Price> priceFuture =
        CompletableFuture.supplyAsync(() -> pricingClient.getPrice(orderId));

CompletableFuture<OrderResponse> orderFuture =
        CompletableFuture.allOf(userFuture, inventoryFuture, priceFuture)
            .thenApply(v -> new OrderResponse(
                    userFuture.join(),
                    inventoryFuture.join(),
                    priceFuture.join()
            ));

OrderResponse response = orderFuture.join();

What is a Lambda Expression?

Statement Syntax
  • A lambda expression is a short, anonymous function that represents an implementation of a functional interface.
  • A lambda can only be used with a functional interface.
  • Introduced in Java 8, lambdas let you write less boilerplate code.
(parameters) -> expression
(or)
(parameters) -> { statements }
      

Functional interface : An interface with exactly one abstract method.

Predicate / Function / Consumer / Supplier Built-in functional interfaces. Reusable lambdas

@FunctionalInterface Examples 👇
    Predicate<T> is used in filter() to test a condition and decide whether an element should be kept or removed.
  • → 👉 used in filter() → returns boolean
// Predicate<T> 👉 Checks a condition, returns boolean 📌 Used for filtering
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
//
// Primitive version of Predicate<Integer>
@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}
@FunctionalInterface
public interface LongPredicate {
    boolean test(long value);
}
@FunctionalInterface
public interface DoublePredicate {
    boolean test(double value);
}
//
// -------------------------------------------------------------------------
// BiPredicate<T, U> : BiPredicate<T, U>
@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}
      
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(10)); // true
// -------------------------------------------------------------------------
// Using negate()
IntPredicate isOdd_Negate = isEven.negate();
System.out.println("IntPredicate.negate(): "+isOdd_Negate.test(10));  // false
//
//Chaining with and(), or() for the same Primitive type.
IntPredicate isGreaterThan10 = n -> n > 10;
IntPredicate evenAndGreaterThan10 = isEven.and(isGreaterThan10);  // and()
System.out.println("IntPredicate.and(): "+evenAndGreaterThan10.test(12)); // true
//
LongPredicate isOdd = n -> n % 2 != 0;
System.out.println("LongPredicate: "+isOdd.test(11)); // true
LongPredicate isOdd_Negate2 = isOdd.negate();
System.out.println("LongPredicate.negate(): "+isOdd_Negate2.test(11)); // false
//
// -------------------------------------------------------------------------
BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;
System.out.println("Validation Logic:"+isGreater.test(10, 5)); // true
      
    Function<T, R> is used in map() to transform an element of type T into a result of type R.
  • → 👉 used in map (type change) → “Transform something” → result
  • → changes type (If input type ≠ output type → Function)
//Function<T, R> 👉 Transforms input into output
@FunctionalInterface
public interface Function<T, R> {
	// <T> input and <R> result of the function.
	R apply(T t);
}
// Primitive specializations (more efficient 🚀)
@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}
@FunctionalInterface
public interface LongFunction<R> {
    R apply(long value);
}
@FunctionalInterface
public interface DoubleFunction<R> {
	R apply(double value);
}
//
// -------------------------------------------------------------------------
// Function that accepts two arguments and produces a result.
// T - first argument, U - second argument, R - result
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
// 🧠 Used when result depends on two parameters
      
Function<Integer, Integer> sum = e -> e + 2;
System.out.println(sum.apply(5)); // 7
// -------------------------------------------------------------------------
IntStream    ofInt    = IntStream.of(1, 2, 3);
IntStream    ofChar   = IntStream.of('Y', 'a', 's', 'h');
LongStream   ofLong   = LongStream.of(1L, 2L);
DoubleStream ofDouble = DoubleStream.of(1.0, 2.0);
//
// AutoBox from primitive-type (int) to wrapper-class (Integer)
IntFunction<Integer> intAutoBoxFunction = i -> (int) i;
System.out.println("AutoBoxFunction (int) :"+ intAutoBoxFunction.apply('a') );
// widening primitive-type (char → int) then AutoBox (int → Integer)
IntFunction<Character> charWidenAutoBoxFunciton = c -> (char) c;
System.out.println("Widening → AutoBoxFunction (char) :"+ charWidenAutoBoxFunciton.apply('a') );
//
LongFunction<Long> longAutoBoxFunciton = c -> (long) c;
System.out.println("AutoBoxFunction (long) :"+ longAutoBoxFunciton.apply( 2L ) );
//
DoubleFunction<Double> doubleAutoBoxFunciton = d -> (double) d;
System.out.println("AutoBoxFunction (double) :"+ doubleAutoBoxFunciton.apply( 2.09000099d ) ); // 2.09000099
// A float: 32 bits and ~7 decimal digits precision [float f = 2.09000099f → as 7 decimal uses 2.090001106262207 ← closest float value]
System.out.println("Widening → AutoBoxFunction (float) :"+ doubleAutoBoxFunciton.apply( 2.09000099f ) ); // 2.090001106262207
//
// -------------------------------------------------------------------------
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
System.out.println("BiFunction combine two values:"+sum.apply(5, 3)); // 8
//🔥 Very common in streams / factories - build an object
BiFunction<String, Integer, Employee> employeeCreator = Employee::new;
Employee emp = employeeCreator.apply("Yash", 25);
System.out.println("BiFunction Emp:Obj:"+ emp); // 8
      
    UnaryOperator<T> is used in map() to modify an element while keeping the same input and output type.
  • → 👉 used in map (same type)
  • → same type (If input type == output type → UnaryOperator)
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
}
      
//Function: type changes
Function<String, Integer> length = String::length;
System.out.println("Function: "+ length.apply("Yash")); // 4
//UnaryOperator: type stays the same
UnaryOperator<String> toUpper = String::toUpperCase;
System.out.println("UnaryOperator: "+toUpper.apply("Yash")); // YASH
//
// Increase salary by 10%
List<Double> salaries = List.of(500.0, 600.0, 700.0);
UnaryOperator<Double> giveRaise = salary -> salary * 1.10;
List<Double> updatedSalaries = salaries.stream()
        .map(giveRaise)
        .toList(); //  [550.0, 660.0, 770.0000000000001]
System.out.println("UnaryOperator: "+ updatedSalaries);
      
    BinaryOperator<T> is used in reduce() to combine two values of the same type into a single result.
  • → 👉 used in reduce() → combines values
// BinaryOperator in reduce()
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}
      
BinaryOperator<Integer> max = Integer::max; 
//→ (a, b) -> Math.max(a, b); → (a >= b) ? a : b;
System.out.println("BinaryOperator Integer::max: "+ max.apply(2, 5)); // 5
// 👉 Optional<T> reduce(BinaryOperator<T> accumulator);
int highest = numbers.stream()
        .reduce(max)
        .orElse(0);
System.out.println("Find maximum value: "+ highest); // 9
//
BinaryOperator<Integer> sumOperator = Integer::sum; //(a, b) -> a + b;
System.out.println("BinaryOperator Integer::sum: "+ sumOperator.apply(2, 5)); // 7
// 👉 T reduce(T identity, BinaryOperator<T> accumulator);
List<Integer> numbers = List.of(1, 2, 3, 4, 9, 5);
int sumOfList = numbers.stream()
        .reduce(0, sumOperator);
System.out.println("Sum of integers: "+ sumOfList); // 15
      
    Consumer<T> is used in forEach() to perform an action on an element without returning any result.
  • → 👉 used in forEach() → “do something, returns nothing
  • → Consumes data, performs action, returns nothing
    Ex: (Worker: logs, saves, sends)
// Consumer<T> 👉 Consumes input, returns nothing
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
//📌 Used for side-effects (logging, printing)
      
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello"); // Hello
//
// Sending notification / email 📌 Real use: messaging, alerts, integrations
Consumer<String> sendEmail = email -> emailService.send(email);
// emails stream
emails.forEach(sendEmail);
//
// Saving data to database (simulation) 📌 Real use: persistence layer, side effects
Consumer<Order> saveOrder = order -> orderRepository.save(order);
// orders stream
orders.forEach(saveOrder);
      
    Supplier<T> is used to provide or generate values without taking any input.
  • → 👉 used in generate() / orElseGet() → “give me something, takes nothing
  • → Supplies data, takes no input
    Ex: (Factory: creates, fetches, generates)
// Supplier 👉 Supplies a value, no input
@FunctionalInterface
public interface Supplier<T> {
    T get();
}
//📌 Used for lazy creation
      
Supplier<Double> random = () -> Math.random();
System.out.println(random.get());
//
// Generate unique IDs: 📌 Real use: IDs, tokens, request tracing
Supplier<UUID> idSupplier = UUID::randomUUID;
UUID tokenId = idSupplier.get();
System.out.println("Supplier: "+ tokenId); // ba30e69c-cd14-4e90-ad69-1242f5132988
//
// Lazy object creation (performance!) 📌 Real use: DB connections
Supplier<Connection> connectionSupplier = () -> dataSource.getConnection();
Connection conn = connectionSupplier.get();
      

Comparator<T> is heavily used in streams for sorting, min, max.

Dummy Employee Data sorted().skip(1), min(), max()
@Data @AllArgsConstructor
@ToString(onlyExplicitlyIncluded = true)
class Employee {
    @ToString.Include
    String name;
    int age, experiance;
    @ToString.Include
    double salary;
    @ToString.Include
    String department;
    String designation, skils;
}
// Employee = name, age, experiance, salary, department, designation, skils
List<Employee> employees = List.of(
    new Employee("Alice", 28, 5, 55000, "IT", "Developer", "Java"),
    new Employee("Bob", 35, 10, 90000, "IT", "Senior Developer", "Java,Spring"),
    new Employee("Charlie", 30, 7, 70000, "HR", "HR Executive", "Recruitment"),
    new Employee("David", 40, 15, 120000, "Finance", "Manager", "Accounting"),
    new Employee("Eva", 26, 3, 48000, "HR", "HR Trainee", "Communication"),
    new Employee("Frank", 45, 20, 150000, "IT", "Architect", "Microservices"),
    new Employee("Grace", 32, 8, 85000, "Finance", "Senior Analyst", "Analysis"),
    new Employee("Henry", 29, 6, 70000, "IT", "Developer", "Java,SQL")
  );
//
employees = Optional.ofNullable(employees).orElse(new ArrayList());
      
Common
Comparator<Employee> deptAndExp = 
        Comparator.comparing(Employee::getDepartment)
                  .thenComparing(Employee::getExperiance)
// → ascending (default),  → descending (reversed())
Comparator<Employee> deptAscExpDesc =
        Comparator.comparing(Employee::getDepartment)
            .thenComparing(Comparator.comparingInt(Employee::getExperiance).reversed());
// Department ↑ and Name (2nd letter) ↑
Comparator<Employee> deptAscNameSecondCharAsc =
        Comparator.comparing(Employee::getDepartment)
        .thenComparing(e -> e.getName().length() > 1 ? e.getName().charAt(1) : Character.MIN_VALUE)
// Name (case-insensitive) ↓ - null-safe (extra solid)
Comparator<Employee> nameIgnoreCaseNullSafe =
        Comparator.comparing(Employee::getName, String.CASE_INSENSITIVE_ORDER)
        .nullsLast()
Comparator<Employee> salaryCompare = Comparator.comparingDouble(Employee::getSalary);
//
// Max salary → Frank (150000, IT), Min salary → Eva (48000, HR)
Employee optMaxSal = employees.stream().max(salaryCompare).orElse(null);
System.out.println("Max salary:"+ optMaxSal );
System.out.println("Min salary:"+ employees.stream().min(salaryCompare).get() );
//
//sorted in reverse: Second highest → David (120000, Finance)
Employee secondHighestSal = employees.stream()
        .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
        .skip(1)
        .findFirst().orElse(null);
System.out.println("Second highest:"+ secondHighestSal );
//
List<Employee> orderedWithExperiance = employees.stream()
        .sorted(Comparator.comparingDouble(Employee::getExperiance))
        .collect(Collectors.toList());
System.out.println("Order based on Experiance:"+ orderedWithExperiance );
//
// Group By Department
Map<String, List<Employee>> departmentMap = 
        employees.stream().collect(
            Collectors.groupingBy( Employee::getDepartment )
                );
System.out.println("Group By Department:"+ departmentMap);
// DepartmentWiseHighestSal
Map<String, Optional<Employee>> departmentWiseHighestSal = 
        employees.stream().collect(
            Collectors.groupingBy(
                Employee::getDepartment, 
                Collectors.maxBy(salaryCompare)
                )
            );
System.out.println("DepartmentWiseHighestSal:"+ departmentWiseHighestSal);
      

Given a list of objects, how can you use Java Streams to extract two separate lists of distinct, non-null properties from those objects?

Combine inside a single stream (recommended) Combine two lists after collecting
flatMap merges them into a single stream
Stream.of(list1, list2) takes both IDs from each participant
List<String> combinedIds = participantsList.stream()
        .flatMap(p -> Stream.of(p.getTeamLeaderId(), p.getBranchManagerId()))
        .filter(Objects::nonNull)
        .distinct()
        .collect(Collectors.toList());
      
Stream.concat(list1, list2)
List<String> teamLeaderIds = participantsList.stream()
        .map(p -> p.getTeamLeaderId())      // Extract team leader IDs
        .filter(Objects::nonNull)          // Remove nulls
        .distinct()                        // Remove duplicates
        .collect(Collectors.toList());
List<String> branchManagerIds = participantsList.stream()
        .filter(p -> p.getBranchManagerId() != null)  // Keep only non-null
        .map(p -> p.getBranchManagerId())             // Extract branch manager IDs
        .distinct()                                   // Remove duplicates
        .collect(Collectors.toList());		
List<String> combined =
        Stream.concat(teamLeaderIds.stream(), branchManagerIds.stream())
            .distinct()
            .collect(Collectors.toList());
      

Collectors.toMap() - “Why use toMap with merge function?”:

To handle duplicate keys explicitly and control which value survives the collision.

Collectors.toMap() - Without Merge Function
Method Signature: toMap(keyMapper, valueMapper)
Collectors.toMap() - With Merge Function
Method Signature: toMap(keyMapper, valueMapper, mergeFunction)
    ✔ Use cases:
  • → You are sure that key-value is unique. (No duplicate keys)
  • → Database guarantees uniqueness (e.g., PK, unique constraint).
    Ex keys: Emp-Id, Order-Id
Map<String, Employee> mapDepartment = employees.stream()
		.collect(Collectors.toMap(Employee::getDepartment, v -> v));
System.out.println("Department Map:"+mapDepartment);
      
⚠️ Fails if two employees belong to the same department.
Exception in thread "main" java.lang.IllegalStateException: Duplicate key IT 
(attempted merging values 
 Employee(name=Alice, department=IT, ...)
 and 
 Employee(name=Bob, department=IT, ...)
 )
  at Collectors.duplicateKeyException(Collectors.java:135)
  at Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:182)
❗ Risk: → If your db-query/data mapped key returns duplicates (even accidentally), the app will crash.
// DB Unique set of Key, Value not an issue
Map<String, Participant> orgMap = 
  participantRepo.findAllByClientIdIn( idsList )
  .stream()
  .collect(Collectors.toMap(Participant::getClientId, v -> v));
      
→ 1️⃣ Merge function: Keep first employee per department
BinaryOperator<Employee> keepFirstEmployeePerDepartment = (existingEmployee, newEmployee) -> {
    System.err.println("Duplicate key=" + existingEmployee.getDepartment());
    return existingEmployee;
};
//
Map<String, Employee> employeeByDepartmentFirst = employees.stream()
        .collect(Collectors.toMap(
                Employee::getDepartment,
                employee -> employee,
                keepFirstEmployeePerDepartment
        ));
//
System.out.println("Department Map:" + employeeByDepartmentFirst);
      
→ 2️⃣ Merge function: Keep highest-salary employee per department
BinaryOperator<Employee> keepHighestSalaryEmployeePerDepartment = (existingEmployee, newEmployee) -> {
    System.err.println(
            "Duplicate key:" + existingEmployee.getDepartment() +
            ", Salary:" + existingEmployee.getSalary() +
            ", Salary:" + newEmployee.getSalary()
    );
    return existingEmployee.getSalary() >= newEmployee.getSalary()
            ? existingEmployee
            : newEmployee;
};
//
Map<String, Employee> employeeByDepartmentMaxSalary = employees.stream()
        .collect(Collectors.toMap(
                Employee::getDepartment,
                employee -> employee,
                keepHighestSalaryEmployeePerDepartment
        ));
//
System.out.println("Department Map:" + employeeByDepartmentMaxSalary);
      

Collectors.toMap() vs Collectors.groupingBy()

  • Collectors.toMap() → One key maps to one value (fails on duplicate keys unless you handle them).
    • → returns Map<K, V>
  • Collectors.groupingBy() → One key maps to multiple values (List), duplicates are expected.
    • → returns Map<K, List<V>>
Collectors.groupingBy() Collectors.groupingByConcurrent()
long startStream = System.nanoTime();
Map<String, List<Employee>> empByDepartment = 
        employees.stream()
	.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println("Map with Stream:"+ empByDepartment);
System.err.println("Stream time: " + (System.nanoTime() - startStream)
        / 1_000_000 + " ms"); // 9ms | 10ms | 13ms
      
long startParallelStream = System.nanoTime();
ConcurrentMap<String, List<Employee>> empByDepartmentParallel = 
        employees.parallelStream()
	.collect(Collectors.groupingByConcurrent(Employee::getDepartment));
System.out.println("ConcurrentMap with ParallelStream:"+ empByDepartmentParallel);
System.err.println("ParallelStream time: " + (System.nanoTime() - startParallelStream)
         / 1_000_000 + " ms"); // 5ms | 5ms | 9ms
      

Problem: Given `"aabbbcccc"`, return `"a2b3c4"`

Finding Duplicate Elements and their occurrence count in a Stream

Java for loop Streams Collectors.groupingBy(), Function.identity(), Collectors.counting()
String str = "aabbbccccabcd";
String[] splitStr = str.split("");
Stream<String> streamStrSplit = Arrays.stream( splitStr );
//
String sortedString = streamStrSplit.sorted().collect(Collectors.joining(""));
System.out.println(sortedString); // aaabbbbcccccd
//
str = sortedString;
//
StringBuilder result = new StringBuilder();
int count = 1;
//
for (int i = 1; i <= str.length(); i++) {
    if (i < str.length() && str.charAt(i) == str.charAt(i - 1)) {
        count++;
    } else {
        result.append(str.charAt(i - 1)).append(count);
        count = 1;
    }
}
System.out.println(result); // a3b4c5d1
      
String strDup = "daabbbccccabcd";
//
String[] splitStr = strDup.split("");
Stream<String> streamStrSplit = Arrays.stream( splitStr );
//
IntStream charStream = strDup.chars();
IntFunction<String> objConversionExp = 
            c -> String.valueOf( (char) c); // int to char to String
Stream<String> streamIntChars = charStream.mapToObj( objConversionExp);
//
Map<String, Integer> map = streamStrSplit
        .collect( Collectors.toMap(
                Function.identity(),
                value -> 1,
                Integer::sum
            ));
System.out.println(map); // {a=3, b=4, c=5, d=2}
//
Map<String, Long> map2 = streamIntChars
            .collect( Collectors.groupingBy(
                    Function.identity(),
                    LinkedHashMap::new,      // keeps order
                    Collectors.counting()
            ));
System.out.println(map2); // {d=2, a=3, b=4, c=5}
//
String resultMapString = map.entrySet().stream()
   .map(e -> e.getKey() + e.getValue())
   .collect(Collectors.joining());
System.out.println(resultMapString); // a3b4c5d2
      

👉 Refactor this code to functional style:

AtomicInteger
mapToInt().sum()
T reduce(T identity, BinaryOperator accumulator)
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
AtomicInteger counter = new AtomicInteger();
numbers.parallelStream().forEach(n -> {
	// Not thread-safe, potential race condition
	counter.addAndGet(n);
});
System.out.println("Counter:"+ counter);
      
    Why is mapToInt().sum() better than reduce()?
  • Specialized primitive stream
  • Avoids boxing/unboxing
  • Optimized internally
numbers.parallelStream().mapToInt(Integer::intValue).sum(); // 👉 Output: 15
      
    Why is reduce() preferred over forEach() in parallel streams?
  • No shared mutable state
  • Functional programming principle
  • Better scalability, Safe parallel aggregation
numbers.parallelStream().reduce(0, (a,b) -> a + b); // 👉 Output: 15
numbers.parallelStream().reduce(0, Integer::sum)    // 👉 Output: 15  Method Reference
//
🔎 How it works: ((((0 + 1) + 2) + 3) + 4) + 5 = 15
      
If we change addition to subtraction, will reduce work correctly in parallel?
    No, use single thread stream for accurate result
  • Subtraction is not associative
  • Parallel result may differ from sequential
  • Dangerous in parallel streams
numbers.parallelStream().reduce(0, (a,b) -> a - b); // 5 - Incorrect
//
numbers.stream().reduce(0, (a,b) -> a - b); // -15
      
numbers.parallelStream().mapToInt(Integer::intValue).max(); // 5
      
numbers.stream().reduce(Integer.MIN_VALUE, Integer::max); // 5
      
⚠️ **GitHub.com Fallback** ⚠️