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
byte → short → int → long → float → double ↑ char
- 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[]} → ListArrays.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.
Here, names is a single collection object holding multiple String objects.
List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob");
- 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 |
@FunctionalInterfaceinterface 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 classesJava 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 Streamstream() / parallelStream()
|
Intermediate operations → lazy, return a Streamfilter() keeps only elements matching a predicatemap() 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
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
→ 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 theBaseStream.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.
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
|
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);
|
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 );
|
|
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);
|
|
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<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<Runnable> 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) | |
| 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 |
|---|---|
|
(parameters) -> expression
(or)
(parameters) -> { statements }
|
Functional interface : An interface with exactly one abstract method.
Predicate / Function / Consumer / SupplierBuilt-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.
// 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.
//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.
@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.
// 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.
// 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.
// 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();
|
| 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());
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)
|
|---|---|
Map<String, Employee> mapDepartment = employees.stream()
.collect(Collectors.toMap(Employee::getDepartment, v -> v));
System.out.println("Department Map:"+mapDepartment);
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)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);
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()vsCollectors.groupingBy()
-
Collectors.toMap()→ One key maps to one value (fails on duplicate keys unless you handle them).- → returns
Map<K, V>
- → returns
-
Collectors.groupingBy()→ One key maps to multiple values (List), duplicates are expected.- → returns
Map<K, List<V>>
- → returns
| 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:
AtomicIntegermapToInt().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);
mapToInt().sum() better than reduce()?
numbers.parallelStream().mapToInt(Integer::intValue).sum(); // 👉 Output: 15
|
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
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
|