Java provides a powerful mechanism called serialization that allows you to convert an object into a sequence of bytes, which can then be saved to a file, transmitted over a network, or stored for later retrieval. The reverse process, deserialization, reconstructs the object from these bytes back into memory.
Serialization is the process of transforming the state of an object into a byte stream. This byte stream captures the object's data, and sometimes metadata, so it can be stored or transmitted. The key benefit is that the object’s entire state can be saved and restored later, enabling:
Without serialization, saving or transmitting complex objects would require manual conversion to a suitable format.
Serializable
InterfaceIn Java, an object must explicitly indicate that it can be serialized by implementing the marker interface java.io.Serializable
. This interface has no methods; it simply marks the class as serializable.
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// Constructor, getters, setters...
}
serialVersionUID
is a unique identifier for the class version. It helps during deserialization to ensure that a serialized object corresponds to a compatible class version. If not provided, Java generates one automatically, but explicitly defining it is best practice for version control.The standard serialization mechanism uses two classes from the java.io
package:
ObjectOutputStream
— Writes serialized objects to an output stream (e.g., file or socket).ObjectInputStream
— Reads serialized objects from an input stream and reconstructs them.This example shows how to serialize a Person
object to a file.
import java.io.*;
public class SerializeExample {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
try (FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(person); // Serialize the person object
System.out.println("Object serialized to person.ser");
} catch (IOException e) {
e.printStackTrace();
}
}
}
person.ser
.To restore the serialized object from the file:
import java.io.*;
public class DeserializeExample {
public static void main(String[] args) {
try (FileInputStream fileIn = new FileInputStream("person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
Person person = (Person) in.readObject(); // Deserialize object
System.out.println("Deserialized Person:");
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
readObject()
method reads the byte stream and reconstructs the Person
object.IOException
and ClassNotFoundException
.If a field should not be serialized (e.g., sensitive information or fields that can be recalculated), declare it as transient
:
private transient String password;
Such fields are ignored during serialization and restored with default values (null
for objects, zero for primitives) on deserialization.
Java serialization handles entire object graphs. If a serialized object references other objects (fields holding other objects), those referenced objects must also implement Serializable
. The whole graph is serialized recursively.
writeObject
and readObject
Classes can customize the serialization process by defining these private methods:
private void writeObject(ObjectOutputStream out) throws IOException {
// Custom serialization logic
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Custom deserialization logic
}
This allows, for example, encryption, compression, or special handling of certain fields.
Changing a class structure without updating serialVersionUID
can lead to InvalidClassException
during deserialization. Always update or maintain consistent version UIDs when evolving classes.
Serializable
to mark classes for serialization.ObjectOutputStream
and ObjectInputStream
for serialization/deserialization.serialVersionUID
.transient
fields to exclude sensitive or non-serializable data.Serialization in Java revolves around two core classes: ObjectOutputStream
and ObjectInputStream
. These classes provide the functionality to convert Java objects into a byte stream and vice versa, enabling easy persistence and communication of complex data structures.
ObjectOutputStream
and ObjectInputStream
?ObjectOutputStream
: Wraps around an underlying OutputStream
and writes Java objects as a serialized stream of bytes.ObjectInputStream
: Wraps around an InputStream
and reads serialized bytes, reconstructing Java objects.Together, they simplify the process of writing and reading serializable objects to/from files, network sockets, or any stream.
ObjectOutputStream
serializes objects implementing the Serializable
interface, including their entire object graph (all referenced objects).ObjectInputStream
reads the byte stream and recreates the objects in memory.The common pattern is to wrap a FileOutputStream
with an ObjectOutputStream
. This allows writing objects directly to a file.
Person
Objectimport java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
try (FileOutputStream fos = new FileOutputStream("person.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(person); // Serialize the person object
System.out.println("Object serialized successfully.");
} catch (IOException e) {
System.err.println("Serialization failed:");
e.printStackTrace();
}
}
}
writeObject(Object obj)
serializes the given object.person
references other serializable objects, they are also serialized recursively.To deserialize, wrap a FileInputStream
with an ObjectInputStream
and call readObject()
:
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("person.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
Person person = (Person) ois.readObject(); // Deserialize object
System.out.println("Deserialized Person:");
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
System.err.println("Deserialization failed:");
e.printStackTrace();
}
}
}
readObject()
reads and reconstructs the object.
It returns Object
, so a cast is required.
Handle both IOException
and ClassNotFoundException
:
IOException
for stream or file errors.ClassNotFoundException
if the class of the serialized object isn't found.serialVersionUID
Java serialization includes a version control mechanism using the serialVersionUID
field, which helps detect class mismatches between serialized objects and the current class version.
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// Constructors, getters, setters...
}
serialVersionUID
of the serialized class and the class at deserialization differ, an InvalidClassException
is thrown.serialVersionUID
protects against accidental incompatibilities caused by compiler-generated values.serialVersionUID
accordingly.Always close streams after use to free system resources and avoid data corruption. Use try-with-resources or explicit close()
calls.
Fields marked transient
are not serialized and will have default values after deserialization.
Classes can override writeObject
and readObject
private methods to customize serialization (e.g., encrypting data or handling transient fields).
private void writeObject(ObjectOutputStream out) throws IOException {
// Custom serialization logic
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Custom deserialization logic
in.defaultReadObject();
}
Common Java collections like ArrayList
, HashMap
, etc., implement Serializable
and can be serialized directly.
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class SerializeMultipleObjects {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
// Serialize list of people
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("people.ser"))) {
oos.writeObject(people);
System.out.println("List serialized.");
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize list of people
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("people.ser"))) {
List<Person> deserializedPeople = (List<Person>) ois.readObject();
System.out.println("Deserialized people:");
for (Person p : deserializedPeople) {
System.out.println(p.getName() + ", Age: " + p.getAge());
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
ObjectOutputStream
and ObjectInputStream
provide seamless Java object serialization and deserialization.writeObject()
to serialize objects and readObject()
to deserialize.IOException
, ClassNotFoundException
) properly.serialVersionUID
to maintain version compatibility.writeObject
and readObject
.Java provides two main interfaces for object serialization: Serializable
and Externalizable
. While both enable object serialization, Externalizable
offers greater control over the serialization process, allowing developers to customize exactly how an object's data is written and read. This section explains the Externalizable
interface, how it differs from Serializable
, and when and how to use it effectively.
Externalizable
?Externalizable
is a subinterface of Serializable
defined in the java.io
package. It requires the implementing class to explicitly define how its fields are serialized and deserialized by overriding two methods:
void writeExternal(ObjectOutput out) throws IOException
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
In contrast, Serializable
uses default serialization, which automatically serializes all non-transient fields.
Serializable
and Externalizable
Aspect | Serializable |
Externalizable |
---|---|---|
Serialization Logic | Automatic by JVM, serializes all non-transient fields | Manual; developer writes explicit serialization code |
Methods to Override | None mandatory (optional writeObject/readObject ) |
Must implement writeExternal and readExternal |
Control Over Data Written | Limited | Complete control over what and how to serialize |
Performance | Simpler, but can be slower due to extra metadata | Can be faster and more compact, if implemented carefully |
Use Case | General-purpose serialization | When custom serialization is needed or optimized serialization required |
Externalizable
?Externalizable
is ideal when:
transient
.Externalizable
InterfaceWhen a class implements Externalizable
, it must provide implementations for both writeExternal
and readExternal
. The serialization runtime will not automatically serialize any fields.
Externalizable
import java.io.*;
public class Person implements Externalizable {
private String name;
private int age;
private transient String password; // will not be serialized
// Mandatory no-arg constructor for deserialization
public Person() {}
public Person(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
// Serialize object data manually
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name); // Write name as UTF string
out.writeInt(age); // Write age as int
// Intentionally exclude password for security
}
// Deserialize object data manually
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
// password remains null after deserialization
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}
public class ExternalizableTest {
public static void main(String[] args) {
Person person = new Person("Alice", 30, "secretPass");
// Serialize to file
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("personExt.ser"))) {
oos.writeObject(person);
System.out.println("Person serialized.");
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize from file
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("personExt.ser"))) {
Person deserializedPerson = (Person) ois.readObject();
System.out.println("Deserialized: " + deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Output:
Person serialized.
Deserialized: Person{name='Alice', age=30, password='null'}
The password is not serialized because it was deliberately excluded in writeExternal
.
readExternal
.Externalizable
For typical use cases where default serialization suffices, Serializable
is easier and less error-prone.
Externalizable
extends Serializable
but requires explicit implementation of writeExternal
and readExternal
.Serializable
mechanism is insufficient.In Java IO, piped streams provide a simple yet powerful mechanism for threads to communicate by sending data through a stream, similar to how one thread writes data that another thread reads. The classes PipedInputStream
and PipedOutputStream
are designed to work together to create a pipe — a one-way communication channel — between threads.
PipedOutputStream
acts as the producer end, where data is written.PipedInputStream
acts as the consumer end, where data is read.PipedOutputStream
are available to read from the connected PipedInputStream
.This mechanism is analogous to a physical pipe: data flows in one end and emerges from the other.
To establish a pipe:
PipedOutputStream
.PipedInputStream
.connect()
method.After connection, writing bytes to the PipedOutputStream
makes those bytes available for reading from the PipedInputStream
.
The example demonstrates two threads:
PipedOutputStream
.PipedInputStream
.import java.io.*;
public class PipedStreamExample {
public static void main(String[] args) {
try {
// Create piped input and output streams and connect them
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
// Producer thread writes to PipedOutputStream
Thread producer = new Thread(() -> {
try (PrintWriter writer = new PrintWriter(pos)) {
String[] messages = {"Hello", "from", "the", "Producer", "thread!"};
for (String msg : messages) {
writer.println(msg);
writer.flush(); // Ensure data is sent immediately
System.out.println("Producer sent: " + msg);
Thread.sleep(500); // Simulate delay
}
} catch (InterruptedException e) {
System.err.println("Producer error: " + e.getMessage());
}
});
// Consumer thread reads from PipedInputStream
Thread consumer = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(pis))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Consumer received: " + line);
}
} catch (IOException e) {
System.err.println("Consumer error: " + e.getMessage());
}
});
// Start both threads
consumer.start();
producer.start();
// Wait for threads to finish
producer.join();
// Closing the output stream signals end of data, consumer will exit read loop
pos.close();
consumer.join();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
PipedInputStream
is constructed with the PipedOutputStream
as an argument, connecting the two.PrintWriter
wrapped around the PipedOutputStream
to write strings line-by-line.BufferedReader
wrapped around an InputStreamReader
on the PipedInputStream
to read lines.PipedOutputStream
. This causes the consumer’s read loop to terminate since readLine()
returns null
on end-of-stream.join()
.Benefits:
Limitations:
BlockingQueue
) for complex scenarios.PipedInputStream
and PipedOutputStream
before use.PipedInputStream(int pipeSize)
if necessary.PipedInputStream
and PipedOutputStream
allow one thread to send data directly to another using a stream.Java IO streams provide versatile tools for reading data sequentially. However, in some scenarios—such as parsing or processing complex data formats—you may need the ability to “unread” bytes or go back to a previously read position in the stream. This is where PushbackInputStream
and the mark()
/ reset()
methods come into play.
PushbackInputStream
?PushbackInputStream
is a subclass of FilterInputStream
that allows you to push bytes back into the input stream, effectively “unreading” them. This is especially useful in parsing scenarios where you read ahead some bytes to decide how to process them but realize that some of those bytes belong to the next data unit.
PushbackInputStream
Work?PushbackInputStream
, you can call the unread()
method to push one or more bytes back into an internal buffer.Imagine reading a stream where the next few bytes determine how to interpret the data, but you do not want to lose these bytes permanently if you decide to process them differently. PushbackInputStream
lets you peek and then push bytes back so they can be reread.
PushbackInputStream
import java.io.*;
public class PushbackExample {
public static void main(String[] args) throws IOException {
byte[] data = { 'a', 'b', 'c', 'd' };
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
PushbackInputStream pushbackInputStream = new PushbackInputStream(byteArrayInputStream);
int firstByte = pushbackInputStream.read();
System.out.println("Read byte: " + (char) firstByte); // Output: a
int secondByte = pushbackInputStream.read();
System.out.println("Read byte: " + (char) secondByte); // Output: b
// Decide to "unread" the second byte
pushbackInputStream.unread(secondByte);
System.out.println("Pushed back byte: " + (char) secondByte);
// Read again, should get the same byte
int rereadByte = pushbackInputStream.read();
System.out.println("Reread byte: " + (char) rereadByte); // Output: b
pushbackInputStream.close();
}
}
Output:
Read byte: a
Read byte: b
Pushed back byte: b
Reread byte: b
PushbackInputStream
IOException
.InputStream
), not character streams (Reader
).mark()
and reset()
MethodsWhile PushbackInputStream
lets you “unread” bytes, many streams support marking a position and later resetting the stream back to that position, allowing multiple bytes to be reread without pushing them back individually.
mark()
and reset()
Workmark(int readlimit)
tells the stream to remember the current position and keep a buffer of up to readlimit
bytes for possible reset.reset()
resets the stream back to that marked position.mark
and reset
.markSupported()
; if it returns true
, you can safely use mark()
and reset()
.BufferedInputStream
, ByteArrayInputStream
, and many readers like BufferedReader
.import java.io.*;
public class MarkResetExample {
public static void main(String[] args) throws IOException {
byte[] data = { 'x', 'y', 'z' };
ByteArrayInputStream bais = new ByteArrayInputStream(data);
BufferedInputStream bis = new BufferedInputStream(bais);
System.out.println("Read: " + (char) bis.read()); // x
if (bis.markSupported()) {
bis.mark(10); // mark current position with a buffer limit
System.out.println("Read after mark: " + (char) bis.read()); // y
System.out.println("Read after mark: " + (char) bis.read()); // z
bis.reset(); // reset to marked position
System.out.println("After reset: " + (char) bis.read()); // y (again)
}
bis.close();
}
}
Output:
Read: x
Read after mark: y
Read after mark: z
After reset: y
Feature | PushbackInputStream |
mark() / reset() |
---|---|---|
Usage | Manually unread bytes | Mark and rewind stream position |
Buffer Size | Fixed buffer size specified in constructor (default 1) | Managed internally by the stream, up to readlimit |
Flexibility | Precise byte-level unread | Allows rewinding to a mark position |
Stream Support | Only byte streams (InputStream ) |
Depends on stream; many support it |
Typical Use | Single or few bytes lookahead/unread | Larger lookahead with automatic rewind |
PushbackInputStream
allows unread bytes to be pushed back into the stream for subsequent re-reading, useful for simple lookahead or correcting read decisions.mark()
and reset()
let you mark a stream position and rewind to it later, enabling flexible multi-byte lookahead and reprocessing.markSupported()
before using mark/reset
.PushbackInputStream
when you want explicit unread control with a small buffer; use mark/reset
for more extensive or automatic rewind functionality.