UNIT 4: Java Programming Lab - Intermediate & Advanced Concepts
4.1. Java Collections Framework Deep Dive
The Java Collections Framework (JCF) provides a unified architecture for representing and manipulating collections. It consists of core interfaces, implementations, and algorithms.
Core Interfaces & Hierarchy:
-
Collection<E>: Root interface for a group of objects.-
List<E>: Ordered collection (sequence), allows duplicates. (ArrayList,LinkedList) -
Set<E>: Unordered collection, no duplicates. (HashSet,TreeSet) -
Queue<E>/Deque<E>: For holding elements prior to processing (FIFO/LIFO).
-
-
Map<K,V>: Not a trueCollection. Stores key-value pairs, keys unique.
Implementations & Performance (Big O Time Complexity):
| Interface | Implementation | Key Characteristics | get()/contains() |
add()/remove() (at end) |
add()/remove() (at index/known position) |
Ordering |
|---|---|---|---|---|---|---|
| List | ArrayList<E> |
Resizable array. Fast random access. | O(1) | O(1) amortized | O(n) (shifts elements) | Insertion order |
LinkedList<E> |
Doubly-linked list. Fast inserts/removes. | O(n) | O(1) (if known node) | O(1) (if known node) | Insertion order | |
| Set | HashSet<E> |
Hash table. Uses hashCode() & equals(). |
O(1) | O(1) | N/A | No guaranteed order |
LinkedHashSet<E> |
Hash table + linked list. | O(1) | O(1) | N/A | Insertion order | |
TreeSet<E> |
Red-Black tree. | O(log n) | O(log n) | N/A | Sorted order (natural/comparator) | |
| Map | HashMap<K,V> |
Hash table. | O(1) | O(1) | N/A | No guaranteed order |
LinkedHashMap<K,V> |
Hash table + linked list. | O(1) | O(1) | N/A | Insertion or access order | |
TreeMap<K,V> |
Red-Black tree. | O(log n) | O(log n) | N/A | Sorted order by keys | |
Hashtable<K,V> |
Legacy. Synchronized (thread-safe). | O(1) | O(1) | N/A | No guaranteed order |
PriorityQueue: Implements a heap. peek()/poll(): O(log n). Ordering by natural order or Comparator.
Common Operations:
-
Iteration: Use enhanced for-loop or
Iterator<E>(for safe removal viaiterator.remove()). -
Sorting:
Collections.sort(list)forList.TreeSet/TreeMapfor sortedSet/Map. -
Conversion:
new ArrayList<>(array);list.toArray(new String[0]).
[!TIP] Exam Focus: Be prepared to choose the right collection based on required operations (access pattern, ordering, uniqueness) and state the time complexity of core operations. Remember
HashMapis not synchronized; useConcurrentHashMapfor thread-safe maps.
4.2. Generics for Type Safety
Motivation: Raw types (e.g., List list = new ArrayList();) allow any Object, leading to ClassCastException at runtime. Generics provide compile-time type checking.
Syntax:
-
Generic Class:
class Box<T> { private T content; } -
Generic Method:
<T> void printArray(T[] arr) { ... } -
Instantiation:
Box<String> stringBox = new Box<>();(Diamond operator<>from Java 7).
Wildcards (?):
-
?: Unknown type. -
? extends T: Producer (PECS). Can readTor its subtypes. Safe for getting values.List<? extends Number> list = new ArrayList<Integer>(); Number n = list.get(0); // Safe read // list.add(10); // Compile error - cannot add -
? super T: Consumer (PECS). Can writeTor its subtypes. Safe for adding values.List<? super Integer> list = new ArrayList<Number>(); list.add(10); // Safe write // Number n = list.get(0); // Compile error - can only get Object
Type Erasure: Generics are implemented by the compiler. All generic type information is erased at runtime and replaced with bounds (e.g., T becomes Object, T extends Number becomes Number). No runtime performance cost, but prevents using new T() or T.class.
[!TIP] Common Pitfall: Confusing
? extends Tand? super T. Remember PECS: Producer Extends, Consumer Super.
4.3. Exception Handling & Robust Programming
Exception Hierarchy:
Throwable
├── Error (System-level, unrecoverable, e.g., OutOfMemoryError)
└── Exception (Application-level)
├── RuntimeException (Unchecked, e.g., NullPointerException, IllegalArgumentException)
└── Checked Exceptions (Must be caught or declared, e.g., IOException, SQLException)
Handling Mechanisms:
try {
// Risky code
} catch (SpecificException e) {
// Handle specific case
} catch (Exception e) {
// Generic fallback (use sparingly)
} finally {
// Always executes (except System.exit()), for cleanup (e.g., closing streams)
}
-
Try-with-resources (Java 7+): For
AutoCloseableresources (streams, connections).try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { // Use br } // br.close() called automatically
Creating Custom Exceptions:
-
Checked:
class MyCheckedException extends Exception { ... }(requiresthrowsdeclaration). -
Unchecked:
class MyRuntimeException extends RuntimeException { ... }(nothrowsneeded).
Best Practices:
-
Catch specific exceptions, not
ExceptionorThrowable. -
Never ignore caught exceptions (empty
catchblock). -
Preserve stack trace: Use
throw new MyException("msg", e);for exception chaining. -
Log exceptions with context (using
Logger). -
Use unchecked exceptions for programming errors (invalid arguments).
-
Use checked exceptions for recoverable conditions (file not found).
[!TIP] Lab Focus: Wrap I/O and JDBC code in try-with-resources. Create domain-specific exceptions (e.g.,
InvalidUserInputException).
4.4. File I/O and Serialization
Byte Streams vs. Character Streams:
-
Byte Streams (
InputStream/OutputStream): For raw binary data (images, .class files). Key classes:FileInputStream,FileOutputStream,BufferedInputStream. -
Character Streams (
Reader/Writer): For text data, handles character encoding (UTF-8, etc.). Key classes:FileReader,FileWriter,BufferedReader,BufferedWriter,PrintWriter.
Modern NIO.2 (java.nio.file - Java 7+):
-
Path&Paths: Platform-independent path representation. -
Filesutility class: Static methods for common operations.Path path = Paths.get("data.txt"); String content = Files.readString(path); // Read all text Files.writeString(path, "new content"); // Write all text List<String> lines = Files.readAllLines(path);
Object Serialization:
-
Purpose: Convert an object graph into a byte stream for storage or transmission.
-
Requirements: Class must implement
java.io.Serializable(marker interface). -
Key Classes:
ObjectOutputStream(writeObject()),ObjectInputStream(readObject()). -
serialVersionUID: Explicitly declare to control versioning and avoidInvalidClassException.
[!TIP] Comparison: Prefer NIO.2 (
Files) over oldFile/FileInputStreamfor simpler code and better performance. Use try-with-resources for all streams.
4.5. Multithreading & Concurrency Basics
Thread Creation:
-
Extend
Threadclass and overriderun(). -
Implement
Runnableinterface and pass toThreadconstructor (preferred, as Java doesn't support multiple inheritance).
Thread Lifecycle States:
NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED.
Synchronization & Locks:
-
synchronizedkeyword:-
Method:
public synchronized void method() { ... }(locks onthis). -
Block:
synchronized(lockObject) { ... }(explicit lock object).
-
-
volatilekeyword: Guarantees visibility of changes across threads (reads/writes go directly to main memory). Does not provide atomicity for compound actions (e.g.,i++).
Concurrency Utilities (java.util.concurrent):
-
ExecutorService: Manages a pool of threads.ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(() -> task()); // Runnable Future<String> result = pool.submit(() -> { return "done"; }); // Callable pool.shutdown(); -
Callable<V>: LikeRunnablebut can return a value and throw checked exceptions.
Common Problems:
-
Race Condition: Multiple threads access shared mutable data without proper synchronization. Fix:
synchronizedorLock. -
Deadlock: Two or more threads waiting for each other's locks indefinitely. Fix: Avoid nested locks, enforce lock ordering.
-
Starvation: A thread is perpetually denied access to resources.
[!TIP] Lab Focus: Use
ExecutorServiceover rawThreadcreation. For simple atomic operations, considerjava.util.concurrent.atomicpackage (e.g.,AtomicInteger).
4.6. Introduction to Design Patterns (Creational & Behavioral)
Creational Patterns:
-
Singleton: Ensure a class has only one instance, provide a global point of access.
// Thread-safe (Java 5+) public class Singleton { private static volatile Singleton instance; // volatile for double-checked locking private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } // **Best:** Enum Singleton (inherently serializable & thread-safe) public enum EnumSingleton { INSTANCE; } -
Factory Method: Define an interface for creating an object, but let subclasses decide which class to instantiate.
-
Abstract Factory: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Behavioral Patterns:
-
Observer: Define a one-to-many dependency so that when one object (subject) changes state, all its dependents (observers) are notified automatically.
- Key:
Subjectinterface withattach(Observer),notifyObservers().Observerinterface withupdate().
- Key:
-
Strategy: Define a family of algorithms, encapsulate each one, and make them interchangeable. Lets the algorithm vary independently from clients that use it.
- Key:
Strategyinterface withexecute(). Concrete strategies implement it. Client holds a reference to aStrategy.
- Key:
[!TIP] Application: Use Singleton for configuration managers, database connection pools. Use Strategy for different payment methods, sorting algorithms.
4.7. Java 8+ Features (Lab Integration)
Lambda Expressions: Concise representation of a functional interface (interface with one abstract method).
-
Syntax:
(parameters) -> expressionor(parameters) -> { statements; }. -
Example:
Runnable r = () -> System.out.println("Hello");
Functional Interfaces (from java.util.function):
-
Predicate<T>:test(T t)→boolean(condition). -
Consumer<T>:accept(T t)→void(operation). -
Function<T,R>:apply(T t)→R(transformation). -
Supplier<T>:get()→T(supply value).
Method References: Shorter syntax for lambdas calling an existing method.
-
Class::staticMethod→(args) -> Class.staticMethod(args) -
instance::instanceMethod→(args) -> instance.instanceMethod(args) -
Class::instanceMethod→(obj, args) -> obj.instanceMethod(args)
Streams API: For processing sequences of elements (from collections, arrays, I/O).
-
Intermediate Operations (lazy, return a
Stream):filter,map,sorted,distinct. -
Terminal Operations (eager, produce result/side-effect):
forEach,collect,reduce,count.list.stream() .filter(s -> s.length() > 3) .map(String::toUpperCase) .sorted() .forEach(System.out::println);
Optional<T>: Container object to represent the presence/absence of a value, avoiding NullPointerException.
-
Optional.of(value),Optional.empty(). -
Methods:
isPresent(),get()(unsafe),orElse(default),ifPresent(consumer).
[!TIP] Refactoring: Convert legacy loops:
// Old
for (String s : list) { if (s.length()>3) System.out.println(s); }
// New
list.stream().filter(s -> s.length()>3).forEach(System.out::println);
4.8. Database Connectivity (JDBC)
JDBC Architecture (4-Tier):
-
Application: Your Java code.
-
JDBC API:
DriverManager,Connection,Statement,ResultSet. -
JDBC Driver Manager: Loads driver-specific classes.
-
Database: Actual DBMS.
Core Steps for CRUD:
// 1. Load Driver (optional since JDBC 4.0)
// Class.forName("com.mysql.cj.jdbc.Driver");
// 2. Get Connection
String url = "jdbc:mysql://localhost:3306/db";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
// 3. Create Statement
// For static SQL (no user input):
// Statement stmt = conn.createStatement();
// ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// **ALWAYS use PreparedStatement for queries with user input** (prevents SQL injection)
String sql = "INSERT INTO users(name, email) VALUES(?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "Alice");
pstmt.setString(2, "[email protected]");
int rows = pstmt.executeUpdate(); // For INSERT/UPDATE/DELETE
}
// 4. Process ResultSet (for SELECT)
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
}
}
}
Transaction Management:
conn.setAutoCommit(false); // Start transaction
try {
// multiple DML statements
conn.commit(); // Success
} catch (Exception e) {
conn.rollback(); // Failure
throw e;
}
DAO Pattern: Encapsulate all JDBC code in a Data Access Object class.
UserDAOwith methods:User findById(int id),void save(User user).
[!TIP] Security: Never concatenate user input into SQL strings. Always use
PreparedStatement. Manage resources with try-with-resources.
4.9. Unit Testing with JUnit 5 (Jupiter)
Core Annotations:
-
@Test: Marks a method as a test method. -
@BeforeEach: Runs before each@Testmethod. -
@AfterEach: Runs after each@Testmethod. -
@BeforeAll: Runs once before all tests in the class (must bestatic). -
@AfterAll: Runs once after all tests (must bestatic). -
@DisplayName("Custom Name"): Custom test name in reports.
Assertions (static imports from org.junit.jupiter.api.Assertions):
-
assertEquals(expected, actual) -
assertTrue(condition),assertFalse(condition) -
assertNull(obj),assertNotNull(obj) -
assertThrows(Exception.class, () -> { method(); })// Verify exception thrown -
assertArrayEquals(expectedArray, actualArray)
Test Lifecycle Example:
class MyTest {
@BeforeAll
static void initAll() { /* runs once */ }
@BeforeEach
void init() { /* runs before each test */ }
@Test
@DisplayName("Test adding positive numbers")
void testAdd() {
assertEquals(5, Calculator.add(2, 3));
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(10, 0));
}
@AfterEach
void tearDown() { /* cleanup */ }
@AfterAll
static void tearDownAll() { /* final cleanup */ }
}
Test Naming Convention: methodName_StateUnderTest_ExpectedBehavior() (e.g., withdraw_ insufficientFunds_throwsException).
[!TIP] Lab Focus: Write tests for business logic methods (not getters/setters). Test edge cases and exception paths. Aim for meaningful assertions and isolated tests (no dependency on external resources like DB; use mocks if needed).