Unit 3: File I/O and JDBC

CSE406 — Advanced Java Programming 10 min read

I. Orientation — Persistent Data in Java

File I/O and JDBC are Java mechanisms for working with persistent data. Java NIO.2, introduced in Java 7, provides path-based file-system operations through java.nio.file, while JDBC (Java Database Connectivity) provides a standard API for communicating with relational databases.

  • Defining properties:
    • Persistence: Files and databases retain data after a Java program terminates.
    • Abstraction: Path abstracts a file-system location; JDBC interfaces abstract vendor-specific database communication.
    • Resource management: Streams, database connections, statements, and result sets must be closed, normally through try-with-resources.
    • Exception handling: NIO.2 operations commonly throw IOException; JDBC operations throw SQLException.
    • Separation of concerns: Application code uses standard Java APIs, while the operating system or JDBC driver performs platform-specific work.
    • Security: File permissions, database credentials, prepared statements, and least-privilege access protect persistent data.

II. Java NIO.2 File and Directory Operations

NIO.2 represents locations as Path objects and performs operations through the utility class Files, providing more flexible and scalable file handling than the older java.io.File API.

A. Using the Path interface to operate on file and directory paths

The Path interface represents a hierarchical file-system path without requiring the referenced file or directory to exist.

  • Creating paths: Path.of() creates a Path from one or more strings; the older equivalent is Paths.get().
JAVA
Path report = Path.of("data", "reports", "sales.txt");
Path absolute = Path.of("/var/log/app.log");
  • Path components:

    • Root: report.getRoot() returns the root component, if present.
    • File name: report.getFileName() returns sales.txt.
    • Parent: report.getParent() returns data/reports.
    • Name count: report.getNameCount() counts elements excluding the root.
    • Subpath: report.subpath(0, 2) returns the first two name elements.
  • Relative and absolute paths:

    1. A relative path, such as data/input.txt, is interpreted from the current working directory.
    2. An absolute path, such as /home/user/input.txt, identifies a location from the file-system root.
JAVA
Path normalized = Path.of("data", ".", "temp", "..", "input.txt")
                      .normalize();
Path fullPath = normalized.toAbsolutePath();
  • Combining paths: base.resolve("file.txt") appends a path, while base.relativize(target) calculates the path from base to target.
  • Comparison: startsWith(), endsWith(), and compareTo() compare path structures; Files.isSameFile() determines whether two paths locate the same existing file.
  • Important distinction: Path is primarily a path representation. File-system access occurs when methods such as toRealPath() or operations in Files are called.
  • Provider dependence: Separators and root conventions depend on the file system, so Path.of("data", "file.txt") is preferable to manually inserting / or \.

B. Using the Files class to check, delete, copy, or move a file or directory

The Files class contains static methods for inspecting and modifying files and directories identified by Path.

  • Checking existence and type:
    • Files.exists(path) checks whether an entry exists.
    • Files.notExists(path) checks confirmed nonexistence.
    • Files.isRegularFile(path) and Files.isDirectory(path) inspect entry type.
    • Files.isReadable(path), isWritable(path), and isExecutable(path) inspect accessibility.
JAVA
Path source = Path.of("data", "input.txt");

if (Files.exists(source) && Files.isRegularFile(source)) {
    System.out.println(Files.size(source));
}
  • Creating entries: createFile() creates a new empty file; createDirectory() creates one directory; createDirectories() creates missing parent directories.

  • Deleting entries:

    1. Files.delete(path) deletes an entry or throws an exception if deletion fails.
    2. Files.deleteIfExists(path) returns false if the entry is absent.

    A directory generally must be empty before deletion.

  • Copying: Files.copy(source, target, options) copies a file or directory entry.

JAVA
Files.copy(
    source,
    Path.of("backup", "input.txt"),
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.COPY_ATTRIBUTES
);
  • Moving or renaming: Files.move() changes an entry’s location; moving within one file system can serve as a rename.
JAVA
Files.move(
    Path.of("draft.txt"),
    Path.of("final.txt"),
    StandardCopyOption.REPLACE_EXISTING
);
  • Operation options: REPLACE_EXISTING permits overwriting, COPY_ATTRIBUTES preserves supported metadata during copying, and ATOMIC_MOVE requests an indivisible move when supported.
  • Failure conditions: Permissions, nonempty directories, existing targets, locks, or unsupported options may produce subclasses of IOException.

C. Using Stream API with NIO2

NIO.2 methods can expose file-system data as streams, allowing lazy filtering, mapping, and aggregation through the Stream API.

  • Directory listing: Files.list(directory) produces a nonrecursive Stream<Path> containing immediate entries.
  • Recursive traversal: Files.walk(start) performs depth-first traversal, while Files.find() combines traversal with a predicate.
  • Line processing: Files.lines(file) lazily produces a Stream<String> using UTF-8 by default or a specified Charset.
JAVA
Path logs = Path.of("logs");

try (Stream<Path> paths = Files.walk(logs)) {
    List<Path> errorLogs = paths
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".log"))
        .toList();
}
  • Resource closure: Streams returned by Files.list(), walk(), find(), and lines() hold file-system resources and therefore belong in try-with-resources.
  • Lazy execution: Intermediate operations such as filter() do not run until a terminal operation such as count(), forEach(), or toList() is invoked.
  • Traversal control: Files.walk(start, maxDepth) limits recursion; FileVisitOption.FOLLOW_LINKS follows symbolic links but can expose cycles.
  • Error handling: Some traversal failures appear as UncheckedIOException during stream processing because stream functional interfaces do not naturally propagate checked IOException.
  • Suitability: Stream-based traversal is concise for selection and aggregation; Files.walkFileTree() is preferable when fine-grained visit control or explicit failure handling is required.

III. JDBC Architecture and Database Connectivity

JDBC, primarily defined in java.sql and javax.sql, standardizes database access while delegating database-specific network protocols and SQL transmission to a JDBC driver.

A. Defining the layout of the JDBC API

The JDBC API is organized around interfaces representing a connection, SQL command, returned rows, metadata, and transaction boundaries.

  • Core components:

    • DriverManager: Selects a registered driver and requests a connection.
    • Connection: Represents a database session and controls transactions.
    • Statement: Executes fixed SQL strings.
    • PreparedStatement: Executes parameterized, precompiled SQL.
    • CallableStatement: Invokes stored procedures.
    • ResultSet: Provides cursor-based access to query rows.
    • SQLException: Reports database-access errors, SQL states, and vendor codes.
  • Typical execution flow:

TEXT
Application
    -> JDBC API
    -> JDBC Driver
    -> Database
    -> ResultSet or update count
  • Data-source alternative: javax.sql.DataSource supplies connections and supports connection pooling and centralized configuration, making it preferable in managed or production applications.
  • JDBC types: java.sql.Types identifies SQL types; methods such as setInt(), setString(), getDate(), and getBigDecimal() map Java values to database values.
  • Transactions: A Connection begins in auto-commit mode by default. Calling setAutoCommit(false) permits explicit commit() or rollback().

B. Connecting to a database by using a JDBC driver

A JDBC driver implements the standard interfaces and translates JDBC operations into the database vendor’s protocol.

  • Required information:
    • Driver dependency: The vendor’s JDBC driver JAR must be on the classpath or module path.
    • JDBC URL: A URL begins with jdbc: and contains vendor-specific connection details.
    • Credentials: A database username and password identify and authenticate the client.
JAVA
String url = "jdbc:mysql://localhost:3306/college";

try (Connection connection =
         DriverManager.getConnection(url, "app_user", "secret")) {
    System.out.println(connection.isValid(2));
}
  • Automatic registration: JDBC 4.0-compatible drivers are discovered automatically when their JAR is available. Explicit calls such as Class.forName("com.mysql.cj.jdbc.Driver") are mainly needed for older drivers or unusual loading environments.
  • Connection lifecycle: Connections are expensive resources; production systems generally obtain them from a pool rather than opening a new physical connection for every operation.
  • Failure diagnosis: SQLException exposes getMessage(), getSQLState(), and getErrorCode() for identifying authentication, network, syntax, or constraint failures.

C. Specifying JDBC driver information externally

External configuration separates deployment-specific connection data from compiled application code.

  • Properties file: Values can be stored in a file such as database.properties.
PROPERTIES
db.url=jdbc:postgresql://localhost:5432/college
db.user=app_user
db.password=secret
  • Loading configuration:
JAVA
Properties config = new Properties();

try (InputStream input =
         Files.newInputStream(Path.of("database.properties"))) {
    config.load(input);
}

Connection connection = DriverManager.getConnection(
    config.getProperty("db.url"),
    config.getProperty("db.user"),
    config.getProperty("db.password")
);
  • Deployment alternatives: Environment variables, command-line properties, JNDI, secret managers, and framework configuration systems can provide the same information.
  • Benefits: Externalization allows development, testing, and production environments to use different URLs and credentials without recompilation.
  • Security limitation: Plain-text passwords should not be committed to source control. Restrictive permissions or a dedicated secret-management service should protect credentials.

IV. Executing SQL and Processing Results

JDBC distinguishes SQL that returns tabular data from SQL that changes database state, and each form has a corresponding execution method.

A. Submitting queries and getting results from the database

A query is submitted through a statement object, and its returned rows are read sequentially from a ResultSet.

  • Query execution: executeQuery() is intended for SQL such as SELECT that produces a ResultSet.
  • Parameterized SQL: Placeholders marked by ? are assigned through one-based parameter indexes.
JAVA
String sql =
    "SELECT id, name, marks FROM student WHERE marks >= ?";

try (PreparedStatement statement =
         connection.prepareStatement(sql)) {
    statement.setInt(1, 60);

    try (ResultSet results = statement.executeQuery()) {
        while (results.next()) {
            int id = results.getInt("id");
            String name = results.getString("name");
            int marks = results.getInt("marks");
            System.out.println(id + " " + name + " " + marks);
        }
    }
}
  • Cursor behavior: A new ResultSet cursor starts before the first row; next() advances it and returns false after the final row.
  • Column access: Values may be retrieved by column label or one-based position. Labels are clearer and less sensitive to changes in column order.
  • SQL NULL: Primitive getters may return default values, such as 0; wasNull() checks whether the last retrieved database value was NULL.
  • Security: PreparedStatement binds values separately from SQL syntax, preventing user input from changing query structure and reducing SQL-injection risk.
  • Metadata: ResultSetMetaData describes returned columns, while DatabaseMetaData describes database features, tables, and supported operations.

V. JDBC Data Modification

JDBC supports the complete CRUD model: creating, reading, updating, and deleting persistent database records.

A. Performing CRUD operations using the JDBC API

CRUD operations use parameterized SQL and appropriate execution methods to manipulate database rows reliably.

  • Create: INSERT adds a row and executeUpdate() returns the number of affected rows.
JAVA
String sql = "INSERT INTO student(name, marks) VALUES (?, ?)";

try (PreparedStatement ps =
         connection.prepareStatement(sql)) {
    ps.setString(1, "Anita");
    ps.setInt(2, 84);
    int inserted = ps.executeUpdate();
}
  • Read: SELECT uses executeQuery() and processes its ResultSet, as shown in the preceding section.
  • Update: UPDATE changes matching rows; a restrictive WHERE clause prevents unintended modifications.
JAVA
PreparedStatement ps = connection.prepareStatement(
    "UPDATE student SET marks = ? WHERE id = ?"
);
ps.setInt(1, 91);
ps.setInt(2, 12);
int updated = ps.executeUpdate();
  • Delete: DELETE removes matching rows and also returns an update count.
JAVA
PreparedStatement ps = connection.prepareStatement(
    "DELETE FROM student WHERE id = ?"
);
ps.setInt(1, 12);
int deleted = ps.executeUpdate();
  • Transaction control: Related changes should succeed or fail together.
JAVA
connection.setAutoCommit(false);

try {
    // Execute related INSERT, UPDATE, or DELETE operations.
    connection.commit();
} catch (SQLException exception) {
    connection.rollback();
    throw exception;
}
  • Generated keys: Passing Statement.RETURN_GENERATED_KEYS when preparing an INSERT allows retrieval of database-generated identifiers.
  • Batch operations: Repeated commands can use addBatch() and executeBatch() to reduce database round trips.
  • Integrity checks: Applications should inspect update counts, handle constraint violations, roll back failed transactions, and close Connection, Statement, and ResultSet objects through try-with-resources.