Java is a statically typed, object-oriented, platform-independent programming language released by Sun Microsystems in 1995. Its "Write Once, Run Anywhere" promise comes from the Java Virtual Machine (JVM).
The full toolkit for developers: compiler (javac), JRE, debugger, and standard libraries. Install this to write Java.
Executes Java bytecode. Platform-specific but Java code is not — the JVM handles the translation automatically.
JVM + standard libraries. Only needed to run Java programs, not develop them.
Enterprise backends · Android · Big data (Hadoop, Spark) · Financial systems · Cloud microservices
.java → compiler produces .class (bytecode) → JVM interprets bytecode on any OS. This is why Java is platform-independent at the source level.
Install JDK 17 (LTS) from adoptium.net or Oracle, then use VS Code or IntelliJ IDEA.
- 1Install JDK 17: Download from adoptium.net, run the installer, and verify with
java --versionin your terminal. - 2Install IntelliJ IDEA Community: Free IDE with first-class Java support, auto-import, refactoring, and debugger built in.
- 3Install Maven or Gradle: Build tools that manage dependencies. Spring Boot projects use Maven by default.
- 4Write and compile: Create
Main.java, write your code, run withjavac Main.javathenjava Main.
// Every Java program starts with a class matching the filename public class Main { // The entry point — JVM calls this first public static void main(String[] args) { // Print a message to the console System.out.println("Hello, Java World!"); // Formatted output (like printf) String name = "Alice"; System.out.printf("Welcome, %s!%n", name); } }
Main.java contains public class Main. This is a hard requirement of the compiler.
Java is statically typed — you must declare the type of every variable. Types fall into two categories: primitives (stored by value) and reference types (stored as memory addresses).
public class DataTypes { public static void main(String[] args) { // ── PRIMITIVE TYPES ────────────────────────────── int age = 25; // 32-bit integer long bigNum = 9_876_543_210L; // 64-bit (note L suffix) double price = 19.99; // 64-bit decimal float rate = 0.18f; // 32-bit decimal (note f) char grade = 'A'; // single character boolean active = true; // true / false only byte small = 127; // -128 to 127 // ── REFERENCE TYPES ────────────────────────────── String name = "Alice Mugisha"; // immutable text int[] scores = {90, 85, 92}; // array // var keyword (Java 10+) — type inferred by compiler var city = "Kigali"; // compiler infers String // ── TYPE CONVERSION ────────────────────────────── int x = 42; double d = x; // implicit widening (safe) int y = (int) 9.99; // explicit casting (truncates) // String ↔ number conversion String numStr = String.valueOf(42); int parsed = Integer.parseInt("100"); System.out.printf("Name: %s, Age: %d, Price: %.2f%n", name, age, price); } }
| Type | Size | Range / Notes | Default |
|---|---|---|---|
| byte | 8 bit | -128 to 127 | 0 |
| int | 32 bit | ±2.1 billion | 0 |
| long | 64 bit | ±9.2 quintillion — use L suffix | 0L |
| double | 64 bit | Decimal — preferred over float | 0.0 |
| char | 16 bit | Unicode — single quotes only | '\u0000' |
| boolean | 1 bit | true / false | false |
| String | ref | Immutable sequence of chars | null |
public class Operators { public static void main(String[] args) { // ── ARITHMETIC ──────────────────────────────────── int a = 10, b = 3; System.out.println(a + b); // 13 — addition System.out.println(a - b); // 7 — subtraction System.out.println(a * b); // 30 — multiplication System.out.println(a / b); // 3 — integer division System.out.println(a % b); // 1 — modulus System.out.println((double) a / b); // 3.33 — cast first // Compound assignment a += 5; // a = a + 5 a++; // a = a + 1 (post-increment) // ── RELATIONAL ──────────────────────────────────── System.out.println(a == b); // false — equal System.out.println(a != b); // true — not equal System.out.println(a > b); // true System.out.println(a <= b); // false // ── LOGICAL ────────────────────────────────────── boolean hasId = true; boolean isOver18 = true; System.out.println(hasId && isOver18); // true — AND System.out.println(hasId || isOver18); // true — OR System.out.println(!hasId); // false — NOT // ── TERNARY ────────────────────────────────────── int score = 75; String result = (score >= 50) ? "Pass" : "Fail"; System.out.println(result); // Pass } }
public class ControlFlow { public static void main(String[] args) { // ── IF / ELSE IF / ELSE ─────────────────────────── int score = 78; if (score >= 90) System.out.println("A"); else if (score >= 80) System.out.println("B"); else if (score >= 70) System.out.println("C"); else System.out.println("F"); // ── SWITCH EXPRESSION (Java 14+) ────────────────── String day = "MONDAY"; String type = switch (day) { case "SATURDAY", "SUNDAY" -> "Weekend"; default -> "Weekday"; }; System.out.println(day + ": " + type); // ── FOR LOOP ────────────────────────────────────── for (int i = 1; i <= 5; i++) { System.out.print(i + " "); } System.out.println(); // newline // ── ENHANCED FOR (for-each) ─────────────────────── String[] students = {"Alice", "Bob", "Claire"}; for (String student : students) { System.out.println("Hello, " + student); } // ── WHILE LOOP ──────────────────────────────────── int count = 0; while (count < 3) { System.out.println("Count: " + count); count++; } // ── DO-WHILE: runs at least once ───────────────── int n = 10; do { System.out.println("n = " + n); n++; } while (n < 10); // runs once even though false } }
Methods are named blocks of reusable code. They can accept parameters, return values, and be overloaded with different parameter signatures.
public class Methods { // ── BASIC METHOD ───────────────────────────────────── public static double calculateTax(double amount, double rate) { return amount * rate; } // ── VOID METHOD (no return value) ──────────────────── public static void printLine(String label, int value) { System.out.printf("%-15s: %d%n", label, value); } // ── METHOD OVERLOADING: same name, different params ─── public static int add(int a, int b) { return a + b; } public static double add(double a, double b) { return a + b; } public static int add(int a, int b, int c) { return a + b + c; } // ── VARARGS: variable number of arguments ───────────── public static int sum(int... numbers) { int total = 0; for (int n : numbers) total += n; return total; } public static void main(String[] args) { System.out.println(calculateTax(10_000, 0.18)); // 1800.0 System.out.println(add(3, 4)); // 7 (int) System.out.println(add(1.5, 2.5)); // 4.0 (double) System.out.println(sum(1, 2, 3, 4, 5)); // 15 } }
import java.util.Arrays; public class ArraysAndStrings { public static void main(String[] args) { // ── ARRAYS ─────────────────────────────────────────── int[] scores = new int[5]; // declare, size 5 int[] grades = {92, 85, 78, 96}; // declare + initialise grades[0] = 95; // update index 0 System.out.println(grades.length); // 4 Arrays.sort(grades); // sort in place System.out.println(Arrays.toString(grades)); // [78, 85, 95, 96] // 2D array (matrix) int[][] matrix = {{1,2},{3,4},{5,6}}; System.out.println(matrix[1][0]); // 3 // ── STRINGS ────────────────────────────────────────── String name = "Alice Mugisha"; System.out.println(name.length()); // 13 System.out.println(name.toUpperCase()); // ALICE MUGISHA System.out.println(name.substring(6)); // Mugisha System.out.println(name.contains("Alice")); // true System.out.println(name.replace("Alice", "Bob")); // Bob Mugisha System.out.println(name.trim()); // removes whitespace String[] parts = name.split(" "); System.out.println(parts[0]); // Alice // String comparison — always use .equals(), not == String a = "hello", b = "hello"; System.out.println(a.equals(b)); // true ✅ System.out.println(a.equalsIgnoreCase("HELLO")); // true // StringBuilder — mutable, efficient for concatenation StringBuilder sb = new StringBuilder(); sb.append("Java ").append("is ").append("great!"); System.out.println(sb.toString()); // Java is great! } }
A class is a blueprint. An object is an instance of a class — a specific entity created from that blueprint with its own data.
/** * Represents a student in the management system. * Demonstrates class structure, fields, methods, and toString. */ public class Student { // ── FIELDS (instance variables) ───────────────────── private int id; private String name; private int age; private String grade; // Static field — shared by ALL instances private static String schoolName = "Tech Academy"; // ── CONSTRUCTOR ───────────────────────────────────── public Student(int id, String name, int age, String grade) { this.id = id; this.name = name; this.age = age; this.grade = grade; } // ── INSTANCE METHOD ────────────────────────────────── public boolean isPassing() { return !grade.equals("F") && !grade.equals("D"); } // ── toString — called by println() ─────────────────── @Override public String toString() { return String.format("Student{id=%d, name='%s', grade='%s'}", id, name, grade); } // Getters and setters (see Encapsulation lesson) public String getName() { return name; } public int getAge() { return age; } public String getGrade() { return grade; } public void setGrade(String grade) { this.grade = grade; } }
public class Main { public static void main(String[] args) { // Create objects using 'new' Student alice = new Student(1, "Alice", 20, "A"); Student bob = new Student(2, "Bob", 22, "C"); System.out.println(alice); // calls toString() System.out.println(alice.isPassing()); // true alice.setGrade("A+"); System.out.println(alice.getGrade()); // A+ } }
Constructors initialise a new object. You can have multiple constructors (constructor overloading) for flexible object creation.
public class Product { private String name; private double price; private int stock; // Constructor 1: all fields public Product(String name, double price, int stock) { this.name = name; this.price = price; this.stock = stock; } // Constructor 2: default stock = 0 public Product(String name, double price) { this(name, price, 0); // delegate to Constructor 1 } // Copy constructor public Product(Product other) { this(other.name, other.price, other.stock); } }
Encapsulation means hiding internal state and providing controlled access through methods. The four access modifiers control visibility.
| Modifier | Same Class | Same Package | Subclass | Anywhere |
|---|---|---|---|---|
| private | ✓ | ✗ | ✗ | ✗ |
| (default) | ✓ | ✓ | ✗ | ✗ |
| protected | ✓ | ✓ | ✓ | ✗ |
| public | ✓ | ✓ | ✓ | ✓ |
public class BankAccount { private String owner; private double balance; // hidden — access only via methods public BankAccount(String owner, double initialBalance) { this.owner = owner; this.balance = Math.max(0, initialBalance); // guard } public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("Amount must be positive"); balance += amount; } public void withdraw(double amount) { if (amount > balance) throw new IllegalStateException("Insufficient funds"); balance -= amount; } // Read-only access — no setter for balance public double getBalance() { return balance; } public String getOwner() { return owner; } }
Inheritance lets a child class reuse and extend a parent class. Java supports single inheritance for classes but multiple inheritance through interfaces.
// ── PARENT CLASS ───────────────────────────────────── public class Person { protected String name; protected int age; public Person(String name, int age) { this.name = name; this.age = age; } public String introduce() { return String.format("Hi, I'm %s, age %d.", name, age); } } // ── CHILD CLASS — extends inherits everything ───────── public class Student extends Person { private String major; public Student(String name, int age, String major) { super(name, age); // must call parent constructor first this.major = major; } @Override // override parent's introduce() public String introduce() { return super.introduce() + " I study " + major + "."; } } // ── GRANDCHILD CLASS ────────────────────────────────── public class GraduateStudent extends Student { private String thesisTopic; public GraduateStudent(String name, int age, String major, String thesis) { super(name, age, major); this.thesisTopic = thesis; } @Override public String introduce() { return super.introduce() + " Thesis: " + thesisTopic; } }
Polymorphism means "many forms". The same method call on a parent type dispatches to the correct child implementation at runtime.
import java.util.List; public class PolymorphismDemo { public static void main(String[] args) { // Parent type references can hold child objects Person alice = new Student("Alice", 20, "CS"); Person bob = new GraduateStudent("Bob", 25, "Math", "AI Ethics"); Person claire = new Person("Claire", 30); // Polymorphic call — correct introduce() is called at runtime List<Person> people = List.of(alice, bob, claire); for (Person p : people) { System.out.println(p.introduce()); // dynamic dispatch } // instanceof check before downcasting if (alice instanceof Student s) { // Java 16+ pattern System.out.println("Major: " + s.getMajor()); } } }
// ── INTERFACE: contract with no state ──────────────── public interface Payable { double calculatePay(); // abstract by default default String paymentSummary() { // default implementation return String.format("Pay: RWF %.2f", calculatePay()); } } public interface Describable { String describe(); } // ── ABSTRACT CLASS: partial implementation ──────────── public abstract class Employee implements Payable, Describable { protected String name; protected double baseSalary; public Employee(String name, double baseSalary) { this.name = name; this.baseSalary = baseSalary; } // Subclasses MUST implement this public abstract double calculateBonus(); @Override public String describe() { return name + " earns " + paymentSummary(); } } // ── CONCRETE IMPLEMENTATION ─────────────────────────── public class FullTimeEmployee extends Employee { public FullTimeEmployee(String name, double salary) { super(name, salary); } @Override public double calculatePay() { return baseSalary; } @Override public double calculateBonus() { return baseSalary * 0.10; } }
Java uses a structured exception model. Checked exceptions must be handled at compile time; unchecked (RuntimeExceptions) are caught at runtime.
import java.io.IOException; // ── CUSTOM EXCEPTION ───────────────────────────────── public class InsufficientFundsException extends RuntimeException { private final double shortfall; public InsufficientFundsException(double shortfall) { super(String.format("Short by RWF %.2f", shortfall)); this.shortfall = shortfall; } public double getShortfall() { return shortfall; } } // ── EXCEPTION HANDLING PATTERNS ────────────────────── public class ExceptionDemo { public static int parseAge(String input) { try { int age = Integer.parseInt(input); if (age < 0) throw new IllegalArgumentException("Age cannot be negative"); return age; } catch (NumberFormatException e) { System.err.println("Not a number: " + input); return -1; } finally { System.out.println("parseAge() completed"); // always runs } } // Multi-catch (Java 7+) public static void processFile(String path) { try { // read file operations... } catch (IOException | SecurityException e) { System.err.println("File error: " + e.getMessage()); } } // try-with-resources — auto closes Closeable resources public static void readFile(String path) { try (var reader = new java.io.BufferedReader( new java.io.FileReader(path))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.err.println("Cannot read file: " + e.getMessage()); } } }
The Java Collections Framework provides dynamic data structures far more powerful than arrays. Key interfaces: List, Set, Map.
import java.util.*; public class CollectionsDemo { public static void main(String[] args) { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ARRAYLIST — ordered, resizable, allows duplicates // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ List<String> courses = new ArrayList<>(); courses.add("Java"); courses.add("Spring"); courses.add("SQL"); courses.add(1, "OOP"); // insert at index courses.remove("SQL"); // remove by value System.out.println(courses.get(0)); // Java System.out.println(courses.size()); // 3 Collections.sort(courses); courses.forEach(System.out::println); // method reference // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // HASHMAP — key→value pairs, O(1) lookup // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 95); scores.put("Bob", 82); scores.put("Claire", 88); System.out.println(scores.get("Alice")); // 95 System.out.println(scores.getOrDefault("Dave", 0)); // 0 System.out.println(scores.containsKey("Bob")); // true for (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.printf("%s → %d%n", entry.getKey(), entry.getValue()); } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // HASHSET — unique elements, no guaranteed order // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Set<String> tags = new HashSet<>(); tags.add("java"); tags.add("spring"); tags.add("java"); System.out.println(tags.size()); // 2 — no duplicates System.out.println(tags.contains("spring")); // true } }
import java.io.*; import java.nio.file.*; import java.util.List; public class FileIO { public static void main(String[] args) throws IOException { Path file = Path.of("students.txt"); // ── WRITE ──────────────────────────────────────────── List<String> lines = List.of("Alice,A", "Bob,C", "Claire,A"); Files.write(file, lines); // ── READ ALL LINES ──────────────────────────────────── List<String> read = Files.readAllLines(file); read.forEach(System.out::println); // ── APPEND to existing file ─────────────────────────── Files.writeString(file, "\nDavid,B", StandardOpenOption.APPEND); // ── BufferedWriter — efficient for many writes ──────── try (BufferedWriter bw = Files.newBufferedWriter( Path.of("report.txt"))) { bw.write("=== Student Report ==="); bw.newLine(); bw.write("Total: 4 students"); } // ── Check if file exists ────────────────────────────── if (Files.exists(file)) { System.out.println("Size: " + Files.size(file) + " bytes"); } } }
Streams (Java 8+) enable functional-style data processing on collections — filter, transform, and aggregate data in a declarative, readable way.
import java.util.*; import java.util.stream.*; public class StreamsDemo { public static void main(String[] args) { List<String> names = List.of("Alice", "Bob", "Claire", "Anna", "Brian"); // Filter + map + collect List<String> aNames = names.stream() .filter(n -> n.startsWith("A")) // keep A-names .map(String::toUpperCase) // transform .sorted() // alphabetical .collect(Collectors.toList()); System.out.println(aNames); // [ALICE, ANNA] // Statistics on a list of scores List<Integer> scores = List.of(85, 92, 78, 96, 71); OptionalDouble avg = scores.stream() .mapToInt(Integer::intValue) .average(); avg.ifPresent(a -> System.out.printf("Avg: %.1f%n", a)); int topScore = scores.stream() .mapToInt(Integer::intValue) .max().orElse(0); System.out.println("Top: " + topScore); // 96 long passing = scores.stream() .filter(s -> s >= 80) .count(); System.out.println("Passing: " + passing); // 3 // Grouping with Collectors.groupingBy Map<Boolean, List<Integer>> grouped = scores.stream() .collect(Collectors.partitioningBy(s -> s >= 80)); System.out.println("Pass: " + grouped.get(true)); System.out.println("Fail: " + grouped.get(false)); } }
GET (read) · POST (create) · PUT (update) · DELETE (remove) — the four verbs of REST
Opinionated framework that auto-configures Spring. Start in minutes with embedded Tomcat — no XML config.
Model (data) · View (response) · Controller (routing). Spring Boot REST uses Model + Controller only.
Spring manages object creation and wiring. You declare what you need; Spring provides it via @Autowired or constructors.
- 1Generate project: Go to start.spring.io → Select Maven, Java 17, add dependencies: Spring Web, Spring Data JPA, MySQL Driver.
- 2Download & open: Extract the zip, open in IntelliJ IDEA — it auto-detects Maven and downloads dependencies.
- 3Configure application.properties: Set DB URL, username, and password for your environment.
- 4Run: Click the Run button or run
mvn spring-boot:run. Access athttp://localhost:8080.
# Server server.port=8080 # Database (MySQL) spring.datasource.url=jdbc:mysql://localhost:3306/school_db spring.datasource.username=root spring.datasource.password=yourpassword spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA / Hibernate spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
<dependencies>
<!-- Spring Web (MVC + embedded Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA (Hibernate ORM) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok — reduces boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
package com.school.controller; import com.school.model.Student; import com.school.service.StudentService; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController // @Controller + @ResponseBody: returns JSON @RequestMapping("/api/students") // base path for all routes public class StudentController { private final StudentService studentService; // Constructor injection (preferred over @Autowired on field) public StudentController(StudentService studentService) { this.studentService = studentService; } // ── GET /api/students — list all ──────────────────── @GetMapping public ResponseEntity<List<Student>> getAllStudents() { return ResponseEntity.ok(studentService.findAll()); } // ── GET /api/students/1 — get one ─────────────────── @GetMapping("/{id}") public ResponseEntity<Student> getStudent(@PathVariable Long id) { return studentService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } // ── POST /api/students — create ───────────────────── @PostMapping public ResponseEntity<Student> createStudent( @RequestBody Student student) { Student saved = studentService.save(student); return ResponseEntity .status(HttpStatus.CREATED) // 201 .body(saved); } // ── PUT /api/students/1 — update ──────────────────── @PutMapping("/{id}") public ResponseEntity<Student> updateStudent( @PathVariable Long id, @RequestBody Student updates) { return studentService.update(id, updates) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } // ── DELETE /api/students/1 ────────────────────────── @DeleteMapping("/{id}") public ResponseEntity<Void> deleteStudent(@PathVariable Long id) { studentService.delete(id); return ResponseEntity.noContent().build(); // 204 } // ── GET /api/students/search?name=alice ───────────── @GetMapping("/search") public ResponseEntity<List<Student>> search( @RequestParam("name") String name) { return ResponseEntity.ok(studentService.findByName(name)); } }
package com.school.service; import com.school.model.Student; import com.school.repository.StudentRepository; import org.springframework.stereotype.Service; import java.util.*; @Service // registers as a Spring bean (injectable) public class StudentService { private final StudentRepository repo; // Spring injects StudentRepository automatically public StudentService(StudentRepository repo) { this.repo = repo; } public List<Student> findAll() { return repo.findAll(); } public Optional<Student> findById(Long id) { return repo.findById(id); } public Student save(Student s) { return repo.save(s); } public void delete(Long id) { repo.deleteById(id); } public List<Student> findByName(String name) { return repo.findByNameContainingIgnoreCase(name); } public Optional<Student> update(Long id, Student updates) { return repo.findById(id).map(existing -> { existing.setName(updates.getName()); existing.setEmail(updates.getEmail()); existing.setGrade(updates.getGrade()); return repo.save(existing); }); } }
JDBC is the low-level API for connecting Java to any SQL database. It provides raw control — useful to understand before using higher-level ORMs.
import java.sql.*; public class JdbcConnection { private static final String URL = "jdbc:mysql://localhost:3306/school_db"; private static final String USER = "root"; private static final String PASS = "yourpassword"; // Reusable method to get a connection public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASS); } public static void main(String[] args) { try (Connection conn = getConnection()) { System.out.println("Connected: " + conn.getMetaData().getDatabaseProductName()); // Create table if not exists String createSQL = """ CREATE TABLE IF NOT EXISTS students ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(150) UNIQUE NOT NULL, grade VARCHAR(5) DEFAULT 'N/A' )"""; conn.createStatement().execute(createSQL); System.out.println("Table ready."); } catch (SQLException e) { System.err.println("DB Error: " + e.getMessage()); } } }
import java.sql.*; import java.util.*; public class StudentDAO { // ── CREATE (INSERT) ─────────────────────────────────── public void insert(String name, String email, String grade) { String sql = "INSERT INTO students (name, email, grade) VALUES (?, ?, ?)"; try (Connection conn = JdbcConnection.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, name); ps.setString(2, email); ps.setString(3, grade); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) System.out.println("New ID: " + keys.getLong(1)); } catch (SQLException e) { e.printStackTrace(); } } // ── READ (SELECT ALL) ───────────────────────────────── public List<String> findAll() { List<String> students = new ArrayList<>(); String sql = "SELECT id, name, email, grade FROM students ORDER BY name"; try (Connection conn = JdbcConnection.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { students.add(String.format("%d | %-15s | %s | %s", rs.getLong("id"), rs.getString("name"), rs.getString("email"), rs.getString("grade"))); } } catch (SQLException e) { e.printStackTrace(); } return students; } // ── UPDATE ──────────────────────────────────────────── public int updateGrade(long id, String newGrade) { String sql = "UPDATE students SET grade = ? WHERE id = ?"; try (Connection conn = JdbcConnection.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, newGrade); ps.setLong(2, id); return ps.executeUpdate(); // returns rows affected } catch (SQLException e) { e.printStackTrace(); return 0; } } // ── DELETE ──────────────────────────────────────────── public int delete(long id) { String sql = "DELETE FROM students WHERE id = ?"; try (Connection conn = JdbcConnection.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, id); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }
// ❌ DANGEROUS — never concatenate user input into SQL String userInput = "' OR '1'='1"; // attacker input String sql = "SELECT * FROM students WHERE name = '" + userInput + "'"; // Becomes: SELECT * FROM students WHERE name = '' OR '1'='1' // Returns ALL rows — massive security breach! // ✅ SAFE — PreparedStatement with placeholders String safeSql = "SELECT * FROM students WHERE name = ?"; try (PreparedStatement ps = conn.prepareStatement(safeSql)) { ps.setString(1, userInput); // JDBC escapes this safely ResultSet rs = ps.executeQuery(); // Returns 0 rows — attack failed ✅ } // ✅ SAFE — JPA/Spring Data (always parameterised) // studentRepository.findByName(userInput); // no injection possible // ── BATCH OPERATIONS for performance ───────────────── String insertSql = "INSERT INTO students (name, email) VALUES (?, ?)"; try (PreparedStatement ps = conn.prepareStatement(insertSql)) { conn.setAutoCommit(false); // manual transaction for (String[] student : studentData) { ps.setString(1, student[0]); ps.setString(2, student[1]); ps.addBatch(); // buffer the statement } ps.executeBatch(); // send all at once conn.commit(); // commit transaction }
? placeholders, or use Spring Data JPA / Hibernate which parameterise automatically.
JPA (Java Persistence API) with Hibernate lets you map Java classes to database tables using annotations — no SQL required for standard CRUD.
package com.school.model; import jakarta.persistence.*; import lombok.*; @Entity // marks this as a JPA entity (DB table) @Table(name = "students") // maps to the "students" table @Data // Lombok: generates getters/setters/toString @NoArgsConstructor // Lombok: generates no-arg constructor @AllArgsConstructor // Lombok: generates all-args constructor public class Student { @Id // primary key @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT private Long id; @Column(nullable = false, length = 100) private String name; @Column(nullable = false, unique = true, length = 150) private String email; @Column(length = 5, columnDefinition = "VARCHAR(5) DEFAULT 'N/A'") private String grade = "N/A"; @Column(name = "created_at", updatable = false) @CreationTimestamp private java.time.LocalDateTime createdAt; }
package com.school.repository; import com.school.model.Student; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface StudentRepository extends JpaRepository<Student, Long> { // Spring Data generates SQL from method names! List<Student> findByGrade(String grade); List<Student> findByNameContainingIgnoreCase(String name); boolean existsByEmail(String email); // Custom JPQL query @Query("SELECT s FROM Student s WHERE s.grade IN :grades ORDER BY s.name") List<Student> findTopStudents(@Param("grades") List<String> grades); // Custom native SQL @Query(value = "SELECT COUNT(*) FROM students WHERE grade = ?1", nativeQuery = true) long countByGrade(String grade); } // JpaRepository provides: findAll, findById, save, deleteById, // count, existsById — all for FREE, no implementation needed!
student-management/ ├── src/main/ │ ├── java/com/school/ │ │ ├── StudentManagementApplication.java // @SpringBootApplication │ │ ├── model/ │ │ │ ├── Student.java // @Entity │ │ │ └── ApiResponse.java // standard JSON wrapper │ │ ├── repository/ │ │ │ └── StudentRepository.java // JpaRepository │ │ ├── service/ │ │ │ ├── StudentService.java // interface │ │ │ └── StudentServiceImpl.java // @Service implementation │ │ ├── controller/ │ │ │ └── StudentController.java // @RestController │ │ └── exception/ │ │ ├── ResourceNotFoundException.java │ │ └── GlobalExceptionHandler.java // @ControllerAdvice │ └── resources/ │ └── application.properties └── pom.xml
package com.school.model; import lombok.*; import java.time.LocalDateTime; @Data @AllArgsConstructor @NoArgsConstructor public class ApiResponse<T> { private boolean success; private String message; private T data; private LocalDateTime timestamp = LocalDateTime.now(); public static <T> ApiResponse<T> success(String msg, T data) { return new ApiResponse<>(true, msg, data, LocalDateTime.now()); } public static <T> ApiResponse<T> error(String msg) { return new ApiResponse<>(false, msg, null, LocalDateTime.now()); } }
package com.school.exception; import com.school.model.ApiResponse; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; @ControllerAdvice // handles exceptions across ALL controllers public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ApiResponse<?>> handleNotFound( ResourceNotFoundException ex) { return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(ApiResponse.error(ex.getMessage())); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<ApiResponse<?>> handleBadRequest( IllegalArgumentException ex) { return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error(ex.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<ApiResponse<?>> handleAll(Exception ex) { return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("An unexpected error occurred")); } }
package com.school.controller; import com.school.model.*; import com.school.service.StudentService; import jakarta.validation.Valid; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/v1/students") public class StudentController { private final StudentService service; public StudentController(StudentService service) { this.service = service; } @GetMapping public ResponseEntity<ApiResponse<List<Student>>> getAll() { List<Student> students = service.findAll(); return ResponseEntity.ok( ApiResponse.success("Found " + students.size() + " students", students) ); } @GetMapping("/{id}") public ResponseEntity<ApiResponse<Student>> getById(@PathVariable Long id) { Student student = service.findByIdOrThrow(id); return ResponseEntity.ok(ApiResponse.success("Student found", student)); } @PostMapping public ResponseEntity<ApiResponse<Student>> create( @Valid @RequestBody Student student) { Student saved = service.create(student); return ResponseEntity .status(HttpStatus.CREATED) .body(ApiResponse.success("Student created", saved)); } @PutMapping("/{id}") public ResponseEntity<ApiResponse<Student>> update( @PathVariable Long id, @Valid @RequestBody Student student) { Student updated = service.update(id, student); return ResponseEntity.ok(ApiResponse.success("Student updated", updated)); } @DeleteMapping("/{id}") public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) { service.delete(id); return ResponseEntity.ok(ApiResponse.success("Student deleted", null)); } } /* Sample JSON responses: GET /api/v1/students/1 { "success": true, "message": "Student found", "data": { "id": 1, "name": "Alice", "email": "alice@school.rw", "grade": "A" }, "timestamp": "2024-03-15T10:22:05" } POST /api/v1/students body: {"name":"Bob","email":"bob@school.rw","grade":"B"} → 201 Created */
mvn spring-boot:run → open http://localhost:8080/api/v1/students in Postman or any REST client. The database tables are created automatically by Hibernate on first run.
http://localhost:8080/api/v1/students. Use GET (list/single), POST with JSON body (create), PUT with ID + body (update), DELETE with ID. All responses follow the standardised ApiResponse wrapper.