You are viewing a preview of this lesson. Sign in to start learning
Back to Java

Core Java Features

Deep dive into Java's essential data structures, I/O operations, and the Collections Framework

Core Java Features

Master Core Java Features with free flashcards and comprehensive code examples. This lesson covers object-oriented programming principles, exception handling, collections framework, generics, and lambda expressionsβ€”essential concepts for Java developers building robust applications.

Welcome to Core Java Features πŸ’»

Java has evolved significantly since its inception in 1995, but certain core features remain the foundation of every Java application. Whether you're building enterprise systems, mobile apps, or web services, understanding these fundamental features is crucial. This lesson will take you through the essential building blocks that make Java a powerful, versatile programming language.

What makes Java special? Java combines object-oriented principles with robust memory management, strong typing, and platform independence. The "Write Once, Run Anywhere" philosophy is powered by the Java Virtual Machine (JVM), which compiles your code into bytecode that runs on any platform with a JVM installed.


Core Concepts in Detail

1. Object-Oriented Programming (OOP) Principles πŸ—οΈ

Java is fundamentally object-oriented, built around four key pillars:

Encapsulation: Bundling data (fields) and methods that operate on that data within a single unit (class), while hiding internal implementation details.

public class BankAccount {
    private double balance; // private field - encapsulated
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    
    public double getBalance() {
        return balance;
    }
}

Inheritance: Creating new classes based on existing ones, promoting code reuse.

public class Animal {
    protected String name;
    
    public void eat() {
        System.out.println(name + " is eating");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println(name + " is barking");
    }
}

Polymorphism: Objects taking multiple forms, allowing one interface to represent different underlying implementations.

public interface Shape {
    double getArea();
}

public class Circle implements Shape {
    private double radius;
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private double width, height;
    
    @Override
    public double getArea() {
        return width * height;
    }
}

Abstraction: Hiding complex implementation details and showing only essential features.

public abstract class Vehicle {
    protected String model;
    
    public abstract void start();
    
    public void displayModel() {
        System.out.println("Model: " + model);
    }
}

πŸ’‘ Memory Device - "APIE": Abstraction, Polymorphism, Inheritance, Encapsulation - the four pillars of OOP!


2. Exception Handling ⚠️

Java's exception handling mechanism provides a structured way to handle runtime errors, preventing application crashes.

Exception Hierarchy:

         Throwable
            β”‚
      β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
      β”‚           β”‚
   Error      Exception
      β”‚           β”‚
   (JVM)    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
            β”‚         β”‚
      IOException  RuntimeException
      SQLException      β”‚
      (Checked)    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
                   β”‚         β”‚
           NullPointer  ArrayIndexOutOfBounds
           (Unchecked)

Checked vs Unchecked Exceptions:

Feature Checked Exceptions Unchecked Exceptions
Compiler Check Must be handled or declared Not enforced by compiler
Examples IOException, SQLException NullPointerException, ArithmeticException
When to Use Recoverable conditions Programming errors

Try-Catch-Finally Structure:

public void readFile(String filename) {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(filename));
        String line = reader.readLine();
        System.out.println(line);
    } catch (FileNotFoundException e) {
        System.err.println("File not found: " + filename);
    } catch (IOException e) {
        System.err.println("Error reading file: " + e.getMessage());
    } finally {
        // Always executes - cleanup code
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Try-with-Resources (Java 7+):

public void readFileModern(String filename) {
    try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
        String line = reader.readLine();
        System.out.println(line);
    } catch (IOException e) {
        System.err.println("Error: " + e.getMessage());
    }
    // reader.close() called automatically!
}

πŸ’‘ Tip: Use try-with-resources for any class implementing AutoCloseable to avoid resource leaks!


3. Collections Framework πŸ“¦

The Collections Framework provides data structures and algorithms for storing and manipulating groups of objects.

Main Interfaces:

         Collection
            β”‚
      β”Œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”
      β”‚     β”‚     β”‚
    List   Set   Queue
      β”‚     β”‚     β”‚
   ArrayList HashSet PriorityQueue
   LinkedList TreeSet LinkedList
   Vector    LinkedHashSet

When to Use Each Collection:

Collection Ordered? Duplicates? Best For
ArrayList Yes Yes Fast random access, frequent reads
LinkedList Yes Yes Frequent insertions/deletions
HashSet No No Fast lookup, unique elements
TreeSet Sorted No Sorted unique elements
HashMap No Unique keys Key-value pairs, fast lookup
// List Example
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Alice"); // Duplicates allowed
System.out.println(names.get(0)); // Random access: "Alice"

// Set Example
Set<Integer> uniqueNumbers = new HashSet<>();
uniqueNumbers.add(5);
uniqueNumbers.add(3);
uniqueNumbers.add(5); // Duplicate ignored
System.out.println(uniqueNumbers.size()); // 2

// Map Example
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 25);
ages.put("Bob", 30);
Integer aliceAge = ages.get("Alice"); // 25

πŸ”§ Try this: Create an ArrayList, add 5 names, then convert it to a HashSet. Notice how duplicates disappear!


4. Generics 🎯

Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. This provides type safety at compile time.

Without Generics (Old way):

List list = new ArrayList();
list.add("Hello");
list.add(42); // No compile error - danger!
String s = (String) list.get(1); // Runtime ClassCastException!

With Generics (Modern way):

List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // Compile error - type safety!
String s = list.get(0); // No cast needed

Generic Classes:

public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
}

// Usage
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer value = intBox.get(); // No casting!

Box<String> strBox = new Box<>();
strBox.set("Hello");

Generic Methods:

public class Util {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}

// Usage
Integer[] intArray = {1, 2, 3};
String[] strArray = {"A", "B", "C"};
Util.printArray(intArray); // Works with Integer[]
Util.printArray(strArray); // Works with String[]

Bounded Type Parameters:

public <T extends Number> double sum(List<T> numbers) {
    double total = 0;
    for (T num : numbers) {
        total += num.doubleValue();
    }
    return total;
}

πŸ€” Did you know? Generic type information is erased at runtime ("type erasure") - List<String> and List<Integer> become just List in bytecode!


5. Lambda Expressions and Functional Programming πŸš€

Introduced in Java 8, lambda expressions enable functional programming style, making code more concise and readable.

Syntax:

(parameters) -> expression
(parameters) -> { statements; }

Before Lambdas (Java 7):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

With Lambdas (Java 8+):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
// Or even simpler:
Collections.sort(names, String::compareTo);

Functional Interfaces:

Interface Method Purpose
Predicate<T> boolean test(T t) Test a condition
Function<T,R> R apply(T t) Transform input to output
Consumer<T> void accept(T t) Consume/use a value
Supplier<T> T get() Supply a value

Stream API Example:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

// Filter even numbers, square them, and collect
List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)        // Predicate
    .map(n -> n * n)                 // Function
    .collect(Collectors.toList());
// Result: [4, 16, 36, 64]

// Sum all numbers
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
// Result: 36

Method References (shorthand for lambdas):

// Lambda: x -> System.out.println(x)
list.forEach(System.out::println);

// Lambda: x -> x.toLowerCase()
list.stream().map(String::toLowerCase);

// Lambda: () -> new ArrayList<>()
Supplier<List<String>> supplier = ArrayList::new;

Detailed Examples

Example 1: Complete OOP Design πŸ›οΈ

// Abstract base class
public abstract class Employee {
    protected String name;
    protected double baseSalary;
    
    public Employee(String name, double baseSalary) {
        this.name = name;
        this.baseSalary = baseSalary;
    }
    
    // Abstract method - must be implemented by subclasses
    public abstract double calculateSalary();
    
    public String getName() {
        return name;
    }
}

// Concrete implementation
public class FullTimeEmployee extends Employee {
    private double bonus;
    
    public FullTimeEmployee(String name, double baseSalary, double bonus) {
        super(name, baseSalary);
        this.bonus = bonus;
    }
    
    @Override
    public double calculateSalary() {
        return baseSalary + bonus;
    }
}

public class ContractEmployee extends Employee {
    private int hoursWorked;
    private double hourlyRate;
    
    public ContractEmployee(String name, int hoursWorked, double hourlyRate) {
        super(name, 0);
        this.hoursWorked = hoursWorked;
        this.hourlyRate = hourlyRate;
    }
    
    @Override
    public double calculateSalary() {
        return hoursWorked * hourlyRate;
    }
}

// Usage with polymorphism
public class PayrollSystem {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new FullTimeEmployee("Alice", 50000, 5000));
        employees.add(new ContractEmployee("Bob", 160, 50));
        
        double totalPayroll = 0;
        for (Employee emp : employees) {
            // Polymorphism in action!
            double salary = emp.calculateSalary();
            System.out.println(emp.getName() + ": $" + salary);
            totalPayroll += salary;
        }
        System.out.println("Total Payroll: $" + totalPayroll);
    }
}

Output:

Alice: $55000.0
Bob: $8000.0
Total Payroll: $63000.0

Example 2: Exception Handling with Custom Exceptions ⚑

// Custom exception
public class InsufficientFundsException extends Exception {
    private double amount;
    
    public InsufficientFundsException(double amount) {
        super("Insufficient funds: needed $" + amount);
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}

public class Account {
    private double balance;
    
    public Account(double initialBalance) {
        this.balance = initialBalance;
    }
    
    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            double shortfall = amount - balance;
            throw new InsufficientFundsException(shortfall);
        }
        balance -= amount;
        System.out.println("Withdrew: $" + amount);
    }
    
    public double getBalance() {
        return balance;
    }
}

// Usage
public class BankingApp {
    public static void main(String[] args) {
        Account account = new Account(100.0);
        
        try {
            account.withdraw(50);
            account.withdraw(80); // This will throw exception
        } catch (InsufficientFundsException e) {
            System.err.println("Transaction failed: " + e.getMessage());
            System.err.println("Short by: $" + e.getAmount());
        }
        
        System.out.println("Final balance: $" + account.getBalance());
    }
}

Output:

Withdrew: $50.0
Transaction failed: Insufficient funds: needed $80.0
Short by: $30.0
Final balance: $50.0

Example 3: Generics with Collections 🎁

public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    @Override
    public String toString() {
        return key + "=" + value;
    }
}

public class GenericDemo {
    // Generic method with bounded type
    public static <T extends Comparable<T>> T findMax(List<T> list) {
        if (list.isEmpty()) {
            return null;
        }
        
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
    
    public static void main(String[] args) {
        // Using Pair with different types
        Pair<String, Integer> age = new Pair<>("Alice", 25);
        Pair<Integer, String> idName = new Pair<>(101, "Bob");
        
        System.out.println(age);      // Alice=25
        System.out.println(idName);   // 101=Bob
        
        // Using generic method
        List<Integer> numbers = Arrays.asList(3, 7, 2, 9, 1);
        Integer maxNum = findMax(numbers);
        System.out.println("Max number: " + maxNum); // 9
        
        List<String> words = Arrays.asList("apple", "zebra", "banana");
        String maxWord = findMax(words);
        System.out.println("Max word: " + maxWord); // zebra
    }
}

Example 4: Lambda Expressions and Stream Processing 🌊

public class Student {
    private String name;
    private int age;
    private double gpa;
    
    public Student(String name, int age, double gpa) {
        this.name = name;
        this.age = age;
        this.gpa = gpa;
    }
    
    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getGpa() { return gpa; }
    
    @Override
    public String toString() {
        return name + " (" + age + ", GPA: " + gpa + ")";
    }
}

public class StreamExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Alice", 20, 3.8),
            new Student("Bob", 22, 3.2),
            new Student("Charlie", 21, 3.9),
            new Student("David", 20, 3.5)
        );
        
        // Filter students with GPA > 3.5
        System.out.println("High achievers:");
        students.stream()
            .filter(s -> s.getGpa() > 3.5)
            .forEach(System.out::println);
        
        // Get average age of students
        double avgAge = students.stream()
            .mapToInt(Student::getAge)
            .average()
            .orElse(0.0);
        System.out.println("\nAverage age: " + avgAge);
        
        // Find top student by GPA
        Student topStudent = students.stream()
            .max(Comparator.comparingDouble(Student::getGpa))
            .orElse(null);
        System.out.println("\nTop student: " + topStudent);
        
        // Group students by age
        Map<Integer, List<Student>> byAge = students.stream()
            .collect(Collectors.groupingBy(Student::getAge));
        System.out.println("\nGrouped by age: " + byAge);
    }
}

Output:

High achievers:
Alice (20, GPA: 3.8)
Charlie (21, GPA: 3.9)

Average age: 20.75

Top student: Charlie (21, GPA: 3.9)

Grouped by age: {20=[Alice (20, GPA: 3.8), David (20, GPA: 3.5)], 21=[Charlie (21, GPA: 3.9)], 22=[Bob (22, GPA: 3.2)]}

⚠️ Common Mistakes

1. NullPointerException (NPE) - The most common Java exception:

❌ Wrong:

String name = null;
System.out.println(name.length()); // NPE!

βœ… Right:

String name = null;
if (name != null) {
    System.out.println(name.length());
}
// Or use Optional in Java 8+
Optional.ofNullable(name).ifPresent(n -> System.out.println(n.length()));

2. Catching Exception Too Broadly:

❌ Wrong:

try {
    // code
} catch (Exception e) {
    // This catches EVERYTHING - too broad!
}

βœ… Right:

try {
    // code
} catch (IOException e) {
    // Handle IO issues
} catch (SQLException e) {
    // Handle database issues
}

3. Not Closing Resources:

❌ Wrong:

FileReader reader = new FileReader("file.txt");
// If exception occurs, reader never closes - resource leak!
reader.read();
reader.close();

βœ… Right:

try (FileReader reader = new FileReader("file.txt")) {
    reader.read();
} // Automatically closed

4. Raw Types Instead of Generics:

❌ Wrong:

List list = new ArrayList(); // Raw type - no type safety
list.add("String");
list.add(42);

βœ… Right:

List<String> list = new ArrayList<>(); // Type-safe
list.add("String");
// list.add(42); // Compile error!

5. Modifying Collection While Iterating:

❌ Wrong:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item); // ConcurrentModificationException!
    }
}

βœ… Right:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String item = iter.next();
    if (item.equals("B")) {
        iter.remove(); // Safe
    }
}
// Or use removeIf (Java 8+)
list.removeIf(item -> item.equals("B"));

Key Takeaways 🎯

βœ… OOP Principles: Encapsulation, Inheritance, Polymorphism, and Abstraction form the foundation of Java design

βœ… Exception Handling: Use try-catch-finally for error handling; prefer try-with-resources for automatic resource management

βœ… Collections Framework: Choose the right collection - ArrayList for fast access, LinkedList for frequent modifications, HashSet for uniqueness, HashMap for key-value pairs

βœ… Generics: Enable type safety at compile time, eliminating runtime ClassCastException errors

βœ… Lambda Expressions: Make code concise and readable; work with functional interfaces and Stream API

βœ… Best Practices: Always close resources, avoid NullPointerException with proper null checks, use generics instead of raw types


πŸ“‹ Quick Reference Card

Concept Key Points Example
Class Definition public class Name { } public class Car { private String model; }
Inheritance extends keyword class Dog extends Animal
Interface implements keyword class MyList implements List
Try-Catch Handle exceptions try { } catch (IOException e) { }
ArrayList List list = new ArrayList<>(); List names = new ArrayList<>();
HashMap Map map = new HashMap<>(); Map ages = new HashMap<>();
Lambda (params) -> expression list.forEach(x -> System.out.println(x))
Stream Filter stream().filter().collect() list.stream().filter(x -> x > 5).collect(Collectors.toList())

πŸ“š Further Study

  1. Oracle Java Documentation: https://docs.oracle.com/en/java/javase/
  2. Java Tutorials by Oracle: https://docs.oracle.com/javase/tutorial/
  3. Effective Java by Joshua Bloch: https://www.oreilly.com/library/view/effective-java/9780134686097/