Sorting in Java
Sorting is arranging objects in a specific order (ascending, descending, by name, by age, etc.). Two ways to sort objects in Java:
Comparable
Comparable is an interface that allows a class to define its default sorting order. When you say "sort this list of users", Java uses the Comparable logic if it exists.
package java.lang;
public interface Comparable<T> {
public int compareTo(T o);
}
When you implement compareTo(), you must return:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
// Sort by age (ascending)
// If this.age = 25, other.age = 30
// 25 - 30 = -5 (negative) → this comes before other
return this.age - other.age;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
}
// Usage
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
Collections.sort(people); // Sorts by age: Bob(25), Alice(30), Charlie(35)
// 1. Sort by age (ascending) - simplest for numbers
public int compareTo(Person other) {
return this.age - other.age;
}
// 2. Sort by age (descending)
public int compareTo(Person other) {
return other.age - this.age; // Reverse the order
}
// 3. Sort by name (alphabetical) - for strings
public int compareTo(Person other) {
return this.name.compareTo(other.name);
// "Alice".compareTo("Bob") = -1 (Alice comes first)
// "Bob".compareTo("Alice") = 1 (Bob comes after)
}
// 4. Sort by name (reverse alphabetical)
public int compareTo(Person other) {
return other.name.compareTo(this.name);
}
// 5. Sort by multiple fields (name first, then age)
public int compareTo(Person other) {
// First compare by name
int nameCompare = this.name.compareTo(other.name);
// If names are different, return that result
if (nameCompare != 0) {
return nameCompare;
}
// If names are same, compare by age
return this.age - other.age;
}
Java classes like String, Integer already have compareTo. But YOUR custom classes (like User, Employee) don't, thats why we implement that Interface. Many Java classes already have natural ordering:
// Java's built-in classes already have compareTo
String s1 = "Alice";
String s2 = "Bob";
s1.compareTo(s2); // Works! Returns -1 (Alice comes before Bob)
Integer i1 = 25;
Integer i2 = 30;
i1.compareTo(i2); // Works! Returns -5 (25 comes before 30)
// But YOUR custom class doesn't have compareTo
public class User {
String name;
int age;
}
User u1 = new User("Alice", 30);
User u2 = new User("Bob", 25);
u1.compareTo(u2); // ERROR! User class doesn't have compareTo
// Strings already have natural ordering
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names); // Result: [Alice, Bob, Charlie]
// Integers already have natural ordering
List<Integer> numbers = Arrays.asList(30, 25, 35);
Collections.sort(numbers); // Result: [25, 30, 35]
NOTE : When you call Collections.sort(list), it internally calls the compareTo() method of each object in the list to determine the order.
┌─────────────────────────────────────────────────────────────────┐
│ Collections.sort() INTERNAL WORKING │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Collections.sort(users) is called │
│ │ │
│ ▼ │
│ 2. Internally calls Arrays.sort() or TimSort │
│ │ │
│ ▼ │
│ 3. Sorting algorithm needs to compare two objects │
│ │ │
│ ▼ │
│ 4. It calls: object1.compareTo(object2) ← YOUR compareTo() │
│ │ │
│ ▼ │
│ 5. Based on the result (-1, 0, 1), it decides ordering │
│ │ │
│ ▼ │
│ 6. Returns sorted list │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementing Comparable allows us to define compareTo(), which is then used by:
import java.util.*;
class User implements Comparable<User> {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(User other) {
return this.age - other.age; // Sort by age
}
public String toString() {
return name + "(" + age + ")";
}
}
public class Test {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 35)
);
// 1. Collections.sort() uses compareTo()
Collections.sort(users);
System.out.println("Sorted list: " + users);
// Output: [Bob(25), Alice(30), Charlie(35)]
// 2. TreeSet uses compareTo() automatically
TreeSet<User> treeSet = new TreeSet<>(users);
System.out.println("TreeSet: " + treeSet);
// Automatically sorted by age
// 3. List.sort() uses compareTo()
List<User> list = new ArrayList<>(users);
list.sort(null); // null means use natural ordering (compareTo)
System.out.println("List.sort(): " + list);
}
}
Comparator
Comparator is an interface that allows you to define custom sorting logic outside the class. Unlike Comparable (which is inside the class and gives one natural order), Comparator lets you create multiple different sorting strategies for the same class.
With Comparable, you only get ONE sorting order. Comparator gives you MULTIPLE sorting options.
package java.util;
public interface Comparator<T> {
int compare(T o1, T o2);
}
The Rule:
// WITH COMPARABLE, you are forced to choose ONE sorting criteria (age in this case).
import java.util.*;
class User implements Comparable<User> {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(User other) {
return this.age - other.age; // ONLY sort by age
}
public String toString() {
return name + "(" + age + ")";
}
}
public class Test {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 35)
);
Collections.sort(users); // Always sorts by age
System.out.println(users); // [Bob(25), Alice(30), Charlie(35)]
// CANNOT sort by name! Stuck with age only!
}
}
//------------------------------------------------------------------------------------------
// WITH COMPARATOR, you can have multiple sorting criteria and choose which
one to use at runtime.
import java.util.*;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + "(" + age + ")";
}
}
public class Test {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 35),
new User("David", 28)
);
// Comparator 1: Sort by age
Comparator<User> byAge = (u1, u2) -> u1.age - u2.age;
users.sort(byAge);
System.out.println("By age: " + users);
// [Bob(25), David(28), Alice(30), Charlie(35)]
// Comparator 2: Sort by name
Comparator<User> byName = (u1, u2) -> u1.name.compareTo(u2.name);
users.sort(byName);
System.out.println("By name: " + users);
// [Alice(30), Bob(25), Charlie(35), David(28)]
// Comparator 3: Sort by age descending
Comparator<User> byAgeDesc = (u1, u2) -> u2.age - u1.age;
users.sort(byAgeDesc);
System.out.println("By age desc: " + users);
// [Charlie(35), Alice(30), David(28), Bob(25)]
// Comparator 4: Sort by name, then by age
Comparator<User> byNameThenAge = (u1, u2) -> {
int nameCompare = u1.name.compareTo(u2.name);
if (nameCompare != 0) return nameCompare;
return u1.age - u2.age;
};
users.sort(byNameThenAge);
}
}

4 Ways to Create Comparators
// 1: Lambda (Easiest)
users.sort((u1, u2) -> u1.age - u2.age);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
// 2: Method Reference (Cleanest)
import java.util.Comparator;
users.sort(Comparator.comparing(u -> u.age));
users.sort(Comparator.comparing(User::getName));
// 3: Anonymous Class (Old Way)
users.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u1.age - u2.age;
}
});
// 4: Separate Class (Reusable)
class AgeComparator implements Comparator<User> {
@Override
public int compare(User u1, User u2) {
return u1.age - u2.age;
}
}
users.sort(new AgeComparator());
import java.util.*;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
class Employee {
private int id;
private String name;
private double salary;
private int age;
}
public class ComparatorExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee(3, "Charlie", 55000, 35),
new Employee(1, "Alice", 75000, 30),
new Employee(2, "Bob", 45000, 25),
new Employee(4, "Alice", 65000, 28)
);
System.out.println("Original: " + employees);
// 1. Sort by age (ascending)
employees.sort((e1, e2) -> e1.getAge() - e2.getAge());
System.out.println("By age: " + employees);
// 2. Sort by name
employees.sort((e1, e2) -> e1.getName().compareTo(e2.getName()));
System.out.println("By name: " + employees);
// 3. Sort by salary (highest first)
employees.sort((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()));
System.out.println("By salary desc: " + employees);
// 4. Sort by name, then by age (clean method)
employees.sort(Comparator
.comparing(Employee::getName)
.thenComparing(Employee::getAge));
System.out.println("By name, then age: " + employees);
// 5. Sort by age, then by name
employees.sort(Comparator
.comparingInt(Employee::getAge)
.thenComparing(Employee::getName));
System.out.println("By age, then name: " + employees);
}
}
----------------------------------------------------------------------------------------------------------------------------
Annotations
Annotations are a way to attach metadata or extra information to your code. They do not change the logic of your code directly, but they provide information to the compiler, JVM, or frameworks like Spring Boot about how to treat your code. Think of them as sticky notes attached to your classes, methods, variables, or parameters.
Annotations always start with the @ symbol and can be placed on classes, methods, variables, and parameters.
@SomeAnnotation
public class MyClass { }
@SomeAnnotation
public void myMethod() { }
@SomeAnnotation
private String myVariable;
Annotations serve 2 audiences :
For the Developer / Compiler
Annotations like @Override and @Deprecated are just hints. They help you write better code and catch mistakes early. The compiler reads them and warns you if something is wrong.
For Frameworks
Annotations like @Component and @Autowired are instructions for Spring Boot. Instead of you manually writing hundreds of lines of configuration code, you just stick an annotation on your class and Spring handles everything automatically.
Built-in Java Annotations
Java ships with some annotations out of the box :
@Override — tells the compiler this method is intentionally overriding a parent method. Gives a compile error if it doesn't match any parent method.@Deprecated — marks something as outdated. Anyone who uses it gets a compiler warning.@SuppressWarnings — tells the compiler to stop showing specific warnings.@FunctionalInterface — tells the compiler this interface must have exactly one abstract method.
Custom Annotations in Java
To create a custom annotation we use the @interface keyword. We also need to define 2 things on top of it — @Retention (how long it lives) and @Target (where it can be placed).
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value(); // required element
int count() default 1; // optional element with default value
}
NOTE : The elements inside an annotation definition look like methods but they are actually properties that store values when the annotation is used. If an element has no default value it is required, if it has a default value it is optional.
Retention policy defines how long an annotation is available during the life of a Java program. A Java program goes through 3 stages — you write the source code, the compiler compiles it into a .class file, and then the JVM executes it. Retention policy controls at which stage the annotation gets thrown away.
There are 3 possible values :
- RetentionPolicy.SOURCE — The annotation is only available in the
.java source file. The compiler reads it and then throws it away. It will not be present in the compiled .class file and cannot be read at runtime. Used for annotations that are only meant for the developer or the compiler such as @Override and @SuppressWarnings. - RetentionPolicy.CLASS — The annotation survives compilation and is present in the
.class file. However it is NOT available when the program is actually running and cannot be read using Reflection. This is the default if you do not specify @Retention at all. Used by bytecode analysis tools and code generation tools. Rarely used in everyday Java development. - RetentionPolicy.RUNTIME — The annotation survives all the way through compilation and is available while the program is running. It can be read using Reflection at runtime. This is what frameworks like Spring Boot and Hibernate use because they need to scan and read your annotations while the application is starting up.
@Retention(RetentionPolicy.SOURCE)
// Annotation is only available in the source code (.java file)
// Thrown away by the compiler, not present in the .class file
// Used when the annotation is only meant for the developer or compiler
// Example : @Override, @SuppressWarnings
@Retention(RetentionPolicy.CLASS)
// Annotation is kept in the compiled .class file
// But NOT available when the program is actually running
// This is the DEFAULT if you dont specify @Retention at all
// Rarely used in practice
@Retention(RetentionPolicy.RUNTIME)
// Annotation is available while the program is running
// Can be read using Reflection at runtime
// Used when a framework or your own code needs to read the annotation
// Example : Spring Boot annotations like @Component, @Autowired
ElementType defines where the annotation is allowed to be placed. Think of your Java file as a document. ElementType just answers one question — "which part of the document can I stick this annotation on?". Below are the most commonly used values for elementType.
import java.lang.annotation.*;
import java.lang.reflect.*;
// 1] TYPE — on classes, interfaces, enums
// Spring Boot example : @Component, @Service, @RestController
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyComponent {
String value() default "I am on a Class";
}
// 2] METHOD — on any method
// Spring Boot example : @GetMapping, @PostMapping, @Transactional
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyGetMapping {
String value() default "I am on a Method";
}
// 3] FIELD — on class level variables
// Spring Boot example : @Autowired, @Value
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MyAutowired {
String value() default "I am on a Field";
}
// 4] PARAMETER — on method parameters
// Spring Boot example : @RequestParam, @PathVariable
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface MyRequestParam {
String value() default "I am on a Parameter";
}
// ============================================================
// Using all 4 annotations
// ============================================================
@MyComponent(value = "UserController handles user related requests")
class UserController {
@MyAutowired(value = "UserService is injected here by Spring")
private String userService;
@MyGetMapping(value = "This method handles GET /users request")
public void getUsers(
@MyRequestParam(value = "page number for pagination") int page,
@MyRequestParam(value = "number of results per page") int size) {
System.out.println("Getting users, page " + page + " size " + size);
}
@MyGetMapping(value = "This method handles GET /users/{id} request")
public void getUserById(
@MyRequestParam(value = "id of the user to fetch") int id) {
System.out.println("Getting user with id " + id);
}
}
// ============================================================
// Reading all 4 annotations using Reflection
// ============================================================
public class Main {
public static void main(String[] args) throws Exception {
UserController controller = new UserController();
// --- Reading TYPE annotation on the class ---
System.out.println("========== TYPE ==========");
MyComponent typeAnn = controller.getClass().getAnnotation(MyComponent.class);
if (typeAnn != null) {
System.out.println("@MyComponent found on : " + controller.getClass().getSimpleName());
System.out.println("Value : " + typeAnn.value());
}
// --- Reading FIELD annotations ---
System.out.println("\n========== FIELD ==========");
for (Field field : controller.getClass().getDeclaredFields()) {
MyAutowired fieldAnn = field.getAnnotation(MyAutowired.class);
if (fieldAnn != null) {
System.out.println("@MyAutowired found on field : " + field.getName());
System.out.println("Value : " + fieldAnn.value());
}
}
// --- Reading METHOD and PARAMETER annotations ---
System.out.println("\n========== METHOD & PARAMETER ==========");
for (Method method : controller.getClass().getDeclaredMethods()) {
MyGetMapping methodAnn = method.getAnnotation(MyGetMapping.class);
if (methodAnn != null) {
System.out.println("@MyGetMapping found on method : " + method.getName());
System.out.println("Value : " + methodAnn.value());
// Reading parameter annotations
Parameter[] params = method.getParameters();
Annotation[][] paramAnnotations = method.getParameterAnnotations();
for (int i = 0; i < params.length; i++) {
for (Annotation annotation : paramAnnotations[i]) {
if (annotation instanceof MyRequestParam) {
MyRequestParam paramAnn = (MyRequestParam) annotation;
System.out.println(" @MyRequestParam on param : " + params[i].getName());
System.out.println(" Value : " + paramAnn.value());
}
}
}
}
}
}
}
import java.lang.annotation.*;
import java.lang.reflect.Method;
// Annotation for CLASS
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ClassInfo {
String name();
}
// Annotation for METHOD
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MethodInfo {
String description();
}
// Using both annotations
@ClassInfo(name = "UserService")
class UserService {
@MethodInfo(description = "This method creates a new user")
public void createUser() {
System.out.println("User created !");
}
@MethodInfo(description = "This method deletes a user")
public void deleteUser() {
System.out.println("User deleted !");
}
}
public class main {
public static void main(String[] args) throws Exception {
UserService service = new UserService();
// --- Reading CLASS annotation ---
// Step 1 : get the class
// Step 2 : get the annotation
// Step 3 : read the value
ClassInfo classAnnotation = service.getClass().getAnnotation(ClassInfo.class);
System.out.println("Class name : " + classAnnotation.name());
System.out.println("---");
// --- Reading METHOD annotation ---
// Step 1 : get the method by its name
// Step 2 : get the annotation
// Step 3 : read the value
Method createUser = service.getClass().getMethod("createUser");
MethodInfo createUserAnnotation = createUser.getAnnotation(MethodInfo.class);
System.out.println("createUser description : " + createUserAnnotation.description());
Method deleteUser = service.getClass().getMethod("deleteUser");
MethodInfo deleteUserAnnotation = deleteUser.getAnnotation(MethodInfo.class);
System.out.println("deleteUser description : " + deleteUserAnnotation.description());
}
}
// Class name : UserService
// ---
// createUser description : This method creates a new user
// deleteUser description : This method deletes a user
You cannot write logic inside an annotation definition itself. Annotations are just metadata — they can only hold values, not logic. Think of them as a form you fill out, not a class you write code in.
// This is NOT allowed
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
// ❌ Cannot have logic inside annotation
String value() {
if(something) { // ❌ not allowed
return "hello";
}
}
// ❌ Cannot have variables
int x = 10; // ❌ not allowed
// ✅ Can only have elements with default values
String value() default "hello";
int count() default 1;
}
---------------------------------------------------------------------------------------------------------------
Optional Class
Before Optional, if a method could not find or return a value it would return null. This caused the most common error in Java — NullPointerException. Optional was introduced in Java 8 to solve this problem by wrapping a value that may or may not be present.
Optional is just a box that either contains a value or is empty. Instead of returning null, you return an empty box. This forces you to handle the "not found" case properly instead of crashing. Optional is simply a wrapper box around a value. The box either :
- Contains a value → something was found
- Is empty → nothing was found
Instead of returning null and risking a crash, you return an empty box. The box never crashes — you just check if it has something inside before using it.
The Optional class has 2 types of methods :
1] Methods to CREATE an Optional — of(), empty(), ofNullable(). These are static methods, meaning you call them on the class directly, not on an object.
// Optional.of() → I have a value, wrap it
Optional<String> box = Optional.of("Deepesh");
// Optional.empty() → I have no value
Optional<String> box = Optional.empty();
// Optional.ofNullable() → I am not sure if I have a value
Optional<String> box = Optional.ofNullable(null); // becomes empty
Optional<String> box = Optional.ofNullable("Hi"); // becomes full
2] Methods to GET value from Optional — orElse(), orElseThrow(), isPresent(), get(). These are instance methods, meaning you call them on the Optional object you created.
Optional<String> box = Optional.of("Deepesh");
// orElseThrow() → give me value, if empty crash with message
String name = box.orElseThrow(() -> new RuntimeException("not found"));
System.out.println(name); // Deepesh
// orElse() → give me value, if empty give me this default
String name = box.orElse("Unknown");
System.out.println(name); // Deepesh
// isPresent() → does box have a value?
boolean exists = box.isPresent();
System.out.println(exists); // true
// get() → give me value directly
String name = box.get();
System.out.println(name); // Deepesh
NOTE : isPresent() is usually paired with get(). You check with isPresent() first and then safely call get(). Never call get() without checking isPresent() first — it will crash if the box is empty.
import java.util.Optional;
class Student {
int id;
String name;
Student(int id, String name) {
this.id = id;
this.name = name;
}
}
class StudentRepository {
public Optional<Student> findById(int id) throws InterruptedException {
System.out.println("Fetching from database...");
Thread.sleep(1000); // simulate delay
if (id == 1) { return Optional.of(new Student(1, "Deepesh")); } // Optional.of()
if (id == 2) { return Optional.of(new Student(2, "Rohan")); } // Optional.of()
return Optional.empty(); // Optional.empty()
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
StudentRepository repo = new StudentRepository();
// --- isPresent() + get() ---
Optional<Student> box1 = repo.findById(1);
if (box1.isPresent()) {
System.out.println("Found : " + box1.get().name); // Found : Deepesh
}
// --- isEmpty() ---
Optional<Student> box2 = repo.findById(99);
if (box2.isEmpty()) {
System.out.println("Student not found"); // Student not found
}
// --- orElse() ---
Student s1 = repo.findById(2).orElse(new Student(0, "Guest"));
System.out.println("Found : " + s1.name); // Found : Rohan
Student s2 = repo.findById(99).orElse(new Student(0, "Guest"));
System.out.println("Found : " + s2.name); // Found : Guest
// --- orElseThrow() ---
Student s3 = repo.findById(1).orElseThrow(() -> new RuntimeException("Student not found"));
System.out.println("Found : " + s3.name); // Found : Deepesh
repo.findById(99).orElseThrow(() -> new RuntimeException("Student not found")); // ❌ RuntimeException
}
}
// Fetching from database...
// Found : Deepesh
// Fetching from database...
// Student not found
// Fetching from database...
// Found : Rohan
// Fetching from database...
// Found : Guest
// Fetching from database...
// Found : Deepesh
// Fetching from database...
// ❌ RuntimeException : Student not found
---------------------------------------------------------------------------------------------------------------
Streams API
A Stream is a pipeline that your data flows through. It does not store data — it processes it. You put a list of data in one end, apply a series of operations, and get a result at the other end. Streams were introduced in Java 8 along with Lambda expressions.
There are 3 parts to every Stream :
- Source — where the data comes from, usually a List, Set, or Array.
- Intermediate Operations — operations that transform or filter the data. You can chain as many as you want. They are lazy meaning they do not run until a terminal operation is called.
- Terminal Operation — the final operation that triggers everything and produces a result. There can only be one per stream.
import java.util.List;
import java.util.stream.Collectors;
class Student {
String name;
int age;
String city;
Student(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Deepesh", 20, "Mumbai"),
new Student("Rohan", 17, "Delhi"),
new Student("Kiran", 22, "Mumbai"),
new Student("Amit", 19, "Delhi"),
new Student("Sara", 16, "Mumbai")
);
// Get names of students from Mumbai who are 18 or older, sorted alphabetically
List<String> result = students.stream()
.filter(s -> s.city.equals("Mumbai")) // keep only Mumbai students
.filter(s -> s.age >= 18) // keep only 18 or older
.map(s -> s.name) // extract just the name
.sorted() // sort alphabetically
.collect(Collectors.toList()); // collect into a List
System.out.println(result); // [Deepesh, Kiran]
}
}
You first convert your List into a Stream using .stream(), and that Stream object provides methods like filter(), map(), sorted(), collect() etc. to filter, transform, and collect data in a clean and readable way.There are 2 types of methods a Stream provides :
- Intermediate Methods — transform or filter the data. Always return a new Stream so you can keep chaining. They are lazy meaning they do not run until a terminal method is called.
- Terminal Methods — trigger the entire pipeline and produce a final result. There can only be one and it must be at the end.
NOTE : Streams do not modify the original list. They always produce a new result. The original data is never changed.
NOTE : The order of operations matters in a Stream. Always filter() before map() so you are transforming less data. For example filter first to reduce the list size and then map to transform only the remaining elements.
NOTE : Intermediate methods are lazy — they do not execute until a terminal method is called. The entire pipeline runs only when you call collect(), forEach(), count() etc.
---------------------------------------------------------------------------------------------------------------
Checked vs Unchecked Exceptions
You already know what exceptions are — when something goes wrong in your code, Java throws an exception. There are just 2 types of exceptions in Java : Checked and Unchecked.
Checked exceptions are exceptions the compiler forces you to handle. If you don't handle them your code won't even compile. These are caused by external factors outside your code like a missing file, a database being down, or a network issue. The compiler warns you in advance saying "this code might go wrong, handle it or I won't compile."
// ❌ Does not compile
// Compiler forces you to handle FileNotFoundException
FileReader file = new FileReader("myfile.txt");
// ✅ Option 1 : handle it with try catch
try {
FileReader file = new FileReader("myfile.txt");
} catch (Exception e) {
System.out.println("File not found : " + e.getMessage());
}
// ✅ Option 2 : pass the responsibility to the caller using throws
public void readFile() throws Exception {
FileReader file = new FileReader("myfile.txt");
}
Unchecked exceptions are exceptions the compiler does not force you to handle. Your code compiles fine but may crash at runtime. These are caused by bugs in your code like accessing a null object, dividing by zero, or going out of array bounds.
// ✅ Compiles fine
// ❌ Crashes at runtime
String name = null;
System.out.println(name.length()); // NullPointerException
int[] arr = {1, 2, 3};
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
int result = 10 / 0; // ArithmeticException
NOTE : The rule is simple — if an exception extends RuntimeException it is unchecked. If it extends Exception directly it is checked.
---------------------------------------------------------------------------------------------------------------
Iterators
An Iterator is an object that allows you to traverse through a collection one element at a time. Every collection in Java like List, Set, and Map implements the Iterable interface which means they all have an iterator() method that returns an Iterator object.
The enhanced for loop and regular for loop both crash if you try to remove an element while looping. Iterator solves this by allowing safe removal while looping. When you remove an element directly inside the loop, the list size changes but the Iterator's counter does not match anymore. Java detects this mismatch and throws ConcurrentModificationException to protect you from unpredictable behavior.
Real Life Analogy
Imagine you have a queue of 5 people standing in a line and you are going through them one by one checking their tickets. You are at person number 3. Suddenly someone removes person number 2 from the line. Now everyone shifts forward. Person 4 becomes person 3, person 5 becomes person 4. You are now confused — you were at position 3 but the person you just checked is gone and everyone shifted. You might skip someone or check the same person twice. This is exactly what happens when you remove an element from a List while looping through it.
Why Enhanced For Loop crashes
Java keeps a modification counter called modCount on every ArrayList. Every time you add or remove an element, this counter goes up by 1. When you start a for each loop, Java notes down the current modCount. On every iteration it checks — "has modCount changed since I started?" If yes → somebody modified the list while I was looping → crash with ConcurrentModificationException to protect you.
ArrayList<String> names = new ArrayList<>();
names.add("Deepesh"); // modCount = 1
names.add("Rohan"); // modCount = 2
names.add("Kiran"); // modCount = 3
// Loop starts, notes modCount = 3
for (String name : names) {
if (name.equals("Rohan")) {
names.remove(name); // modCount becomes 4
// next iteration checks — modCount changed from 3 to 4
// ❌ ConcurrentModificationException
}
}
Why Iterator works
When you call iterator.remove(), the removal happens through the Iterator itself. The Iterator updates its own copy of modCount at the same time. So on the next iteration when it checks — "has modCount changed?" — the answer is no because the Iterator already knows about the change.
An Iterator object only has the following 3 methods :
// hasNext() — returns true if there are more elements to iterate
iterator.hasNext();
// next() — returns the next element
iterator.next();
// remove() — removes the last element returned by next()
iterator.remove();
Example] Below we remove an element using the Iterator class
Iterator<String> it = names.iterator();
// Iterator notes modCount = 3
while (it.hasNext()) {
String name = it.next();
if (name.equals("Rohan")) {
it.remove(); // modCount becomes 4
// Iterator also updates its own copy to 4
// next iteration checks — modCount still matches ✅
}
}
```
---
### In Simple Terms
```
Enhanced For Loop removes → List knows → Iterator does not know → MISMATCH → crash
Iterator removes → List knows → Iterator also knows → NO MISMATCH → works
Here are all the reasons you would use an Iterator :
- 1] Remove elements while looping — the main reason, as we just discussed.
- 2] More control over traversal — with an Iterator you decide exactly when to call
next(). You can pause, skip, or stop at any point. - 3] Works with all collections — some collections like
Set do not support index based access like set.get(0). Iterator works on all collections the same way.
---------------------------------------------------------------------------------------------------------------
Build Tools — Maven & Gradle
When building a Java project you need external libraries like Spring Boot, MySQL drivers, JWT etc. Without a build tool you would have to manually download each library as a .jar file, add it to your project, and manage versions yourself. Build tools solve this problem by automatically downloading and managing libraries for you. They also compile your code, run tests, and package everything into a single runnable .jar file.
Java has 2 popular build tools — Maven and Gradle. Both do the same job, just with different syntax and config files.
All Java libraries are stored on a central online repository called Maven Central — think of it like npm registry but for Java. Library authors and companies upload their libraries there. For example the Spring team uploads Spring Boot, MySQL uploads their database driver etc.
When you declare a dependency in your config file this is what happens :
You declare dependency in config file
↓
Build tool looks in your local machine cache first
↓
If not found locally → goes to Maven Central and downloads it
↓
Saves it in local cache for future use
↓
Adds it to your project automatically
NOTE : Think of it exactly like npm install in Node.js. You declare what you need in a config file, the tool downloads everything automatically from a central registry.
Maven
Maven is the older and more widely used build tool in enterprise Java. Its config file is called pom.xml and uses XML syntax. Most Spring Boot tutorials use Maven.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project>
<!-- Project information -->
<groupId>com.deepesh</groupId> <!-- your company/name -->
<artifactId>my-app</artifactId> <!-- your project name -->
<version>1.0.0</version> <!-- your project version -->
<!-- Dependencies — libraries you need -->
<!-- Maven downloads these automatically from Maven Central -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
</project>
NOTE : Think of pom.xml like package.json in Node.js. You declare what you need, Maven handles downloading and managing everything.
Gradle
Gradle is the newer and faster build tool. Its config file is called build.gradle and uses Groovy/Kotlin syntax which is much cleaner than XML. Gradle is commonly used in Android development and is growing in Spring Boot projects too.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
}
// Project information
group = 'com.deepesh'
version = '1.0.0'
// Dependencies — libraries you need
// Gradle downloads these automatically from Maven Central
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.0'
implementation 'mysql:mysql-connector-java:8.0.33'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
NOTE : Think of build.gradle like pom.xml but with cleaner syntax. Same concept, different file format. Gradle also downloads from Maven Central by default.
NOTE : Both tools do the same job. Maven is more widely used in enterprise Java and most Spring Boot tutorials use it so we will use Maven when we start Spring Boot.---------------------------------------------------------------------------------------------------------------
Comments
Post a Comment