☕ Java

transient

The transient keyword marks an instance field as excluded from Java's default serialization mechanism. When ObjectOutputStream serializes an object, it skips all transient fields — they are not written to the stream. When ObjectInputStream deserializes the object, transient fields are set to their default values: null for object references, 0 for numeric types, false for boolean. The transient modifier is used for three distinct purposes: fields whose values cannot be meaningfully serialized (network connections, threads, file handles, native resources), fields that contain sensitive data that must not leave the JVM (passwords, session tokens, cryptographic keys), and fields that can be derived from other serialized fields (cached computed values, derived indexes, memoized results). When transient fields need to be re-populated after deserialization, the readObject() hook is used to re-derive them. This entry covers the full transient contract, each use case with its correct pattern, interaction with final fields (transient final is legal but pointless since final fields cannot be re-assigned in readObject), and the distinction between transient and serialPersistentFields.

transient Contract — Behavior and Default Values

The transient modifier applies only to instance fields. It has no effect on static fields (which are never serialized regardless), local variables, or method parameters. A transient field behaves normally at runtime — it holds a value and can be read or written freely. The transient modifier only affects what ObjectOutputStream does when it serializes the object: transient fields are skipped. When ObjectInputStream deserializes an object containing transient fields, those fields receive their type's default value: null for any reference type (including String, Collections, arrays, and objects), 0 for int, long, short, byte, and char, 0.0 for float and double, false for boolean. These are the same defaults that field initializers would produce before a constructor runs. Any initialization that was assigned to the field in the constructor (or a field initializer expression) does not run — the constructor is bypassed by deserialization. The consequence: after deserialization, a transient field is in the same state as if the object had just been allocated with no constructor called. If the application code uses the field without first re-initializing it, it will encounter the default value — null for references, which commonly causes NullPointerException. This is why readObject() must re-derive transient fields from the serialized state, and why lazily-initialized transient fields must check for null before use (the same check as normal lazy initialization). A transient volatile field is valid — volatile affects memory visibility, transient affects serialization. Both modifiers can appear on the same field. Similarly, transient synchronized makes no sense (synchronized applies to methods, not fields), and transient static is redundant (static fields are never serialized with or without transient).
Java
// ── Basic transient behavior ──────────────────────────────────────────
public class ConnectionWrapper implements Serializable {
    private static final long serialVersionUID = 1L;

    private String   serverUrl;
    private int      port;
    private transient Socket connection;       // cannot be serialized — skip it
    private transient InputStream reader;      // tied to connection — skip it

    public ConnectionWrapper(String serverUrl, int port) throws IOException {
        this.serverUrl  = serverUrl;
        this.port       = port;
        this.connection = new Socket(serverUrl, port);  // establish connection
        this.reader     = connection.getInputStream();
    }

    // After deserialization: connection = null, reader = null
    // Must reconnect before use — readObject handles this:
    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject();    // restores serverUrl and port
        // Re-establish the connection using the restored server/port:
        this.connection = new Socket(serverUrl, port);
        this.reader     = connection.getInputStream();
    }
}

// ── Default values after deserialization ──────────────────────────────
public class TransientDemo implements Serializable {
    private static final long serialVersionUID = 1L;

    // These are serialized normally:
    private String  name  = "default";
    private int     count = 42;

    // These are transient — set to type defaults after deserialization:
    private transient String      computed = "COMPUTED";   // → null after deserialization
    private transient int         cached   = 999;          // → 0   after deserialization
    private transient boolean     ready    = true;         // → false after deserialization
    private transient List<String> index;                  // → null after deserialization
}

TransientDemo obj = new TransientDemo();
System.out.println(obj.computed);  // "COMPUTED" — set by field initializer

byte[] bytes = serialize(obj);
TransientDemo restored = (TransientDemo) deserialize(bytes);
System.out.println(restored.name);     // "default" — serialized
System.out.println(restored.count);    // 42 — serialized
System.out.println(restored.computed); // nulltransient field reset to default
System.out.println(restored.cached);   // 0transient field reset to default
System.out.println(restored.ready);    // falsetransient field reset to default

// ── transient volatile: both modifiers are valid on one field ──────────
public class CachedResult implements Serializable {
    private static final long serialVersionUID = 1L;
    private int data;
    private transient volatile String cachedStr;  // transient + volatile: valid and useful
    // transient: not serialized
    // volatile: thread-safe lazy initialization in multi-threaded context
}

// ── transient final: legal but largely useless ─────────────────────────
public class FinalTransient implements Serializable {
    private static final long serialVersionUID = 1L;
    private final transient int value;   // transient final: excluded from serialization
    // After deserialization: value = 0 (int default)
    // Cannot be re-assigned in readObject() — final fields are immutable after construction
    // The only way to "set" it would be via Unsafe or serialization proxy pattern
    FinalTransient(int v) { this.value = v; }
}

Use Cases — Sensitive Data, Derived Fields, and Non-Serializable Resources

The three distinct use cases for transient each have their own correct pattern. For sensitive data (passwords, API keys, session tokens, PINs), the field is transient to prevent it from being written to disk or transmitted over the network. If the value needs to survive serialization in encrypted form, writeObject writes the encrypted representation and readObject decrypts it. If the value must not survive at all (a one-time session token), the field is simply transient with no writeObject handling — after deserialization, it is null and the application must re-authenticate. For derived or cached fields — values computed from other serialized fields — the transient field is re-derived in readObject after defaultReadObject(). This is the canonical pattern for cached hash codes: the hash is marked transient (to avoid serializing a value that could be recomputed), and readObject does not need to explicitly re-compute it — the lazy initialization on first hashCode() call handles it naturally. For more expensive derived structures like an index over a list, readObject builds the index from the serialized list. For non-serializable resources — java.net.Socket, java.io.InputStream, java.lang.Thread, java.util.concurrent.locks.Lock, java.sql.Connection, java.io.FileDescriptor — these objects cannot be meaningfully serialized because their value is tied to OS state (file descriptors, network connections, thread scheduler state) that does not survive serialization. Marking them transient causes them to be null after deserialization. The application must then re-establish the resource when needed — either eagerly in readObject or lazily on first use. The lazy initialization pattern for transient fields after deserialization: the field is transient, initialized to null. Every method that uses the field checks for null first and initializes on demand. This is identical to the normal lazy initialization pattern but is forced by deserialization setting the field to null. For thread-safe lazy initialization, the double-checked locking pattern with volatile applies.
Java
// ── Use case 1: Sensitive data — password field ───────────────────────
public class UserCredentials implements Serializable {
    private static final long serialVersionUID = 1L;

    private String   username;
    private transient String password;   // NEVER serialize passwords in plaintext
    private transient char[] securePassword; // Or as char[] (can be zeroed out)

    public UserCredentials(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // After deserialization: password = null, securePassword = null
    // Application must re-authenticate to get the password again
    // DO NOT write password in writeObject — that would defeat the purpose
}

// Sensitive data with encrypted persistence:
public class ApiClient implements Serializable {
    private static final long serialVersionUID = 1L;

    private String   endpoint;
    private transient String apiKey;   // sensitive — not in default stream

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeObject(encryptApiKey(apiKey));  // write encrypted form only
    }

    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        String encrypted = (String) ois.readObject();
        this.apiKey = decryptApiKey(encrypted);  // decrypt on load
    }

    private String encryptApiKey(String key) { return "ENCRYPTED:" + key; }
    private String decryptApiKey(String enc)  { return enc.substring(10); }
}

// ── Use case 2: Derived/cached fields ────────────────────────────────
public class Document implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<String>  words;  // serialized: the source of truth
    private transient Map<String, Integer> wordIndex;  // derived: rebuilt in readObject
    private transient int cachedWordCount = -1;        // cached: recomputed lazily

    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject();          // restore 'words'
        this.wordIndex = buildIndex(words); // rebuild derived index
        this.cachedWordCount = -1;         // reset cache sentinel
    }

    public int wordCount() {
        if (cachedWordCount == -1) cachedWordCount = words.size();
        return cachedWordCount;
    }

    private Map<String, Integer> buildIndex(List<String> words) {
        Map<String, Integer> index = new HashMap<>();
        for (int i = 0; i < words.size(); i++) index.put(words.get(i), i);
        return index;
    }
}

// ── Use case 3: Non-serializable resource with lazy reconnect ──────────
public class DatabaseRepository implements Serializable {
    private static final long serialVersionUID = 1L;

    private String  jdbcUrl;
    private String  username;
    private transient Connection conn;  // Connection is not Serializable

    // Lazy initialization — works both initially and after deserialization:
    private Connection getConnection() throws SQLException {
        if (conn == null || conn.isClosed()) {
            conn = DriverManager.getConnection(jdbcUrl, username, getPassword());
        }
        return conn;
    }

    public List<String> queryAll() throws SQLException {
        try (PreparedStatement ps = getConnection().prepareStatement("SELECT name FROM data");
             ResultSet rs = ps.executeQuery()) {
            List<String> results = new ArrayList<>();
            while (rs.next()) results.add(rs.getString(1));
            return results;
        }
    }
    // After deserialization: conn = null → getConnection() creates new connection lazily
}

// ── Thread-safe lazy initialization for transient volatile ────────────
public class HeavyCache implements Serializable {
    private static final long serialVersionUID = 1L;
    private List<String> data;
    private transient volatile Map<String, List<String>> groupIndex;  // transient + volatile

    public Map<String, List<String>> getIndex() {
        if (groupIndex == null) {
            synchronized (this) {
                if (groupIndex == null) {
                    groupIndex = buildGroupIndex(data);  // thread-safe lazy init
                }
            }
        }
        return groupIndex;
    }
    // Works correctly both on first use and after deserialization (where groupIndex = null)
}

Related Topics in Java I/O

FileWriter
FileWriter is a convenience class for writing characters to a file, extending OutputStreamWriter with a FileOutputStream underneath. It encodes Java characters into bytes using the platform's default charset (or an explicit charset since Java 11) and writes them to a named file or File object. FileWriter supports two modes: overwrite (the default, which truncates the file to zero length on opening) and append (which positions the write pointer at the end of the existing file content). Like FileReader, FileWriter is unbuffered — each write() call propagates immediately to the underlying FileOutputStream, triggering system calls. In practice, FileWriter is almost always wrapped in a BufferedWriter to batch writes into efficient OS calls. The charset trap is identical to FileReader: pre-Java-11 constructors use the platform default charset silently, which causes portability problems; Java 11 constructors accept an explicit Charset. This entry covers all constructor variants with their charset and append semantics, the write methods and their character vs string behavior, newLine() in BufferedWriter, the flush/close contract, and the preferred modern alternatives.
BufferedReader
BufferedReader wraps any Reader with an in-memory character buffer, dramatically reducing system calls for character-by-character or line-by-line reading. Its defining method is readLine(), which reads a complete line of text terminated by \n, \r, or \r\n and returns it without the terminator, or returns null at end-of-file. Beyond buffering, BufferedReader also exposes a lines() method (Java 8+) that returns a lazy Stream<String> of lines, enabling the full Stream API for file processing without loading the entire file into memory. BufferedReader supports mark/reset with a caller-specified readAheadLimit. It is obtained either by wrapping a Reader (new BufferedReader(new FileReader(...))) or directly from Files.newBufferedReader(path, charset), which is the preferred idiom in modern Java. This entry covers construction and buffer sizing, all read methods and their contracts, readLine() edge cases (empty lines, last line without terminator), the lines() stream and its relationship to reader lifecycle, mark/reset semantics with readAheadLimit, and the use of BufferedReader as a base for protocol parsing.
BufferedWriter
BufferedWriter wraps any Writer with an in-memory character buffer, reducing system calls by accumulating characters until the buffer fills, flush() is called, or close() is called. It adds two capabilities not present in Writer: newLine(), which writes the platform-specific line separator, and an optimized write(String, int, int) that avoids creating a char[] copy by writing directly from the String. BufferedWriter is the standard output partner to BufferedReader — together they provide efficient line-by-line text file processing. It is constructed either by wrapping a Writer (new BufferedWriter(new FileWriter(...))) or via Files.newBufferedWriter(path, charset, options), the modern idiomatic alternative. Like all buffered streams, correct usage requires try-with-resources to guarantee that buffered data is flushed and the file is closed even when exceptions occur. This entry covers construction and buffer sizing, all write methods and their interaction with the buffer, newLine() and its platform behavior, flush semantics including when explicit flush is necessary, the difference between close() and flush(), and performance patterns for high-throughput text writing.
PrintWriter
PrintWriter is a character-based output class that wraps any Writer or OutputStream and adds convenience methods for printing formatted representations of all Java primitive types, strings, and objects. Its defining characteristic is that none of its print(), println(), and printf() methods throw checked IOException — errors are silently swallowed and can only be detected after the fact by calling checkError(). This makes PrintWriter easy to use interactively and in situations where I/O failure is genuinely unrecoverable (writing to System.out, generating diagnostic output), but makes it dangerous for critical data writing where exceptions must be caught and handled. PrintWriter can auto-flush on println(), printf(), and format() calls when constructed with autoFlush=true, which is useful for interactive console output and network protocol streams. Its printf() and format() methods delegate to java.util.Formatter, enabling C-style formatted output with full locale awareness. This entry covers all constructor variants and their autoFlush and buffering behavior, every print/println/printf method, the checkError() error detection model, the difference between PrintWriter and PrintStream, charset handling, and when PrintWriter is the right choice versus BufferedWriter.