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 |
| HashMap | Map |
Map |
| 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
- Oracle Java Documentation: https://docs.oracle.com/en/java/javase/
- Java Tutorials by Oracle: https://docs.oracle.com/javase/tutorial/
- Effective Java by Joshua Bloch: https://www.oreilly.com/library/view/effective-java/9780134686097/