Object-Oriented Programming (Java)

Chapter 9
Input, Output, Persistence, and Data Access

Bachelors Program in IT
Object-Oriented Programming (Java)   •   Bachelor

Input, Output, Persistence, and Data Access

Introduction

Most useful software must remember information after a program terminates. A student registration system stores learners and courses; an inventory application records products and stock movements; a banking system preserves accounts and transactions. In an object-oriented Java application, persistence is the process of storing object state so that it can be reconstructed later.

Persistence introduces an important boundary. Objects live in memory and are governed by Java types, references, inheritance, and encapsulation. Files and databases store durable representations governed by formats, schemas, permissions, concurrency rules, and failure-recovery mechanisms. Good software therefore treats data access as a deliberate architectural concern rather than scattering file or SQL operations throughout business classes.

This chapter develops a practical progression:

  1. byte and character input/output;
  2. modern file operations using Java NIO;
  3. object serialization and its security risks;
  4. structured data using JSON;
  5. relational databases and JDBC;
  6. transactions and failure handling;
  7. repositories and data-access abstractions; and
  8. ORM concepts and their trade-offs.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish streams, readers, writers, files, and paths;
  • use Java NIO safely for common file operations;
  • explain why native Java serialization requires caution;
  • select suitable representations for persistent data;
  • connect to a relational database using JDBC;
  • use prepared statements and transactions correctly;
  • design repository interfaces that separate domain logic from storage;
  • explain identity mapping, relationships, lazy loading, and the N+1 query problem in ORM systems; and
  • evaluate persistence designs in terms of reliability, security, maintainability, and performance.

1. The Persistence Boundary

An in-memory object has identity, behavior, and references. A persistent representation normally has data, an external identifier, and relationships represented explicitly. For example, a Java object may contain a reference from an Order to a Customer, whereas a relational database may store customer_id in an orders table.

A persistence boundary should answer four questions:

  • What is stored: entities, events, documents, files, or configuration?
  • Where is it stored: a local file, network file system, relational database, or cloud service?
  • How is it represented: binary, text, JSON, XML, or relational rows?
  • What guarantees are required: durability, consistency, concurrent access, recovery, and auditability?

The choice depends on the data. A small application configuration may appropriately use JSON. A large set of related records requiring concurrent updates generally requires a database. A report exported for another system may use CSV, while an image requires binary I/O.

Persistence also affects object design. A class intended for durable storage usually needs a stable identity and a clearly defined representation. Internal fields such as cached calculations, passwords, or open network connections should not automatically be persisted.

2. Java Input and Output Abstractions

Java I/O is organized around streams. An input stream reads data into a program, and an output stream writes data from a program. Byte streams (InputStream and OutputStream) are appropriate for binary data. Character streams (Reader and Writer) are appropriate for text and perform character decoding or encoding.

The distinction matters. A byte is not necessarily a character, and text encoded as UTF-8 must not be interpreted using an unrelated encoding. Explicitly specifying a character set prevents platform-dependent behavior.

try (BufferedReader reader = Files.newBufferedReader(
        path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

The try-with-resources statement closes the reader automatically, even if an exception occurs. This is essential because files, sockets, and database connections are operating-system resources rather than ordinary memory objects.

Buffering reduces the number of expensive operating-system calls. A buffered stream does not change the meaning of data; it improves access efficiency. It should not, however, be confused with durability. Data written to a buffer may not yet have reached the storage device.

Important exceptions include:

  • IOException, for general input/output failures;
  • NoSuchFileException, when a required file is absent;
  • AccessDeniedException, when permissions prevent access; and
  • MalformedInputException, when bytes cannot be decoded using the selected character set.

Robust programs handle these failures explicitly. Catching Exception broadly and ignoring it is poor practice because it hides data loss and operational problems.

3. Java NIO and the Path-Centered Model

The java.nio.file package, commonly called NIO.2, provides the modern file API. Path represents a file-system location, while Files provides operations on that location.

Path report = Path.of("data", "reports", "summary.txt");

Files.createDirectories(report.getParent());
Files.writeString(
        report,
        "Completed\n",
        StandardCharsets.UTF_8,
        StandardOpenOption.CREATE,
        StandardOpenOption.APPEND);

Path objects are not necessarily existing files. They are descriptions of locations. Therefore, operations can fail if a path is invalid, inaccessible, or refers to an unexpected object.

Common operations include:

OperationTypical methodImportant consideration
Test existenceFiles.existsExistence may change immediately afterward
Read textFiles.readStringSuitable for reasonably sized files
Write textFiles.writeStringSelect creation and replacement behavior
CopyFiles.copyDecide whether replacement is allowed
Move or renameFiles.moveCross-file-system moves may differ
DeleteFiles.deleteFails if the target does not exist
List directoryFiles.listClose the returned stream

A time-of-check/time-of-use problem occurs when code checks a file and then acts on the assumption that the file has not changed. For example, exists(path) followed by readString(path) is not an atomic operation. In security-sensitive code, prefer an operation that directly attempts the required action and handles failure.

File paths supplied by users also create path traversal risks. A request such as ../../secrets.txt may escape an intended directory. Applications should validate input, resolve it against an approved base directory, and verify that the normalized result remains within that directory. File permissions should follow least privilege.

For large files, avoid loading all content into memory. Use buffered streams, Files.lines, or a channel-based design, and close every resource. When replacing an important file, a safer pattern is to write a temporary file, flush and close it, and then atomically move it where the file system supports atomic replacement.

4. Native Serialization: Convenience with Risks

Java native serialization converts an object graph into a byte sequence and reconstructs it later. A class implements Serializable to indicate that instances may be serialized.

try (ObjectOutputStream out = new ObjectOutputStream(
        Files.newOutputStream(Path.of("user.bin")))) {
    out.writeObject(user);
}

Native serialization can preserve references and object graphs conveniently, but it has serious limitations. Serialized data is tied to class structure and serialVersionUID; class evolution can cause compatibility failures. More importantly, deserialization processes data that may be supplied by an untrusted source. Gadget chains in a classpath can cause unintended behavior during deserialization.

Therefore:

  • do not deserialize untrusted bytes by default;
  • prefer JSON, a database, or a deliberately designed binary format for interchange;
  • use allow-list filters where native serialization is unavoidable;
  • never treat serialization as encryption; and
  • do not serialize secrets or live resources such as sockets and database connections.

transient prevents a field from being included in default serialization, but it is not a general security mechanism. Sensitive data should be excluded by design, and confidential data should use appropriate encryption and key management.

5. JSON and Data Transfer Objects

JSON is a text format consisting of objects, arrays, strings, numbers, Boolean values, and null. It is widely used for configuration, REST APIs, and durable documents. JSON is generally more interoperable than Java native serialization, but it does not automatically solve validation, compatibility, or security problems.

A useful design separates domain objects from data-transfer objects (DTOs). A DTO describes the external representation, while a domain object protects business invariants. For example, an API may accept CreateUserRequest containing a name and email, then construct a User only after validating those values.

A JSON library such as Jackson or Gson can map Java objects to JSON. A conceptual Jackson example is:

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
UserDto dto = mapper.readValue(json, UserDto.class);

External data must be treated as untrusted. Validate required fields, lengths, ranges, formats, and allowed enumerations. Avoid enabling polymorphic deserialization from arbitrary type names unless a strict allow-list is used. Consider schema versioning: adding an optional field is often compatible, whereas renaming or changing a field's meaning may break existing consumers.

JSON is not always ideal. It may be verbose for very large datasets, lacks database transactions, and can represent relationships awkwardly. It is best viewed as a representation format, not as a complete persistence system.

6. Relational Databases and JDBC

A relational database stores data in tables consisting of rows and columns. A primary key identifies a row; a foreign key refers to a key in another table. Constraints such as NOT NULL, UNIQUE, CHECK, and foreign-key rules enforce important invariants close to the data.

Suppose an order schema contains:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL,
    total DECIMAL(12, 2) NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

DECIMAL(12, 2) represents a decimal value with up to 12 total digits, including 2 digits after the decimal point. Monetary calculations should generally use BigDecimal in Java rather than binary floating-point types.

JDBC is Java's standard API for relational database access. The normal sequence is:

  1. obtain a Connection;
  2. create a PreparedStatement;
  3. bind parameters;
  4. execute the statement;
  5. process the result; and
  6. close resources.
String sql = "SELECT id, status, total FROM orders WHERE customer_id = ?";

try (Connection connection = dataSource.getConnection();
     PreparedStatement statement = connection.prepareStatement(sql)) {

    statement.setLong(1, customerId);
    try (ResultSet results = statement.executeQuery()) {
        while (results.next()) {
            long id = results.getLong("id");
            String status = results.getString("status");
            BigDecimal total = results.getBigDecimal("total");
            // Map the row to a domain object or DTO.
        }
    }
}

Prepared statements separate SQL structure from values and protect against SQL injection. String concatenation such as "... WHERE name = '" + input + "'" is unsafe and should not be used.

A ResultSet is cursor-like. Its getters must use the correct column type, and nullable columns require attention because primitive Java values cannot directly distinguish SQL NULL from zero or false. JDBC also supports batch operations, generated keys, metadata, and stored procedures, but these features should be used only when they improve the design.

7. Transactions and Consistency

A transaction groups related database operations into one logical unit. The traditional ACID properties are:

  • Atomicity: all operations commit, or the transaction is rolled back;
  • Consistency: constraints and application invariants remain satisfied;
  • Isolation: concurrent transactions observe behavior controlled by the database's isolation rules; and
  • Durability: committed data survives ordinary failures according to the database's guarantees.

Consider transferring money. The debit and credit must not be committed independently. A JDBC implementation may be structured as follows:

try (Connection connection = dataSource.getConnection()) {
    connection.setAutoCommit(false);
    try {
        debit(connection, fromAccount, amount);
        credit(connection, toAccount, amount);
        connection.commit();
    } catch (SQLException failure) {
        connection.rollback();
        throw failure;
    } finally {
        connection.setAutoCommit(true);
    }
}

The example assumes that debit and credit use the same connection and that database constraints and business checks are correctly implemented. If a failure occurs after the debit but before the credit, rollback restores the transaction's prior state.

Isolation levels address anomalies such as dirty reads, non-repeatable reads, and phantom reads. Higher isolation can reduce anomalies but may increase locking, waiting, or serialization failures. Applications should select an isolation level based on business requirements rather than assuming that the strongest level is always best.

Transactions should be short. Holding a connection while performing network calls or user interaction increases contention. A transaction is not a substitute for validation, and a successful commit does not guarantee that every external side effect—such as an email—has occurred. Reliable integration may require an outbox pattern, in which an event is stored in the same transaction and published separately.

8. Repository Design

A repository provides a collection-like interface to persistent domain objects. It hides SQL, file formats, and connection management from application services.

public interface OrderRepository {
    Optional<Order> findById(long id);
    List<Order> findByCustomerId(long customerId);
    Order save(Order order);
    void deleteById(long id);
}

A service can depend on this interface rather than on JDBC classes. A production implementation may use a database, while a test implementation may use an in-memory map. This supports dependency inversion and makes business logic easier to test.

Repositories should represent meaningful aggregate boundaries rather than becoming generic containers for every query. Query methods should have clear semantics concerning missing data, ordering, pagination, and transaction behavior. A repository should not silently return incomplete objects if callers assume relationships are available.

The repository pattern does not eliminate persistence complexity. Mapping rows to objects, handling optimistic concurrency, translating exceptions, and controlling transaction scope remain necessary. It is an architectural boundary, not magic insulation.

9. Object-Relational Mapping

ORM frameworks map classes and objects to relational tables. JPA is a Java persistence specification, and Hibernate is a widely used implementation. An entity commonly has an identifier and annotations describing table mapping.

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal price;
}

ORM reduces repetitive mapping code and can manage entity states such as transient, managed, detached, and removed. A persistence context tracks managed objects and may delay SQL until flush or transaction commit.

ORM introduces its own hazards:

  • N+1 queries: loading one collection and then issuing one query per item;
  • unexpected lazy loading: accessing a relationship after the persistence context is closed;
  • over-fetching: retrieving large graphs when only a few fields are needed;
  • implicit SQL: framework-generated queries may be inefficient or surprising; and
  • identity confusion: object equality and database identity are not automatically identical.

Use fetch joins, projections, explicit query design, pagination, and query monitoring where appropriate. ORM is valuable for ordinary transactional applications, but direct SQL may be clearer for reporting, bulk operations, or database-specific optimization. The correct choice is contextual rather than ideological.

10. Applied Case: Course Registration Service

The following is an illustrative case.

Background

A university course-registration service must store students, course sections, and enrollments. Students can register only when a section has available capacity. The system must prevent two simultaneous registrations from exceeding capacity.

Problem

A naïve implementation reads the current enrollment count, checks capacity in Java, and then inserts an enrollment. Two concurrent requests can both observe one remaining place and both insert, producing over-enrollment.

Analysis

The rule depends on a shared resource and concurrent updates. A file-based solution would require careful locking and recovery, and would become difficult across multiple application servers. A relational database is more appropriate because it can enforce uniqueness, coordinate transactions, and recover from failures.

A schema might include students, sections, and enrollments. A unique constraint on (student_id, section_id) prevents duplicate registration. Capacity control can use a transaction with a row lock, or an atomic conditional update such as:

UPDATE sections
SET enrolled = enrolled + 1
WHERE id = ? AND enrolled < capacity;

The application checks the number of affected rows. If it is one, it inserts the enrollment in the same transaction. If it is zero, the section is full or absent. A failure during the enrollment insert causes rollback, so the counter is not incorrectly increased.

Approach and Outcome

The service uses an EnrollmentRepository, a transaction boundary in the application service, prepared statements or parameterized ORM queries, and database constraints. The database remains the final authority for concurrency-sensitive invariants. Audit records may be written in the same transaction, while notification email is handled asynchronously after successful commitment.

Learning Points

The case demonstrates that persistence design is not merely about saving objects. It requires identifying invariants, choosing a storage model, defining transaction scope, enforcing rules at the database boundary, and separating business services from storage details.

11. Security, Governance, and Professional Practice

Persistent data often contains personal, financial, or educational information. Professional systems should apply data minimization, access control, encryption in transit, and appropriate encryption at rest. Credentials must not be stored in source code. Database accounts should have only the permissions required by the application.

Logs must be useful without exposing passwords, tokens, health information, or unnecessary personal data. Retention and deletion policies should reflect organizational requirements and applicable privacy regulations. Backups are also sensitive copies and require access control, testing, and documented retention.

Schema migrations should be versioned, reviewed, and tested. A migration that changes a column or constraint can affect old application versions and rollback procedures. Disaster recovery is incomplete unless restoration has actually been tested.

AI-assisted programming can generate JDBC, SQL, or ORM code quickly, but generated code must be reviewed for injection risks, transaction errors, incorrect mappings, leaked secrets, and licensing or privacy concerns. A plausible-looking query is not evidence that it is correct.

12. Best Practices Checklist

  • Choose a representation according to data characteristics and interoperability needs.
  • Specify UTF-8 or another deliberate character set for text.
  • Use try-with-resources for I/O and JDBC resources.
  • Validate and constrain user-supplied paths.
  • Avoid deserializing untrusted native Java objects.
  • Use DTOs and validate external JSON.
  • Use prepared statements, never SQL string concatenation.
  • Keep transactions short and define their boundaries explicitly.
  • Enforce critical invariants with database constraints as well as application checks.
  • Monitor ORM-generated SQL and test for N+1 queries.
  • Separate domain logic from persistence through suitable interfaces.
  • Test failures, rollback behavior, migrations, backups, and recovery—not only successful reads and writes.

Chapter Summary

Persistent object-oriented applications translate in-memory objects into durable representations. Java byte streams support binary data, while character streams support text with explicit encodings. Java NIO provides path-oriented, resource-safe file operations, but file access must account for permissions, path traversal, race conditions, and large files.

Native Java serialization preserves object graphs but creates compatibility and deserialization risks. JSON improves interoperability but still requires validation, schema evolution, and careful handling of untrusted data. Relational databases provide structured storage, constraints, concurrency control, and transactions. JDBC offers direct access through connections, prepared statements, result sets, and explicit transaction management.

Repositories separate application services from persistence mechanisms. ORM frameworks reduce mapping effort but introduce issues such as lazy loading, N+1 queries, and hidden SQL. Effective persistence design combines object-oriented principles with database discipline: clear identity, explicit invariants, safe resource management, transactionally consistent updates, security controls, and tested recovery procedures.

Key Terms

Persistence: Storing data so it remains available after program termination.

Stream: An abstraction for sequential input or output of data.

Byte stream: A stream that processes raw binary bytes.

Character stream: A stream that processes text characters through encoding and decoding.

NIO.2: Java's modern file-system API in java.nio.file.

Path: An object representing a file-system location.

Serialization: Converting an object or object graph into a storable representation.

Deserialization: Reconstructing an object from a serialized representation.

JSON: A language-independent text format for structured data.

DTO: A data-transfer object used to carry data across boundaries.

JDBC: Java's standard API for relational database access.

Prepared statement: A parameterized SQL statement that separates code from values.

Primary key: A column or column set that uniquely identifies a row.

Foreign key: A column or column set referencing a key in another table.

Transaction: A logical unit of database work with defined completion or rollback.

ACID: Atomicity, consistency, isolation, and durability properties.

Repository: An abstraction representing access to persistent domain objects.

ORM: Object-relational mapping between classes and relational data.

Lazy loading: Loading related data only when it is accessed.

N+1 query problem: Inefficient execution of one initial query followed by one query per returned item.

Review Questions

Short Answer Questions

  1. What is the difference between a byte stream and a character stream?
  2. Why should a Java program specify a character set when reading or writing text?
  3. What security problem is prevented by using a prepared statement?
  4. State the four ACID properties of a transaction.
  5. What is the purpose of a repository interface?

Long Answer Questions

  1. Explain how Java NIO improves file handling and discuss path traversal and time-of-check/time-of-use risks.
  2. Compare native Java serialization and JSON in terms of compatibility, interoperability, and security.
  3. Describe the lifecycle of a JDBC query, including resource management and result mapping.
  4. Explain transaction boundaries, rollback, isolation, and the importance of database constraints.
  5. Discuss the benefits and limitations of ORM, including lazy loading and the N+1 query problem.

Analytical / Application Questions

  1. Design a persistence solution for a library system that stores books, members, loans, and overdue charges. Justify your choice of files, JSON, or a relational database.
  2. A registration system checks seat availability and then inserts a registration in separate transactions. Analyze the concurrency defect and propose a database-based correction.
  3. An application deserializes user-uploaded Java objects and uses ORM to load an entire customer graph for every request. Identify the security and performance risks and recommend a safer design.

Multiple Choice Questions with Answers

Question 1

Which Java type is most appropriate for representing a file-system location without necessarily opening the file?

A. Path

B. ResultSet

C. BigDecimal

D. Socket

Correct Answer: A. Path

Question 2

Why should SQL values supplied by users be bound through a PreparedStatement?

A. It automatically creates database backups

B. It separates SQL structure from values and helps prevent SQL injection

C. It guarantees that every query is indexed

D. It removes the need for transactions

Correct Answer: B. It separates SQL structure from values and helps prevent SQL injection

Question 3

Which property of ACID means that all operations in a transaction succeed together or are undone together?

A. Isolation

B. Durability

C. Atomicity

D. Consistency

Correct Answer: C. Atomicity

Question 4

What is a major risk of deserializing untrusted native Java serialized data?

A. The data is always converted to UTF-8

B. It may trigger gadget-chain behavior during deserialization

C. It cannot preserve object references

D. It automatically deletes the source file

Correct Answer: B. It may trigger gadget-chain behavior during deserialization

Question 5

Which class is generally preferable for storing monetary decimal values in Java?

A. float

B. double

C. char

D. BigDecimal

Correct Answer: D. BigDecimal

Question 6

What does a foreign key normally represent?

A. A reference from one table to a key in another table

B. A compressed database backup

C. A Java object method

D. A file encoding

Correct Answer: A. A reference from one table to a key in another table

Question 7

What is the main purpose of try-with-resources?

A. To retry failed SQL statements indefinitely

B. To close eligible resources automatically

C. To encrypt file contents

D. To create ORM entities

Correct Answer: B. To close eligible resources automatically

Question 8

What is the N+1 query problem?

A. A transaction contains no statements

B. One query is followed by an additional query for each returned item

C. A database has N+1 primary keys

D. A query uses exactly one parameter

Correct Answer: B. One query is followed by an additional query for each returned item

Question 9

In a repository-based design, which dependency should a business service normally use?

A. A concrete JDBC driver class

B. A repository abstraction

C. A file-system permission object

D. A JSON parser's internal buffer

Correct Answer: B. A repository abstraction

Question 10

Why should a money transfer debit and credit use the same transaction?

A. To ensure both changes commit or roll back as one logical operation

B. To force the database to use JSON

C. To prevent all concurrent reads permanently

D. To avoid defining account identifiers

Correct Answer: A. To ensure both changes commit or roll back as one logical operation

Object-Oriented Programming (Java)
Chapter 9 of 15