Introduction to Java
Java is a high-level, object-oriented, platform-independent programming language developed by Sun Microsystems (now Oracle) in 1995. It was designed with a clear focus on reliability, portability, and long-term maintainability for large systems.
Java uses a two-step execution model: source code is compiled into bytecode by the Java compiler, and that bytecode runs on the Java Virtual Machine (JVM). This “write once, run anywhere” model is what makes Java highly portable across operating systems and hardware.
How the Java Platform Fits Together
- JDK (Java Development Kit): Tools for developers (compiler, debugger, build tools).
- JRE (Java Runtime Environment): The runtime components required to execute Java applications.
- JVM (Java Virtual Machine): The engine that executes bytecode and handles memory management and optimization.
Key Characteristics
- Object-oriented: Encapsulation, inheritance, and polymorphism are core to the language.
- Strongly typed: Types are enforced at compile time to reduce runtime errors.
- Automatic memory management: Garbage collection reduces manual memory errors.
- Rich standard library: Networking, concurrency, collections, I/O, and more.
- Robust tooling ecosystem: Works well with Maven/Gradle, IDEs, and CI/CD.
Where Java Excels
Java is a popular choice for backend services, REST APIs, enterprise systems (banking, telecom), and data-heavy applications. The JVM ecosystem also powers big-data tools like Apache Hadoop and Apache Spark.
Java releases follow a predictable cadence, with long-term support (LTS) versions providing stability for production systems. This makes Java a strong choice for organizations that prioritize reliability and support.
Hello World Example
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
Java Basics
This section covers core building blocks you will use in almost every Java program: variables, data types, operators, and input. Understanding type rules, numeric promotion, and Java’s standard input/output patterns will make the rest of the language much easier.
Variables and Data Types
Java is statically typed. Every variable must be declared with a type, and that type determines the size, range, and valid operations.
- Primitive types: byte, short, int, long, float, double, char, boolean.
- Reference types: String, arrays, classes, interfaces, and objects you create.
- Type inference: Use
var(Java 10+) for local variables when the type is obvious.
Numeric literals can include underscores for readability and suffixes for type:
int population = 1_500_000;
long distance = 9_460_000_000L;
double pi = 3.14159;
float rate = 7.5f;
Java performs automatic numeric promotions in expressions. For example, byte and short are promoted to int during arithmetic, and mixing int with double yields a double.
int age = 20;
double salary = 45000.50;
char grade = 'A';
boolean isJavaFun = true;
String name = "Student";
Operators
Operators are grouped into several categories. The most common ones include arithmetic, relational, logical, assignment, and ternary operators. Operator precedence matters, so use parentheses when in doubt.
int a = 10, b = 5;
int sum = a + b; // Arithmetic
boolean res = (a > b); // Relational
a += 5; // Assignment
boolean cond = (a > 0 && b < 10); // Logical
The ternary operator is a compact alternative to if-else:
int max = (a > b) ? a : b;
Strings and StringBuilder
Strings are immutable, meaning modifications create a new object. For frequent concatenation in loops, use StringBuilder for better performance.
StringBuilder sb = new StringBuilder();
sb.append("Java").append(" ").append("Basics");
String result = sb.toString();
Input from User
The Scanner class is convenient for basic input. For performance-sensitive input, many programs use BufferedReader or custom fast scanners.
import java.util.Scanner;
Scanner sc = new Scanner(System.in);
System.out.print("Enter your name: ");
String name = sc.nextLine();
System.out.println("Hello " + name);
Always close the scanner when you are done to free resources.
Control Statements
Control statements direct the flow of execution in a program. Java provides branching, looping, and jump statements to write clear and efficient logic.
If-Else
Use if for conditional execution. Combine conditions with && and || (short-circuit operators) to avoid unnecessary checks.
int number = 10;
if(number > 0){
System.out.println("Positive number");
}else{
System.out.println("Negative number");
}
For multiple ranges, use else if chains and keep conditions readable with parentheses.
Switch
switch is ideal when checking one value against multiple constants. Modern Java supports switch expressions for concise mappings.
int day = 3;
switch(day){
case 1: System.out.println("Monday"); break;
case 2: System.out.println("Tuesday"); break;
default: System.out.println("Other day");
}
Modern switch with arrow syntax:
String label = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
default -> "Other day";
};
Loops
for is best for known counts, while for condition-driven loops, and do-while when the body must run at least once.
// For loop
for(int i=0;i<5;i++){
System.out.println(i);
}
// While loop
int j=0;
while(j<5){
System.out.println(j);
j++;
}
// Do-while loop
int k=0;
do{
System.out.println(k);
k++;
}while(k<5);
Break and Continue
break exits a loop early, while continue skips the current iteration.
for (int k = 1; k <= 5; k++) {
if (k == 3) continue; // skip 3
if (k == 5) break; // stop at 4
System.out.println(k);
}
You can also use labeled break to exit nested loops safely.
Functions / Methods
In Java, functions are called methods. Methods live inside classes and define reusable behavior. A method can accept parameters, return a value, or both. Access modifiers and the static keyword control how a method is used.
Method Structure
A method signature includes the access modifier, return type, name, and parameter list. The return type can be a concrete type or void if nothing is returned.
public class Calculator {
// Method with parameters
public int add(int a, int b){
return a + b;
}
public static void main(String[] args){
Calculator calc = new Calculator();
int sum = calc.add(5, 10);
System.out.println("Sum: " + sum);
}
}
Static vs Instance Methods
Instance methods operate on a specific object and can access instance fields. Static methods belong to the class and are called without creating an object.
class MathUtil {
static int square(int x) { return x * x; }
int increment(int x) { return x + 1; }
}
int a = MathUtil.square(5); // static call
MathUtil util = new MathUtil();
int b = util.increment(5); // instance call
Method Overloading
Java supports overloading: same method name with different parameter lists. Overloading improves readability for similar operations.
int sum(int a, int b) { return a + b; }
int sum(int a, int b, int c) { return a + b + c; }
double sum(double a, double b) { return a + b; }
Pass-by-Value Semantics
Java is strictly pass-by-value. For primitives, the value is copied. For objects, the reference is copied, so methods can modify the object’s state but cannot replace the caller’s reference.
Return Values and Early Exit
Use return to send a value back or to exit early from a method.
int findPositive(int[] nums) {
for (int n : nums) {
if (n > 0) return n;
}
return -1;
}
Object-Oriented Programming (OOP)
Java is built around OOP, which organizes software as objects that combine data (fields) and behavior (methods). The four pillars are encapsulation, inheritance, polymorphism, and abstraction.
Classes and Objects
A class is a blueprint; an object is an instance. Constructors initialize object state and can be overloaded.
public class Person {
String name;
int age;
public void display(){
System.out.println("Name: "+name+", Age: "+age);
}
public static void main(String[] args){
Person p = new Person();
p.name = "John";
p.age = 25;
p.display();
}
}
Constructors
class Car {
String model;
int year;
Car(String model, int year) {
this.model = model;
this.year = year;
}
}
Inheritance
Inheritance lets a class reuse and extend behavior from a parent class. Use super to access parent members and constructors.
class Animal {
void eat(){ System.out.println("Eating"); }
}
class Dog extends Animal {
void bark(){ System.out.println("Barking"); }
}
public class Test {
public static void main(String[] args){
Dog d = new Dog();
d.eat();
d.bark();
}
}
Polymorphism
Polymorphism allows the same method call to behave differently based on the object type. Java supports compile-time (overloading) and runtime (overriding) polymorphism.
// Compile-time (Method Overloading)
class MathOp {
int add(int a,int b){ return a+b; }
double add(double a,double b){ return a+b; }
}
// Runtime (Method Overriding)
class Animal {
void sound(){ System.out.println("Animal sound"); }
}
class Dog extends Animal {
void sound(){ System.out.println("Dog barks"); }
}
With dynamic dispatch, an Animal reference can point to a Dog object and call the overridden method.
Animal a = new Dog();
a.sound(); // Dog barks
Abstraction & Interfaces
Abstraction hides implementation details and exposes only essentials. Use abstract classes for shared base behavior and interfaces for contracts.
// Abstract class
abstract class Shape {
abstract void draw();
}
// Interface
interface Drawable {
void draw();
}
class Circle implements Drawable {
public void draw(){ System.out.println("Draw Circle"); }
}
Interfaces can also define default methods (Java 8+), allowing shared behavior without breaking existing implementations.
Encapsulation
Encapsulation protects data by keeping fields private and exposing controlled access through getters/setters. This improves maintainability and validation.
class Student {
private String name;
public void setName(String name){ this.name = name; }
public String getName(){ return name; }
}
Access Modifiers
private limits access to the same class, protected allows subclasses and same-package access, public exposes members to all, and package-private (no modifier) limits to the same package.
Composition vs Inheritance
Prefer composition when you want to reuse behavior without tight coupling. Inheritance is powerful but can make designs rigid if overused.
Exception Handling
Java uses exceptions to signal abnormal conditions at runtime. Handling exceptions correctly makes programs safer and easier to maintain by separating error-handling logic from normal flow.
Try-Catch-Finally
try contains risky code, catch handles errors, and finally always runs for cleanup.
try{
int result = 10/0;
}catch(ArithmeticException e){
System.out.println("Cannot divide by zero!");
}finally{
System.out.println("Execution complete");
}
Checked vs Unchecked Exceptions
- Checked: Must be handled or declared (e.g.,
IOException). - Unchecked: Runtime exceptions (e.g.,
NullPointerException).
Multiple Catch Blocks
You can handle different exception types separately, from most specific to most general.
try {
// risky code
} catch (NumberFormatException e) {
System.out.println("Invalid number");
} catch (Exception e) {
System.out.println("General error");
}
Try-With-Resources
For I/O resources that must be closed, use try-with-resources to avoid leaks.
try (java.io.BufferedReader br =
new java.io.BufferedReader(new java.io.FileReader("data.txt"))) {
System.out.println(br.readLine());
}
Throwing Exceptions
Use throw to raise an exception, and throws to declare it in a method signature.
void validateAge(int age) throws IllegalArgumentException {
if (age < 18) {
throw new IllegalArgumentException("Age must be 18+");
}
}
Custom Exceptions
Create custom exception classes for domain-specific errors.
class InvalidOrderException extends Exception {
public InvalidOrderException(String message) {
super(message);
}
}
Collections Framework
The Collections Framework provides a unified architecture for storing and manipulating groups of objects. It includes interfaces (e.g., List, Set, Map, Queue), concrete implementations, and utility methods in java.util.Collections.
Core Interfaces
- List: Ordered, allows duplicates.
- Set: Unique elements (no duplicates).
- Map: Key-value pairs, keys are unique.
- Queue/Deque: FIFO/LIFO access patterns.
List
Use ArrayList for fast random access, and LinkedList for frequent insertions/removals in the middle.
import java.util.ArrayList; ArrayListlist = new ArrayList<>(); list.add("Java"); list.add("Python"); System.out.println(list);
Set
HashSet is fast and unordered. Use LinkedHashSet to preserve insertion order, and TreeSet for sorted order.
import java.util.HashSet; HashSetset = new HashSet<>(); set.add(1); set.add(2); set.add(1); System.out.println(set); // No duplicates
Map
HashMap provides fast lookup. Use LinkedHashMap to preserve insertion order, and TreeMap for sorted keys.
import java.util.HashMap; HashMapmap = new HashMap<>(); map.put("John",25); map.put("Alice",30); System.out.println(map.get("John"));
Queue
Use Queue for FIFO, and Deque for both ends (stack/queue behavior).
import java.util.LinkedList; Queuequeue = new LinkedList<>(); queue.add(1); queue.add(2); System.out.println(queue.poll()); // 1
Iteration and Traversal
Java provides enhanced for-loops, iterators, and streams for traversal.
for (String item : list) {
System.out.println(item);
}
list.forEach(System.out::println);
Utility Methods
The Collections class provides helpers like sorting and unmodifiable wrappers.
import java.util.Collections; Collections.sort(list); ListreadOnly = Collections.unmodifiableList(list);
Generics
Generics enable type-safe code by allowing classes, interfaces, and methods to operate on typed parameters. They help eliminate casts and prevent runtime ClassCastException errors.
Generic Classes
class Box{ private T value; public void set(T value){ this.value = value; } public T get(){ return value; } } Box intBox = new Box<>(); intBox.set(10); System.out.println(intBox.get());
Generic Methods
A method can declare its own type parameter independent of the class.
public staticvoid printArray(T[] arr) { for (T item : arr) { System.out.println(item); } }
Bounds (extends / super)
Use bounded types to restrict the kind of types allowed.
// Upper bound class NumberBox{ T value; NumberBox(T value){ this.value = value; } } // Lower bound with wildcards static void addIntegers(java.util.List super Integer> list) { list.add(1); list.add(2); }
Wildcards
Wildcards make APIs more flexible:
? extends Tfor read-only (producer)? super Tfor write-only (consumer)
Type Erasure
Java generics are implemented with type erasure, meaning type information is removed at runtime. This is why you cannot create arrays of generic types directly.
File I/O
Java provides two primary I/O APIs: the classic java.io streams and the newer java.nio.file package. Use streams for sequential reads/writes and NIO for higher-level utilities and better performance.
Buffered Streams
Buffered I/O reduces system calls and improves performance when reading or writing files.
import java.io.*;
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"));
writer.write("Hello Java");
writer.close();
BufferedReader reader = new BufferedReader(new FileReader("output.txt"));
String line = reader.readLine();
System.out.println(line);
reader.close();
Try-With-Resources
Always close streams. Try-with-resources automatically closes them even if errors occur.
try (BufferedWriter w = new BufferedWriter(new FileWriter("data.txt"))) {
w.write("Line 1");
}
java.nio.file (Recommended)
The NIO API provides convenient methods for reading/writing files in one line, listing directories, and working with paths.
import java.nio.file.*;
Path path = Paths.get("notes.txt");
Files.writeString(path, "Hello NIO");
String content = Files.readString(path);
System.out.println(content);
Reading Large Files
Use streaming to avoid loading entire files into memory.
try (BufferedReader br = new BufferedReader(new FileReader("big.txt"))) {
String line;
while ((line = br.readLine()) != null) {
// process line
}
}
Multithreading
Multithreading allows multiple tasks to run concurrently within the same process. It improves responsiveness and throughput but requires careful handling of shared data.
Thread Lifecycle
Common states include NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. Use Thread.getState() for debugging.
Creating Threads
// Using Thread class
class MyThread extends Thread {
public void run(){ System.out.println("Thread running"); }
}
MyThread t1 = new MyThread();
t1.start();
// Using Runnable interface
class MyRunnable implements Runnable {
public void run(){ System.out.println("Runnable running"); }
}
Thread t2 = new Thread(new MyRunnable());
t2.start();
Synchronization & Locks
Use synchronized to protect critical sections. For more control (try-lock, fairness), use ReentrantLock.
class Counter {
private int value = 0;
public synchronized void inc() { value++; }
public synchronized int get() { return value; }
}
import java.util.concurrent.locks.*;
class SafeCounter {
private int value = 0;
private final Lock lock = new ReentrantLock();
public void inc() {
lock.lock();
try { value++; } finally { lock.unlock(); }
}
}
Volatile & Atomic Variables
volatile guarantees visibility, not atomicity. For atomic updates, use classes from java.util.concurrent.atomic.
import java.util.concurrent.atomic.AtomicInteger;
class Stats {
private final AtomicInteger hits = new AtomicInteger();
public void record() { hits.incrementAndGet(); }
public int getHits() { return hits.get(); }
}
Wait/Notify (Coordination)
Use wait() and notify() for condition-based coordination. Always call them inside a synchronized block.
class SimpleQueue {
private final java.util.LinkedList q = new java.util.LinkedList<>();
public synchronized void put(int x) {
q.add(x);
notify();
}
public synchronized int take() throws InterruptedException {
while (q.isEmpty()) wait();
return q.removeFirst();
}
}
Executor Framework (Recommended)
Prefer executors over manual thread creation. They manage thread pools and task lifecycles.
import java.util.concurrent.*; ExecutorService pool = Executors.newFixedThreadPool(4); Futuref = pool.submit(() -> 42); System.out.println("Result: " + f.get()); pool.shutdown();
CompletableFuture (Async Pipelines)
Compose async tasks without blocking and handle errors gracefully.
import java.util.concurrent.*;
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println)
.exceptionally(ex -> { ex.printStackTrace(); return null; });
Thread Safety Tips
- Prefer immutability and local variables.
- Use concurrent collections like
ConcurrentHashMap. - Minimize shared state and keep locks small.
- Avoid deadlocks by acquiring locks in a consistent order.
Java Streams & Lambda Expressions
Streams provide a declarative, functional-style pipeline for processing collections. Lambdas are concise implementations of functional interfaces and are the building blocks of stream operations.
Stream Pipeline (Source → Intermediate → Terminal)
import java.util.*; Listnums = Arrays.asList(1,2,3,4,5); nums.stream().filter(n -> n%2==0).forEach(System.out::println);
Intermediate Operations (Lazy)
Intermediate operations are lazy and only run when a terminal operation is invoked.
Listnames = Arrays.asList("Alice","Bob","Charlie","Anita"); List result = names.stream() .filter(n -> n.startsWith("A")) .map(String::toUpperCase) .sorted() .toList();
Terminal Operations
Terminal operations produce a result or a side effect and close the stream.
long count = names.stream().filter(n -> n.length() > 3).count(); Optionalany = names.stream().findAny(); int sum = nums.stream().reduce(0, Integer::sum);
Collectors (Grouping, Mapping, Reducing)
import java.util.stream.*;
class Person { String dept; int salary; Person(String d,int s){dept=d;salary=s;} }
List people = List.of(new Person("IT", 120), new Person("HR", 90), new Person("IT", 150));
Map> byDept = people.stream()
.collect(Collectors.groupingBy(p -> p.dept));
Map totalByDept = people.stream()
.collect(Collectors.groupingBy(p -> p.dept, Collectors.summingInt(p -> p.salary)));
FlatMap (Flatten Nested Structures)
List> matrix = List.of(List.of(1,2), List.of(3,4)); List
flat = matrix.stream() .flatMap(List::stream) .toList();
Parallel Streams (Use Carefully)
Parallel streams can speed up CPU-bound work but may hurt performance for small datasets or blocking I/O.
int total = nums.parallelStream()
.mapToInt(n -> n * n)
.sum();
Lambdas & Functional Interfaces
A lambda implements a functional interface (an interface with a single abstract method).
@FunctionalInterface
interface Formatter { String format(String s); }
Formatter f = s -> "[" + s.toUpperCase() + "]";
System.out.println(f.format("java"));
Method References
Listlist = Arrays.asList("a","bb","ccc"); list.stream().map(String::length).forEach(System.out::println);
Primitive Streams
Use IntStream, LongStream, and DoubleStream to avoid boxing.
int max = IntStream.of(3, 7, 2).max().orElse(0);
double avg = IntStream.range(1, 6).average().orElse(0.0);
Stream Best Practices
- Prefer pure functions: avoid mutating external state inside stream operations.
- Keep pipelines short and readable; extract helper methods if needed.
- Use
peek()only for debugging. - Choose sequential streams by default; benchmark before using parallel.
Java Networking
Java networking supports low-level TCP/UDP sockets and high-level HTTP clients. Use TCP for reliable streams, UDP for low-latency datagrams, and HTTP for web APIs.
TCP Client (Socket)
import java.net.*;
Socket socket = new Socket("example.com", 80);
System.out.println("Connected to server");
socket.close();
TCP Server (ServerSocket)
import java.io.*;
import java.net.*;
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
out.println("Hello Client");
client.close();
server.close();
UDP (DatagramSocket)
import java.net.*;
byte[] buf = "ping".getBytes();
DatagramSocket ds = new DatagramSocket();
DatagramPacket pkt = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 9999);
ds.send(pkt);
ds.close();
HTTP Client (Java 11+)
import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://example.com")).GET().build();
HttpResponse res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode());
Timeouts & Error Handling
Always set timeouts and handle exceptions to avoid hanging sockets.
Socket s = new Socket();
s.connect(new InetSocketAddress("example.com", 80), 2000);
s.setSoTimeout(2000);
Concurrency (Handling Many Clients)
Use a thread pool to handle multiple clients efficiently.
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(8);
while (true) {
Socket c = server.accept();
pool.submit(() -> handleClient(c));
}
Best Practices
- Prefer HTTP client for web APIs; use raw sockets for custom protocols.
- Close streams/sockets with try-with-resources.
- Validate input to avoid protocol injection.
- Use TLS (SSL) for secure communication.
Java GUI (Swing)
Swing is a lightweight, platform-independent GUI toolkit. It follows a single-threaded model: all UI updates must occur on the Event Dispatch Thread (EDT).
Basic Window Setup (EDT)
import javax.swing.*;
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("My Window");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(320, 220);
frame.setVisible(true);
});
Layout Managers
Use layout managers instead of absolute positioning for responsive UIs.
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JButton("North"), BorderLayout.NORTH);
panel.add(new JButton("Center"), BorderLayout.CENTER);
panel.add(new JButton("South"), BorderLayout.SOUTH);
frame.setContentPane(panel);
Event Handling
JButton btn = new JButton("Click Me");
btn.addActionListener(e -> System.out.println("Clicked"));
Model-View Separation
Keep UI separate from data logic to improve maintainability.
DefaultListModelmodel = new DefaultListModel<>(); model.addElement("Item 1"); JList list = new JList<>(model);
Dialogs & Validation
String name = JOptionPane.showInputDialog(frame, "Enter name:");
if (name == null || name.isBlank()) {
JOptionPane.showMessageDialog(frame, "Name required");
}
Custom Painting
Override paintComponent for custom drawing.
JPanel canvas = new JPanel() {
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawOval(20, 20, 80, 80);
}
};
Background Tasks (SwingWorker)
Use SwingWorker for long-running tasks without freezing the UI.
SwingWorkerworker = new SwingWorker<>() { protected Integer doInBackground() throws Exception { Thread.sleep(1000); return 42; } protected void done() { try { System.out.println(get()); } catch (Exception ignored) {} } }; worker.execute();
Best Practices
- Update UI only on the EDT.
- Prefer layout managers over absolute positioning.
- Keep listeners small; delegate to services.
- Dispose windows you no longer use to avoid leaks.
Advanced Topics
This section covers common enterprise features and advanced language capabilities used in real-world Java applications.
JDBC Example
import java.sql.*;
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb","root","");
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while(rs.next()){
System.out.println(rs.getString("name"));
}
con.close();
JDBC Best Practices
Use PreparedStatement to avoid SQL injection and enable query caching.
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setInt(1, 10);
ResultSet rs = ps.executeQuery();
}
Transactions
Transactions ensure atomic updates. Always commit or roll back on errors.
try (Connection con = DriverManager.getConnection(url, user, pass)) {
con.setAutoCommit(false);
try (PreparedStatement ps = con.prepareStatement("UPDATE accounts SET bal=bal-? WHERE id=?")) {
ps.setInt(1, 100);
ps.setInt(2, 1);
ps.executeUpdate();
}
con.commit();
} catch (SQLException e) {
// con.rollback() if con is available
}
Annotations & Reflection
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@interface Info { String author(); }
@Info(author="John")
class MyClass{}
Info info = MyClass.class.getAnnotation(Info.class);
System.out.println(info.author());
Reflection (Fields & Methods)
Class> cls = Class.forName("com.example.User");
Object obj = cls.getDeclaredConstructor().newInstance();
for (var m : cls.getDeclaredMethods()) {
System.out.println(m.getName());
}
Serialization
Serialize objects for storage or network transfer. Use serialVersionUID for compatibility.
import java.io.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String n){ name = n; }
}
Generics (Advanced)
Use bounds and wildcards to make APIs flexible while keeping type safety.
static double sum(List extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
Best Practices
- Use connection pools for high-traffic apps.
- Prefer annotations over XML for configuration.
- Limit reflection usage due to performance and security.
- Handle checked exceptions explicitly for clarity.
JVM Memory Model (High-Level)
The JVM divides memory into stack (method calls, local variables), heap (objects), and metaspace (class metadata). Understanding these areas helps diagnose leaks and performance issues.
// Stack: each thread has its own call stack
// Heap: shared objects, managed by GC
// Metaspace: class metadata (replaces PermGen)
JVM Memory Model (More Detail)
Heap is typically split into Young Generation (Eden + Survivor spaces) and Old Generation. Most objects die young, so keeping short-lived objects in young gen reduces GC cost.
// Eden: new objects start here
// Survivor: objects that survive GC cycles
// Old Gen: long-lived objects promoted from Young Gen
// Direct/Off-Heap: ByteBuffer.allocateDirect() uses native memory
Garbage Collection (GC) Tuning Basics
GC automatically reclaims heap memory. Tuning focuses on latency vs throughput and heap sizing.
// Common options (examples)
// -Xms512m -Xmx2g (heap size)
// -XX:+UseG1GC (G1 collector)
// -XX:MaxGCPauseMillis=200 (target pause time)
GC Logs & Analysis
Enable GC logs to inspect pause times, allocation rates, and promotion patterns.
// Java 9+ unified logging
// -Xlog:gc*:file=gc.log:time,uptime,level,tags
// Key signals:
// - Frequent young GCs: high allocation rate
// - Long old GC pauses: old gen pressure or large heaps
GC Collector Choice (Summary)
G1 is the default in modern JVMs and balances latency/throughput. ZGC and Shenandoah target very low pause times on large heaps.
Spring Basics (Intro)
Spring is a popular Java framework for building enterprise apps. Core ideas include dependency injection (DI), inversion of control (IoC), and annotations for configuration.
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
class UserService {
private final UserRepo repo;
@Autowired
UserService(UserRepo repo) { this.repo = repo; }
}
Spring Boot Starters
Starters are dependency bundles that quickly enable features (web, data, security). They simplify setup and provide sensible defaults.
// build.gradle (example)
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
}
Spring Boot REST Example
import org.springframework.web.bind.annotation.*;
@RestController
class HelloController {
@GetMapping("/hello")
String hello() { return "Hello Spring"; }
}