Sunday, October 31, 2021

Java Tutorial: BigInteger Class

Chapters

BigInteger Class

BigInteger class is immutable arbitrary-precision integers. This class is counterpart of int primitive type. Although, Unlike int primitive, BigInteger can store a very large number that suprasses even the long value range. BigInteger has a range value of -2^Integer.MAX_VALUE(exclusive) to 2^Integer.MAX_VALUE(exclusive) and may support values outside of that range.
By the way, Integer.MAX_VALUE = 2,147,483,647.

BigInteger Constructors

BigInteger has several constructors. Although, I'm only gonna demonstrate two of them which are easy to understand and implement. Other constructors can be seen in the documentation.
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args){
    
    //1 byte = 8 bits
    byte[] bytes = {0b0101110, 0b00001010};
    
    //Constructor form
    //public BigInteger(byte[] val)
    BigInteger bi1 = new BigInteger(bytes);
    
    //use toString() to get the BigInteger
    //value in String form
    System.out.println("output1: " + bi1.toString());
    
    //Constructor form
    //BigInteger(String val)
    BigInteger bi2 = new BigInteger("1500000000");
    
    System.out.println("output2: " + bi2.toString());
    
    //Constructor form
    //BigInteger(int signum, byte[] magnitude)
    BigInteger bi3 = new BigInteger(-1, bytes);
    System.out.println("output3: " + bi3);
  }
}

Result
output1: 11786
output2: 1500000000
output3: -11786
This constructor public BigInteger(byte[] val) accepts an array of bytes then, the bits of the bytes in the array are concatenated. This constructor BigInteger(String val) converts the string to a BigInteger digit.

This constructor BigInteger(int signum, byte[] magnitude) accepts and array of bytes, the bits of the bytes in the array are concatenated and translates the sign represented as an integer signum value: -1 for negative, 0 for zero, or 1 for positive. This constructor throws NumberFormatException if signum is not one of the three legal values (-1, 0, and 1), or signum is 0 and magnitude contains one or more non-zero bytes.

BigInteger Operations

Since BigInteger is an object type, standard operators like "+" operator don't work with BigInteger. However, BigInteger has methods that are analogous to standard operations. I'm gonna demonstrate some of them here.
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args){
  
    BigInteger bi1 = new BigInteger("1040700980");
    BigInteger bi2 = new BigInteger("2500555000");
    BigInteger result = null;
    
    //add two BigInteger values
    result = bi1.add(bi2);
    System.out.println(bi1 + " + " + bi2 + " = " + result);
    
    //subtract two BigInteger values
    result = bi2.subtract(bi1);
    System.out.println(bi2 + " - " + bi1 + " = " + result);
    
    //multiply two BigInteger values
    result = bi1.multiply(bi2);
    System.out.println(bi1 + " * " + bi2 + " = " + result);
    
    //divide two BigInteger values
    result = bi2.divide(bi1);
    System.out.println(bi2 + " / " + bi1 + " = " + result);
  }
}

Result
1040700980 + 2500555000 = 3541255980
2500555000 - 1040700980 = 1459854020
1040700980 * 2500555000 = 2602330039043900000
2500555000 / 1040700980 = 2

Converting BigInteger value to Primitive Type

BigInteger has methods that will let us convert the value in BigInteger to primitive types. We already know that we can convert BigInteger value to string by using toString() method. In this topic, I'm gonna demonstrate converting BigInteger value to primitive types.

Note: Typecasting principles are still applied when converting BigInteger value to a primitive type.
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args){
  
    BigInteger bi = new BigInteger("12345678");
    
    //intValue() converts BigInteger to
    //int primitive
    int intVal = bi.intValue();
    System.out.println("int: " + intVal);
    
    //longValue() converts BigInteger to
    //long primitive
    long longVal = bi.longValue();
    System.out.println("long: " + longVal);
    
    //shortValue() converts BigInteger to
    //short primitive
    short shortVal = bi.shortValue();
    System.out.println("short: " + shortVal);
  }
}

Result
int: 12345678
long: 12345678
short: 24910
negate() Method

This method negates BigInteger value. We use this method to make a BigInteger negative if the BigInteger value is positive or positive if the BigInteger value is negative.
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args){
  
    BigInteger bi1 = new BigInteger("-1040700980");
    BigInteger bi2 = new BigInteger("2500555000");
    BigInteger result = null;
    
    //negate bi2.
    bi2 = bi2.negate();
    
    result = bi2.add(bi1);
    System.out.println(bi2 + " + " + bi1 + " = " + result);
    
    //negate bi2 again. Now bi2 sign is positive again.
    bi2 = bi2.negate();
    
    result = bi2.add(bi1);
    System.out.println(bi2 + " + " + bi1 + " = " + result);
  }
}

Result
-2500555000 + -1040700980 = -3541255980
2500555000 + -1040700980 = 1459854020

Java OOPs: Coupling and Cohesion

Chapters

Coupling

Coupling refers to the dependency between two classes. There are two types of coupling: Tight Coupling and Loose Coupling.

Tight Coupling

Tight coupling means that two classes are highly dependent on each other. In tight coupling, if a class has changes then the other class will very likely need to do some changes to preserve their dependency on each other.

Tight coupling is used sparingly in coding due to its restrictive nature that makes our code stiff and hard to maintain. Too much use of tight coupling is a bad software design.
public class SampleClass{

  public static void main(String[] args){
    ClassB b1 = new ClassB();
    b1.sum();
  }
}

class ClassA{
  
  protected int a = 1;
  protected int b = 2;
  protected int c = 3;
  protected int d = 4;
  
  
}

class ClassB extends ClassA{

  void sum(){
    int result = a + b + c + d;
    System.out.println("sum: " + result);
  }
}
In the example above ClassA and ClassB are tighly coupled. How? you might ask. Try removing int a from ClassA. if you do that, you need to remove the "a" in ClassB. Same goes for other variables in ClassA. The changes in the code structure of ClassA have effect in the code structure of ClassB.

Loose Coupling

Loose coupling means that two classes are not highly dependent on each other. In loose coupling, if a class has changes then the other class will likely not need to do some changes to preserve their dependency on each other.

Loose coupling is often used in coding due to its flexible nature that makes our code easy to maintain. A good software design has more loosely coupled classes than tightly coupled ones.
public class SampleClass{

  public static void main(String[] args){
    ClassB b1 = new ClassB();
    b1.sum();
  }
}

class ClassA{
  
  private int a = 1;
  private int b = 2;
  private int c = 3;
  private int d = 4;
  
  protected int addFirst(){
    return a + b;
  }
  
  protected int addSecond(){
    return c + d;
  }
}

class ClassB extends ClassA{

  void sum(){
    int result = addFirst() + addSecond();
    System.out.println("sum: " + result);
  }
}
In the above example, ClassA and ClassB are loosely coupled. How? you might ask. Try removing int a and remove "a" and "+" in addFirst() block. If you do that, ClassB doesn't need to do any changes because the changes in the code structure of ClassA doesn't have any effect in the code structure of ClassB.

Cohesion

Cohesion refers to the functionality of a class. There are two types of cohesion: Low Cohesion and High Cohesion.

Low Cohesion

a class has low cohesion if it's focusing on multiple functionalities. A class with low cohesion may cause confusion which make our code less maintainalble and hard to read. We shouldn't create low cohesive classes often.
class DataOperation{

  void printData(){
    System.out.println("Data Printed!");
  }
  
  void printData(String filter){
    System.out.println("Data Printed!");
    System.out.println("Filter: " + filter);
  }
  
  void filterData(){
    System.out.println("Data Filtered!");
  }
  
  void filterData(String filter){
    System.out.println("Data Filtered!");
    System.out.println("Filter: " + filter);
  }
  
}
The class in the example above is a low cohesive class because the class focuses on two functionalities: the filter and print functionalities.

High Cohesion

a class has has cohesion if it's highly or solely focusing on a single functionality. A class with high cohesion are less confusing and easy to read. A good software design has more high cohesive classes that low cohesive ones.
class DataPrinter{

  void printData(){
    System.out.println("Data Printed!");
  }
  
  void printData(String filter){
    System.out.println("Data Printed!");
    System.out.println("Filter: " + filter);
  }
  
}
The class in the example above is a high cohesive class because the class solely focuses on one functionality: the print functionality.

Saturday, October 30, 2021

Java Design Pattern: Singleton Class

Chapters

Singleton class

Singletons are classes that have only one instance that may be accessed globally. To create a singleton class, we need three elements: static reference, private constructor and static factory method. Also, there are two types of singleton design pattern: Early Instantiation and Lazy Instantiation.

Early Instantiation

Early Instantiation is the creation of an instance at load time. This example demonstrates Early Instantiation.
public class SampleClass{

  public static void main(String[] args){
    
    //get singleton instance
    Box box1 = Box.getInstance();
    Box box2 = Box.getInstance();
    System.out.println(box1 == box2);
  }
}

class Box{

  //static reference
  //myBox is instantiated early
  private static Box myBox = new Box();
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  //static factory method. This
  //method produces and returns
  //the singleton instance
  public static Box getInstance(){
    return myBox;
  }
  
  //getters
  public static String getName(){
    return name;
  }
  
}
Lazy Instantiation

Lazy Instantation is the creation of an instance when it's only needed. This example demonstrates Lazy Instantiation.
public class SampleClass{

  public static void main(String[] args){
    
    //get singleton instance
    Box box1 = Box.getInstance();
    Box box2 = Box.getInstance();
    System.out.println(box1 == box2);
  }
}

class Box{

  //static reference
  private static Box myBox;
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  //static factory method. This
  //method produces the singleton
  //instance
  //singleton instance is instantiated
  //when it's needed
  public static Box getInstance(){
    
    if(myBox == null)
      myBox = new Box();
      
    return myBox;
  }
  
  //getters
  public static String getName(){
    return name;
  }
  
}
In the getInstance() method, we use null to check if there's already a singleton instance. Though, we can use boolean variable to check if a singleton instance exists.
class Box{

  private static Box myBox;
  //boolean variable for checking
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}
Enum Singleton

An enum class can be a singleton. An enum class is inherently constant, so, if we want a singleton that is constant then, consider using enum class as singleton. Creation of enum instance is thread safe. However, the methods are not.
public class SampleClass{

  public static void main(String[] args){
    System.out.println(Box.BOX.getName());
  }
}

enum Box{
  
  BOX("myBox");
  
  private String name;
  
  private Box(String name){
    this.name = name;
  }
  
  public Box getInstance(){
    return BOX;
  }
  
  public String getName(){
    return name;
  }
}
Singleton Issues

Singleton is easy to implement and can be useful in some situations. Though, this design pattern has issues with other java concepts.

Classloaders

If a singleton class is loaded by two classloaders, each classloader will create an instance of the singleton class. This breaks the principle of singleton class.

Deserialization

If we deserialize a serialized singleton object, the singleton object that has been returned won't be the same as its previous form. Take a look at this example.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      
      oos.writeObject(input);
      oos.flush();
      
      Box output = (Box)ois.readObject();
      
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  private Box(){}
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}

Result
false
As you can see, the Box object in input variable is different from the Box object in output variable. This issue, however, has a solution. We need to manually define the readResolve() method in a serializable class which is the Box class in this case. readResolve() method allows a class to replace/resolve the object read from the stream before it is returned to the caller.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      
      oos.writeObject(input);
      oos.flush();
      
      Box output = (Box)ois.readObject();
      
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //Constructor
  private Box(){}
  
  //manually define readResolve() method
  protected Object readResolve(){
    return getInstance();
  }
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}

Result
true
Remember that readResolve() replaces the object from the stream. So, expect that the some values of the fields of the deserialized object may not be the same as their previous values.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      System.out.println("Input name: " + input.getName());
      
      System.out.println("Serializing input object...");
      oos.writeObject(input);
      oos.flush();
      
      System.out.println("Changing input name...");
      input.setName("boxBox");
      System.out.println("New input name: " + input.getName());
      
      System.out.println("Deserializing...");
      Box output = (Box)ois.readObject();
      
      System.out.println("Output name: " + output.getName());
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //Constructor
  private Box(){}
  
  //manually define readResolve() method
  protected Object readResolve(){
    return getInstance();
  }
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
  public static void setName(String newName){
    name = newName;
  }
  
}

Result
Input name: myBox
Serializing input object...
Changing input name...
New input name: boxBox
Deserializing...
Output name: boxBox
true
As you can see from the result, the name of the Box object in input variable before serialization is different from the name of the Box object in output variable after serialization.

JVMs

A singleton is unique per JVM. A system that involves multiple JVMs breaks the principle of singleton class. Systems that rely on distributed technologies may involve multiple JVMs.

Garbage Collection

A singleton object might be garbage-collected if it's not being referenced. If this happens, new instance will be created and the old one will be replaced. This may cause problems in some situations.

Friday, October 29, 2021

Java Tutorial: Serialization and Deserialization

Chapters

Serialization and Deserialization

Serialization is a process of writing primitive types and state of object types to an output stream. We use ObjectOutputStream to process serialization. Deserialization is a process of reading serialized objects and primitive types from an input stream. We use ObjectInputStream to process deserialization.

Note: Static elements like static class and its members are not serializable because they belong to the class itself not to the class instance. Serialization only serializes class instance and its members.

Serialization Using ObjectOutputStream

This example demonstrates serialization using ObjectOutputStream. To serialize an object of a class, the class must implement Serializable interface.

Note: Objects that come out from inner, local and anonymous classes are discouraged by java to be serialized.
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    Profile profileOne = new Profile("John", 22,
                         "Johnny",
                         new Message("Hello!"),
                         1442);
    
    //when serializing an array of objects, be
    //sure that the objects in the array are
    //all serializable or else
    //serialization will fail
    Data[] datArr = new Data[2];
    
    datArr[0] = new Profile("Mark", 20,
                            "Marky",
                            new Message("Hi!"),
                            1322);
                            
    datArr[1] = new Profile("Dolly", 18,
                            "lily",
                            new Message("Hey!"),
                            40298);
    
    File file = new File("C:\\test\\test.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file)) ){
            
      oos.writeObject(profileOne);
      oos.writeObject(datArr);
      
      //flush ObjectOutputStream
      //so, the data can be written
      //to the underlying output stream
      oos.flush();
    }
    System.out.println("Done!");
  }
}

//a class must implement Serializable interface
//to make its objects serializable
//if the serializable class has subclasses,
//those subclasses are also serializable
abstract class Data implements Serializable{
  
  //if a serializable class has class
  //references, those references must
  //be serializable. Otherwise, java
  //will throw NotSerializableException
  //when you try to serialize a
  //serializable class with
  //class reference that is not
  //serializable
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}

class Profile extends Data{

  private String nickName;
  
  //we can add a transient modifier
  //to a class reference that are
  //not serializable to avoid
  //NotSerializableException if
  //the unserializable class reference
  //is in a serializable class
  private transient Message message;
  
  //by default, static elements
  //are not serializable
  //because static elements
  //are not part of an object
  //Though, they don't trigger
  //NotSerializableException
  //adding transient in a static
  //field is redundant
  private static int sessionNo;
  
  Profile(String name, int age, String nickName,
          Message message, int session){
    super(name, age);
    this.nickName = nickName;
    this.message = message;
    sessionNo = session;
  }
  
  public static int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    String str = null;
    
    if(message != null)
      str = message.getMessage();
    
    return str;
  }
}

class Message{
  
  private String msg;
  
  Message(String msg){
    this.msg = msg;
  }
  
  public String getMessage(){
    return msg;
  }
}
transient Keyword

Variables that have transient keyword won't be included in the serialization process. In the example above, we use the transient keyword in an object reference. Though, transient keyword can be used for primitive and object type variables. The transient keyword is used to avoid NotSerializableException. This exception occurs if an unserializable class has a reference in a serializable class.

In they example above, Message class is not serializable that's why I put transient keyword in the message variable in Profile class which is a serializable class. Try removing the transient keyword and you will see a NotSerializableException.

Deserialization Using ObjectInputStream

This example demonstrates serialization using ObjectOutputStream.
Note: the example below requires test.obj that can be created by executing the example code in the Serialization Using ObjectOutputStream topic.

Warning: Deserialization of untrusted data is inherently dangerous and should be avoided. Untrusted data should be carefully validated according to the "Serialization and Deserialization" section of the Secure Coding Guidelines for Java SE. Serialization Filtering describes best practices for defensive use of serial filters.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
         throws IOException, ClassNotFoundException{
    
    File file = new File("C:\\test\\test.obj");
    try(ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file)) ){
      
      //Read object types
      //read the source data according to
      //the order of how the source data
      //have been written
      Profile p1 = (Profile)ois.readObject();
      Data[] arr = (Data[])ois.readObject();
      System.out.println("Deserialization Complete!");
      System.out.println();
      
      System.out.println("Profile #1");
      System.out.println("name: " + p1.getName());
      System.out.println("age: " + p1.getAge());
      System.out.println("nickname: " + p1.getNickName());
      System.out.println("message: " + p1.getMessage());
      System.out.println("session #: " + p1.getSessionNo());
      System.out.println();
      
      System.out.println("Profile #2");
      Profile p2 = (Profile)arr[0];
      System.out.println("name: " + p2.getName());
      System.out.println("age: " + p2.getAge());
      System.out.println("nickname: " + p2.getNickName());
      System.out.println("message: " + p2.getMessage());
      System.out.println("session #: " + p2.getSessionNo());
      
    }
  }
}

abstract class Data implements Serializable{
  
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}

class Profile extends Data{

  private String nickName;
  
  private transient Message message;
  
  private static int sessionNo;
  
  Profile(String name, int age, String nickName,
          Message message, int session){
    super(name, age);
    this.nickName = nickName;
    this.message = message;
    sessionNo = session;
  }
  
  public static int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    String str = null;
    
    if(message != null)
      str = message.getMessage();
    
    return str;
  }
}

class Message{
  
  private String msg;
  
  Message(String msg){
    this.msg = msg;
  }
  
  public String getMessage(){
    return msg;
  }
}

Result
Deserialization Complete!

Profile #1
name: John
age: 22
nickname: Johnny
message: null
session #: 0

Profile #2
name: Mark
age: 20
nickname: Marky
message: null
session #: 0
In the result above, message is null and session # is 0 because message variable is transient and sessionNo variable is static.
SerialVersionUID

SerialVersionUID is used to verify serializable class between the receiver and the sender. The SerialVersionUID of a serializable class in the sender's program must be the same as the SerialVersionUID of the serializable class in the receiver's program.

Otherwise, InvalidClassException will be thrown during deserialization. We can manually declare a serializable class's SerialVersionUID. If SerialVersionUID is not declared, java will automatically generate a SerialVersionUID for that class. This is how we declare a SerialVersionUID.
private static final long SerialVersionUID = 1L;
You can change the value on the right side of the equal operator but you shouldn't change the declaration expression on the left side. It's strongly recommended to manually declare SerialVersionUID in serializable classes, except for serializable enum classes, to avoid unwanted compatibility issues. Let's try adding SerialVersionUID in a serializable class.
abstract class Data implements Serializable{
  
  //explicit declaration of SerialVersionUID
  private static final long SerialVersionUID = 100L;
  
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}
Custom Way of Reading and Writing Serializable Objects

Java has a default way of reading/writing serializable objects. However, we can override that default way by defining a readObject() and a writeObject() methods in a serializable class. In this way, java will execute the readObject() and writeObject() that we defined in the serializable class everytime we serialize/deserialize objects of that serializable class.

This customization is particularly used when we want to serialize some serializable fields from unserializable object reference.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
    
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
        Data input = new Data("John", 22,
                    new Profile(1225,"Johnny",
                                "Hello!"));
        oos.writeObject(input);
        Data output = (Data)ois.readObject();
    }
    
  }
}

class Data implements Serializable{
  private static long SerialVersionUID = 1L;
  
  private String name;
  private int age;
  private transient Profile profile;
  
  Data(String name, int age,
       Profile profile){
    this.name = name;
    this.age = age;
    this.profile = profile;
  }
  
  //define custom writeObject() method
  private void writeObject(ObjectOutputStream oos)
                           throws IOException{
    
    System.out.println("Writing Objects...");
    //this method serializes serializable and
    //non-transient fields
    oos.defaultWriteObject();
    
    //In this way, we are able to serialize
    //the value of the message variable in 
    //profile object.
    if(profile != null)
      oos.writeObject(profile.getMessage());
  }
  
  //define custom readObject() method
  private void readObject(ObjectInputStream ois)
          throws IOException, ClassNotFoundException{
    
    System.out.println("Reading Objects...");
    //this method reads serialized
    //objects of the Data object
    ois.defaultReadObject();
    
    //read the serialized message
    //that is not a member of Data object
    System.out.println(name+" Previous Message: " +
                       ois.readObject());
  }
  
  String getName(){
    return name;
  }
  
  int getAge(){
    return age;
  }
  
  void setProfile(Profile profile){
    this.profile = profile;
  }
}

class Profile{

  private int sessionNo;
  private String nickName;
  private String message;
  
  Profile(int sessionNo, String nickName,
          String message){
          
    this.sessionNo = sessionNo;
    this.nickName = nickName;
    this.message = message;
  }
  
  int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    return message;
  }
  
  void setMessage(String message){
    this.message = message;
  }
}

Result
Writing Objects...
Reading Objects...
John Previous Message: Hello!
Externalizable Interface

Externalizable is just like Serializable. Although, Externalizable gives full control of serialization/deserialization process to the class itself.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInput;
import java.io.ObjectOutputStream;
import java.io.ObjectOutput;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Externalizable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
      
      Data input = new Data(22, "John", 3.5f);
      input.writeExternal(oos);
      
      //flush ObjectOutputStream so the
      //data can be written to the undelying
      //output stream
      oos.flush();
      
      //when deserializing, we need to create
      //an object first before calling
      //readExternal()
      Data output = new Data();
      output.readExternal(ois);
      
      System.out.println("output");
      System.out.println("name: " + output.name);
      System.out.println("age: " + output.age);
      System.out.println("score: " + output.score);
    }
    
  }
}

class Data implements Externalizable{
  private static long SerialVersionUID = 1L;
  
  Integer age;
  String name;
  Float score;
  
  Data(int age, String name, float score){
  
    this.age = age;
    this.name = name;
    this.score = score;
  }
  
  //Default Constructor
  Data(){}
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    System.out.println("Writing Serializable Fields");
    out.writeInt(age);
    out.writeUTF(name);
    out.writeFloat(score);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    System.out.println("Reading Serializable Fields");
    this.age = in.readInt();
    this.name = in.readUTF();
    this.score = in.readFloat();
  }
  
}

Result
Writing Serializable Fields
Reading Serializable Fields
output
name: John
age: 22
score: 3.5
When a serializable class that extends Externalizable has subclass, the subclass needs to override writeExternal() and readExternal() methods and call writeExternal() and readExternal() of the super class. This will ensure that the serializable fields in the super class are included in the serialization/deserialization process in the subclass.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInput;
import java.io.ObjectOutputStream;
import java.io.ObjectOutput;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Externalizable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
      
      Profile input = new Profile(22, "John",
                     "Johnny", 2250, 55);
      input.writeExternal(oos);
      
      //flush ObjectOutputStream so the
      //data can be written to the undelying
      //output stream
      oos.flush();
      
      Profile output = new Profile(3307);
      output.readExternal(ois);
      
      System.out.println("output");
      System.out.println("name: " + output.name);
      System.out.println("age: " + output.age);
      System.out.println("nickname: " + output.nickname);
      System.out.println("score: " + output.score);
    }
    
  }
}

abstract class Data implements Externalizable{
  private static long SerialVersionUID = 1L;

  Integer age;
  String name;
  
  Data(int age, String name){
  
    this.age = age;
    this.name = name;
  }
  
  //Default Constructor
  Data(){}
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    out.writeInt(age);
    out.writeUTF(name);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    this.age = in.readInt();
    this.name = in.readUTF();
  }
  
}

class Profile extends Data{
  private static long SerialVersionUID = 1L;
  
  String nickname;
  private int sessionNo;
  int score;
  
  Profile(int age, String name, String nickname,
          int sessionNo, int score){
          
    super(age,name);
    this.nickname = nickname;
    this.sessionNo = sessionNo;
    this.score = score;
  }
  
  
  Profile(int sessionNo){
    this.sessionNo = sessionNo;
  }
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    super.writeExternal(out);
    out.writeUTF(nickname);
    out.writeInt(score);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    super.readExternal(in);
    nickname = in.readUTF();
    score = in.readInt();
  }
}

Result
output
name: John
age: 22
nickname: Johnny
score: 55
In the example above, sessionNo has been skipped. When serializing/deserializing object with Externalizable interface, We don't need to add transient modifier to the fields that we don't wanna be serialized. We just skip them.
Differences Between Externalizable and Serializable

These two interfaces are closely similar to each other. Although, these two have different usage. These are the differences between these two interfaces:
  • Externalizable requires two methods to be overriden. Serializable doesn't require any methods to be overriden.
  • Serializable is easy to implement and the JVM does most of the job in serialization/deserialization process. Externalizable is tedious to implement and the implementor is responsible for the logical process of serialization/deserialization
  • Serializable has internal serialization/deserialization processes that our project may not need whereas in Externalizable, we decide the serialization/deserialization logic. Thus, Using externalizable often yields better performance.
  • In Serializable, we add transient keyword to any fields that we don't want to be serialized whereas in Externalizable, we just skip fields that we don't want to be serialized.
  • When doing simple serialization/deserialization task, Serializable will suffice. When doing advanced serialization/deserialization task, consider using Externalizable

Java tutorial: Exploring java.io Package

Chapters

InputStream, OutputStream, Reader and Writer

In this tutorial, we will explore the streams in java.io package. We will discuss streams that we may use in our future projects. First off, let's start with InputStream and OutputStream.

InputStream is an abstract class that is the superclass of all classes representing an input stream of bytes whereas OutputStream is an abstract class that is the superclass of all classes representing an output stream of bytes. Reader is an abstract class for reading character streams and Writer is an abstract class for writing to character streams. Now, let's talk about some of their subclasses.

Note: After using a stream, don't forget to close it. Use the close() method to close a stream to free up memory. Every stream has close() method.

FileInputStream and FileOutputStream

FileInputStream is meant for reading bytes from a file whereas FileOutputStream is meant for writing bytes to a file. These streams operate files such as video, images, audio, etc. They can read/write characters but other streams are much better at reading/writing characters such as FileReader and FileWriter.
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class SampleClass{
  
  public static void main(String[] args){
    File source = new File("C:\\test\\img.png");
    File target = new File("C:\\test\\folder1\\"+
                           "imgCopy.png");
    
    try(FileInputStream fis = 
        new FileInputStream(source);
        FileOutputStream fos = 
        new FileOutputStream(target)){
        
        //readAllBytes() method Reads all
        //remaining bytes from the input stream.
        //this method is intended for simple
        //cases where it is convenient to read
        //all bytes into a byte array.
        System.out.println("Reading bytes...");
        byte[] bytes = fis.readAllBytes();
        
        System.out.println("File size(bytes):"+
                           bytes.length);
                           
        System.out.println("Writing bytes...");
        //write() method writes bytes to the
        //target file
        fos.write(bytes);
        System.out.println("Done!");
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
Another way to read/write bytes is to read/write one byte at a time. Though, this kind of read/write process is inefficient especially when reading/writing large files.
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class SampleClass{
  
  public static void main(String[] args){
    File source = new File("C:\\test\\img.png");
    File target = new File("C:\\test\\folder1\\"+
                           "imgCopy.png");
    
    try(FileInputStream fis = 
        new FileInputStream(source);
        FileOutputStream fos = 
        new FileOutputStream(target)){
        
        int num = 0;
        
        //read one byte per loop
        //read() method returns a byte
        //of data. It returns -1 if the
        //end of a file is reached.
        while((num = fis.read()) != -1)
          fos.write(num);
        
        System.out.println("Done!");
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
Another way to read/write data is to create a buffer. This method is more efficient than the two previous methods. Buffer is like a container that contains set of bytes of data.
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class SampleClass{
  
  public static void main(String[] args){
    File source = new File("C:\\test\\img.png");
    File target = new File("C:\\test\\folder1\\"+
                           "imgCopy.png");
    
    try(FileInputStream fis = 
        new FileInputStream(source);
        FileOutputStream fos = 
        new FileOutputStream(target)){
        
        //a buffer with a size of 1024 bytes
        //or 1kb
        byte[] bytes = new byte[1024];
        
        //this read(byte[] b) method tries to
        //fill up the array parameter with
        //fresh bytes of data from the
        //inputstream per loop. Returns the
        //total number of bytes read from
        //the buffer, or -1 if there is no
        //more data because the end of the
        //file has been reached
        while(fis.read(bytes) != -1)
          fos.write(bytes);
        
        System.out.println("Done!");
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
What is a buffer?

A buffer is a temporary storage of data. A buffer can help improve the efficiency of a data transfer. For example, when we read data from a file, sometimes, transferring of data is faster than receiving the data and vice-versa. Without a buffer, our program may experience slow down or interruption due to varying rates of tranferring and receiving data. By using buffer, we can minimize these slow down or interruption. A buffer acts as a placeholder of data between two or more entities. In this way, data transfer happens in the buffer instead of direct data transfer between entities.

Buffer size can vary based on a task our program is doing. In the example above, I use 1024 bytes of 1kb(in binary) because it's one of the standard buffer sizes and it's suitable for transferring small files. Though, you can increase the buffer size if you want to. If you're doing a complex task regarding data transfer or transferring very large files, you may wanna do some testing and analyze the most suitable buffer size that your program can take advantage of.

ByteArrayInputStream and ByteArrayOutputStream

ByteArrayInputStream contains an internal buffer that contains bytes that may be read from the stream. ByteArrayOutputStream implements an output stream in which the data is written into a byte array. The buffer automatically grows as data is written to it. These two streams are sometimes used in conjunction with other streams.
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

public class SampleClass{
  
  public static void main(String[]args)
                     throws IOException{
    
    ByteArrayInputStream bai = null;
    ByteArrayOutputStream bao = null;
    try{
      
      byte[] buf = {96,97,98,99,100};
  
      //ByteArrayInputStream that takes
      //a byte array as constructor's
      //argument. the argument is used 
      //as inputstream's internal buffer
      bai = new ByteArrayInputStream(buf);
  
      int num = 0;
      System.out.println("ByteArrayInputStream");
      while((num = bai.read()) != -1)
        System.out.print((char)num);
      System.out.println("\n");
  
      //ByteArrayOutputStream
      bao = new ByteArrayOutputStream();
      //write some bytes into the
      //outputstream internal buffer
      for(int i = buf.length; i > 0; i--)
        bao.write(buf[i-1]);
      
      //get the bytes from the outpustream's
      //internal buffer
      byte[] bytes = bao.toByteArray();
      
      System.out.println("ByteArrayOutputStream");
      for(byte b : bytes)
        System.out.print((char)b);
      System.out.println("\n");
  
    }
    finally{
      if(bai != null)
        bai.close();
      if(bao != null)
        bao.close();
    }
    
  }
}

Result
ByteArrayInputStream
'abcd

ByteArrayOutputStream
dcba'
InputStreamReader and OutputStreamWriter

InputStreamReader is a bridge from byte streams to character streams: It reads bytes and decodes them into characters using a specified charset. the default charset may be used if there's no specified charset.

On the other hand, OutputStreamWriter is also a bridge from byte streams to character streams that encodes characters into bytes using a specified charset. Default charset may be used if there's no specified charset.

Note: For top efficiency, consider wrapping an InputStreamReader/OutputStreamWriter within a BufferedReader/BufferedWriter. For example:
BufferedReader in = new BufferedReader(new InputStreamReader(anInputStream));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(anOutputStream));

This example demonstrates InputStreamReader and OutputStreamWriter.
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;

public class SampleClass{

  public static void main(String[] args)
  throws IOException{
  
    byte[] bytes = {97,98,99,100,101};
    byte[] encodeChar = null;
    String decoded = "";
    String orig = "";
    String encoded = "";
    
    try(InputStreamReader isr =
        new InputStreamReader(
              new ByteArrayInputStream(bytes),
              "UTF-16");
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(bao)){
              
        int num = 0;
        while((num = isr.read()) != -1){
          decoded += String.valueOf(num);
          decoded += " ";
          osw.write(num);
        }
        //Flush the OutputStreamWriter so
        //the bytes will be written into
        //the specified OutputStream.
        osw.flush();
        
        encodeChar = bao.toByteArray();
        
        for(byte b : bytes){
          orig += String.valueOf(b);
          orig += " ";
        }
        
        for(byte b: encodeChar){
          encoded += String.valueOf(b);
          encoded += " ";
        }
        
        System.out.println("Default charset: " +
                           Charset.defaultCharset().
                           displayName());
        System.out.println("Original bytes: " + orig);
        System.out.println("Decoded into "+
                           isr.getEncoding()+": "+
                           decoded);
        System.out.println("Encoded into "+
                           osw.getEncoding()+": "+
                           encoded);
    }
    
  }
}

Result(Note: Result may vary)
Default charset: windows-1252
Original bytes: 97 98 99 100 101
Decoded into UTF-16: 25185 25699 65533
Encoded into cp1252: 63 63 63
FileReader and FileWriter

FileReader reads text from a file and FileWriter writes text to a file. They use specified charset to decode and encode. Otherwise, they may use the default charset if a charset is not specified.
import java.io.File;
import java.io.IOException;
import java.io.FileReader;
import java.io.FileWriter;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File source = new File("C:\\test\\readTest.txt");
    File target = new File("C:\\test\\writeTest.txt");
    try(FileReader fr = new FileReader(source);
        FileWriter fw = new FileWriter(target)){
        
        int num = 0;
        while((num = fr.read()) != -1)
          fw.write((char)num + "\r\n");
    }
    System.out.println("Done!");
    
  }
  
}
CharArrayReader and CharArrayWriter

CharArrayReader is used for reading characters from a char array type whereas CharArrayWriter is used for writing characters to a char array type.
import java.io.IOException;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
    char[] input = {'a','b','c','d'};
    char[] output = null;
    
    CharArrayReader car = null;
    CharArrayWriter caw = null;
    
    try{
      car = new CharArrayReader(input);
      caw = new CharArrayWriter();
      
      int num = 0;
      System.out.println("Input");
      while((num = car.read()) != -1){
        System.out.print((char)num);
        caw.write(num + 1);
      }
      System.out.println();
      
      System.out.println("Output");
      System.out.println(caw.toString());
      
    }
    catch(Exception e){
      e.printStackTrace();
    }
    finally{
      if(car != null)
        car.close();
      if(caw != null)
        caw.close();
    }
    
  }
}

Result
Input
abcd
Output
bcde
BufferedReader and BufferedWriter

BufferedReader reads characters from another stream whereas BufferedWriterwrites bytes to another stream. They have internal buffers and the buffer sizes can be adjusted. The default size(8192 or 8kb) may be used if the buffers sizes are not specified. Wrapping character-based streams with this buffered streams increases the efficiency of read/write operations.

Streams like InputStreamReader, OutputStreamWriter, FileReader and FileWriter benefit from these character-based buffered streams. These character-based streams may hog our system resources. Wrapping them around character-based buffered streams will make them operate efficiently.
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File source = new File("C:\\test\\readTest.txt");
    File target = new File("C:\\test\\writeTest.txt");
    try(BufferedReader reader = new BufferedReader(
                                new FileReader(source));
        BufferedWriter writer = new BufferedWriter(
                                new FileWriter(target))){
        String str = "";
        
        //readLine() is a bufferedReader's method that
        //reads a line of text.
        while((str = reader.readLine()) != null){
          //write the data to the other text file
          writer.write(str);
          //create newline
          writer.newLine();
        }
        
        System.out.println("Done!");
    }
  }
}
BufferedInputStream and BufferedOutputStream

BufferedInputStream reads bytes from another stream whereas BufferedOutputStream write bytes to another stream. These streams add functionalities to other byte-based streams. Also, BufferedInputStream and BufferedOutputStream have internal buffer.
import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File source = new File("C:\\test\\img.png");
    File target = new File("C:\\test\\imgCopy.png");
    try(BufferedInputStream bis = new BufferedInputStream(
                                  new FileInputStream(source));
        BufferedOutputStream bos = new BufferedOutputStream(
                                  new FileOutputStream(target))){
        int num = 0;
        while((num = bis.read()) != -1)
          bos.write(num);
        System.out.println("Done!");
    }
    
  }
}
PrintStream and PrintWriter

PrintStream and PrintWriter add new functionalities to other output streams. For byte-based output streams, use PrintStream. For character-based output streams, use PrintWriter.

This example demonstrates PrintStream.
import java.io.File;
import java.io.IOException;
import java.io.CharArrayReader;
import java.io.FileOutputStream;
import java.io.PrintStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File target = new File("C:\\test\\writeTest.txt");
    char[] characters = {'!','a','~','c','i'};
    try(CharArrayReader car = 
        new CharArrayReader(characters);
        PrintStream ps = 
        new PrintStream(
            new FileOutputStream(target),
            true,
            "UTF-16")){
        
        int num = 0;
        while((num = car.read()) != -1)
          ps.println(num); 
    }
    System.out.println("Done!");
    
  }
}
This example demonstrates PrintWriter.
import java.io.File;
import java.io.IOException;
import java.io.CharArrayReader;
import java.io.FileWriter;
import java.io.PrintWriter;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File target = new File("C:\\test\\writeTest.txt");
    char[] characters = {'!','a','~','c','i'};
    try(CharArrayReader car = 
        new CharArrayReader(characters);
        PrintWriter ps = 
        new PrintWriter(
            new FileWriter(target),
            true)){
        
        int num = 0;
        while((num = car.read()) != -1)
          ps.println((char)num); 
    }
    System.out.println("Done!");
    
  }
}
PipedInputStream and PipedOutputStream

PipedInputStream and PipedOutputStream should be connected with each other and they should operate on different threads. It's not recommended for them to operate on the same thread as it may cause deadlock. A pipe stream is said to be broken if a thread where the pipe stream is working on is not alive. We use PipedOutputStream to write bytes into the pipe and we use PipedInputStream to read the written bytes from the pipe.
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class SampleClass{
  
  public static void main(String[] args)
                     throws IOException{
  
    PipedOutputStream pos = new PipedOutputStream();
    PipedInputStream pis = new PipedInputStream(pos);
    
    Thread t1 = new Thread(() -> {
      
      try{
        
        for(int i = 0; i < 5; i++){
          pos.write(97 + i);
          Thread.sleep(50);
        }
        pos.close();
      }
      catch(Exception e){
        e.printStackTrace();
      }
      
    });
    
    Thread t2 = new Thread(() -> {
      try{
        
        int num = 0;
        while((num = pis.read()) != -1){
          System.out.print(num + " ");
          Thread.sleep(50);
        }
        pis.close();
      }
      catch(Exception e){
        e.printStackTrace();
      }
      
    });
    
    t1.start();
    t2.start();
    
  }
}

Result
97 98 99 100 101
PipedReader and PipedWriter

PipedReader and PipedWriter are the character-based stream versions of PipedInputStream and PipedOutputStream.
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class SampleClass{
  
  public static void main(String[] args)
                     throws IOException{
  
    PipedWriter pw = new PipedWriter();
    PipedReader pr = new PipedReader(pw);
    
    Thread t1 = new Thread(() -> {
      
      try{
        
        for(int i = 0; i < 5; i++){
          pw.write(97 + i);
          Thread.sleep(50);
        }
        pw.close();
      }
      catch(Exception e){
        e.printStackTrace();
      }
      
    });
    
    Thread t2 = new Thread(() -> {
      try{
        
        int num = 0;
        while((num = pr.read()) != -1){
          System.out.print((char)num);
          System.out.print(" ");
          Thread.sleep(50);
        }
        pr.close();
      }
      catch(Exception e){
        e.printStackTrace();
      }
      
    });
    
    t1.start();
    t2.start();
    
  }
}

Result
a b c d e
PushBackInputStream and PushBackReader

A PushbackInputStream adds functionality to another input stream, namely the ability to "push back" or "unread" bytes, by storing pushed-back bytes in an internal buffer.
import java.io.IOException;
import java.io.PushbackInputStream;
import java.io.ByteArrayInputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
    byte[] bytes = {75,10,25,30,45,55,65,80};
    byte[] byteMod = new byte[bytes.length];
    
    try(ByteArrayInputStream bai = 
        new ByteArrayInputStream(bytes);
        PushbackInputStream pis =
        new PushbackInputStream(bai)){
        
        int num = 0;
        int i = 0;
        while((num = pis.read()) != -1){
          
          if((num % 5) == 5){
             byteMod[i] = (byte)(num - 5);
             i++;
             
             //read one step ahead
             num = pis.read();
             if(num != -1){
               if((num % 5) == 0){
                 byteMod[i] = (byte)(num + 15);
                 i++;
               }
               else{
                 //push back the data that
                 //has been read
                 pis.unread(num);
               }
             }
               
          }
          else if((num % 5) == 0){
            byteMod[i] = (byte)(num + 5);
            i++;
          }
            
        }
        
        System.out.println("Original Bytes");
        for(byte b : bytes)
          System.out.print(b + " ");
        
        System.out.println("\n");
        System.out.println("Modified Bytes");
        for(byte b : byteMod)
          System.out.print(b + " ");
    }
  }
}

Result
Original Bytes
75 10 25 30 45 55 65 80

Modified Bytes
80 15 30 35 50 60 70 85
PushBackReader is the character-based stream version of PushbackInputStream.
import java.io.IOException;
import java.io.PushbackReader;
import java.io.CharArrayReader;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
    char[] chars = {'b','a','b','b','a','g','e'};
    StringBuilder sb = new StringBuilder();
    
    try(CharArrayReader car = 
        new CharArrayReader(chars);
        PushbackReader pr =
        new PushbackReader(car)){
        
        int num = 0;
        while((num = pr.read()) != -1){
          
          if((char)num == 'b' ){
             char temp = (char)num;
             
             //read one step ahead
             num = pr.read();
             if(num != -1){
               if((char)num == 'b'){
                 sb.append("gg");
               }
               else{
                 sb.append(temp);
                 //push back the data that
                 //has been read
                 pr.unread(num);
               }
             }
               
          }
          else sb.append((char)num);
          
        }
        
        System.out.println("Original Characters");
        for(char c : chars)
          System.out.print(c);
        System.out.println("\n");
        
        System.out.println("Modified Characters");
        System.out.println(sb.toString());
    }
  }
}

Result
Original Characters
babbage

Modified Characters
baggage
RandomAccessFile

Instances of RandomAccessFile class support both reading and writing to a random access file. A random access file behaves like a large array of bytes stored in the file system. This class has file pointer. This pointer points to an index of the array of bytes in the file system. The pointer starts at the beginning of the array and move past the bytes read until an EOF(End Of File) has been reached.

Instantiating a RandomAccessFile requires a mode that determines the operation of the instance. These are the modes that we can apply:
  • "r" - Open for reading only. Invoking any of the write methods of the resulting object will cause an IOException to be thrown.
  • "rw" - Open for reading and writing. If the file does not already exist then an attempt will be made to create it.
  • "rws" - Open for reading and writing, as with "rw", and also require that every update to the file's content or metadata be written synchronously to the underlying storage device.
  • "rwd" - Open for reading and writing, as with "rw", and also require that every update to the file's content be written synchronously to the underlying storage device.
My explanation here is simplified. Check RandomAccessFile for more information.
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Random;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File file = new File("C:\\test\\fileTest.txt");
    try(RandomAccessFile raf = 
        new RandomAccessFile(file,"rw")){
        
        //In a simple text file,
        //file length(in bytes) corresponds to the
        //number of characters in that file
        long ln = raf.length();
        System.out.println("File length: " + ln);
        
        char[] vowels = {'a','e','i','o','u'};
        Random rand = new Random();
        
        //reading and writing random bytes from the file
        for(int i = 0; i < 5; i++){
          raf.seek(rand.nextInt((int)ln));
          System.out.println((char)raf.read() + " at index: "
                             + " " + (raf.getFilePointer()-1));
          
          //after a call to read() or write(), file pointer
          //will past the index where a byte is read/written.
          //To point the file pointer back to the previous
          //index we need to move it back by one step.
          //Thus, there's -1 
          //in this expression: raf.getFilePointer()-1
          raf.seek(raf.getFilePointer()-1);
          
          char replacement = vowels[rand.nextInt(vowels.length)];
          raf.write(replacement);
          System.out.println("Replaced by: " + replacement);
          System.out.println();
          
        }
        
    }
  }
}
seek() method repositions the file pointer at the specified index. getFilePointer() returns the index where the file pointer is pointing. The example above replaces any characters in the file with vowels.

Create Dummy File Using RandomAccessFile

We can create a dummy file. The dummy file that we're going to create is a file that has gibberish bytes in it.
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File file = new File("C:\\test\\dummy.dum");
    
    //In binary
    //1024 bytes = 1 kilobyte
    //1024 kilobytes = 1 megabyte
    //In decimal
    //1000 bytes = 1 kilobyte
    //1000 kilobytes = 1 megabyte
    //Windows uses the binary size
    //I'm not sure if there are operating
    //systems that use the decimal size
    long dummySize = 1024L * (1024L * 4L);
    try(RandomAccessFile raf = 
        new RandomAccessFile(file,"rw")){
        
        //This method sets the total length
        //of the file that we're going to
        //create. Undefined bytes are added
        //in the process
        raf.setLength(dummySize);
    }
    System.out.println("dummy file created!");
  }
}
SequenceInputStream

SequenceInputStream is a stream that uses multiple input streams as its source of data. It reads from the first one until it reaches EOF(End Of File), then reads from the second one and so on.
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.SequenceInputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    FileInputStream fileOne = 
    new FileInputStream("C:\\test\\test1.txt");
    
    FileInputStream fileTwo =
    new FileInputStream("C:\\test\\test2.txt");
    
    FileOutputStream target = 
    new FileOutputStream("C:\\test\\output.txt");
    
    SequenceInputStream sis = 
    new SequenceInputStream(fileOne, fileTwo);
    
    
    try{
      
      int num = 0;
      while((num = sis.read()) != -1)
        target.write((char)num);
    }
    finally{
      fileOne.close();
      fileTwo.close();
      target.close();
      sis.close();
    }
    System.out.println("Done!");
    
  }
}
To read more than two files, we need to use the Enumeration interface.
import java.util.Vector;
import java.util.Enumeration;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.SequenceInputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    FileInputStream fileOne = 
    new FileInputStream("C:\\test\\test1.txt");
    
    FileInputStream fileTwo =
    new FileInputStream("C:\\test\\test2.txt");
    
    FileInputStream fileThree =
    new FileInputStream("C:\\test\\test3.txt");
    
    FileOutputStream target = 
    new FileOutputStream("C:\\test\\output.txt");
    
    Vector<InputStream> streams = 
    new Vector<>();
    
    streams.add(fileOne);
    streams.add(fileTwo);
    streams.add(fileThree);
    
    Enumeration<InputStream> e =
    streams.elements();
    
    SequenceInputStream sis = 
    new SequenceInputStream(e);
    
    try{
      
      int num = 0;
      while((num = sis.read()) != -1)
        target.write((char)num);
    }
    finally{
      fileOne.close();
      fileTwo.close();
      target.close();
      sis.close();
    }
    System.out.println("Done!");
    
  }
}
StringReader and StringWriter

StringReader is used to read from a String object and StringWriter is used to write to a String object.
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
    String input = "I am a String!";
    String output = null;
    
    StringReader reader = new StringReader(input);
    StringWriter writer = new StringWriter();
    
    try{
    
      int num = 0;
      
      //read/write string input
      while((num = reader.read()) != -1)
        writer.write(num);
      
      //get the written strings written
      //to the writer
      output = writer.toString();
    }
    finally{
      reader.close();
      writer.close();
    }
    System.out.println("Input: " + input);
    System.out.println("Output: " + output);
    
  }
}

Result
Input: I am a String!
Output: I am a String!
Console

Console class can access any character-based console device, if any, associated with the current Java virtual machine. This class provides a functionality to read password that is encrypted on the console screen.
import java.io.Console;

public class SampleClass{

  public static void main(String[] args){
    
    //System.console) returns the 
    //console associated with the
    //jvm
    Console c = System.console();
    
    if(c != null){
      char[] pass = null;
      
      System.out.println("Console Test!");
      System.out.println();
      
      System.out.print("Enter name: ");
      String name = c.readLine();
      System.out.println("name: " + name);
      System.out.println();
      
      System.out.print("Enter password");
      if((pass = c.readPassword())
          != null){
          StringBuilder sb = 
          new StringBuilder(String.valueOf(pass));
          
          //java documentation recommends 
          //manually removing characters
          //in the array where the inputted
          //sensitive data is stored to
          //minimize the lifetime of the
          //sensitive data
          java.util.Arrays.fill(pass, ' ');
          
          System.out.print("Password: ");
          System.out.println(sb.toString());
          
          //delete the characters in sb
          //StringBuilder after you're done
          //using it to minimize the lifetime
          //of the sensitive data
          sb.setLength(0);
      }
    }
    
  }
  
}
StreamTokenizer

The StreamTokenizer class takes an input stream and parses it into "tokens", allowing the tokens to be read one at a time. The parsing process is controlled by a table and a number of flags that can be set to various states. The stream tokenizer can recognize identifiers, numbers, quoted strings, and various comment styles.

These fields are important for us to understand.
  • nval - If the current token is a number, this field contains the value of that number.
  • sval - If the current token is a word token, this field contains a string giving the characters of the word token.
  • TT_EOF - A constant indicating that the end of the stream has been read.
  • TT_EOL - A constant indicating that the end of the line has been read.
  • TT_NUMBER - A constant indicating that a number token has been read.
  • TT_WORD - A constant indicating that a word token has been read.
  • ttype - After a call to the nextToken method, this field contains the type of the token just read.
This example demonstrate StreamTokenizer and the usage of the fields above.
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.FileReader;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
  
     try(FileReader reader =
         new FileReader("C:\\test\\test.txt")){
         
         StreamTokenizer tokenizer =
         new StreamTokenizer(reader);
         
         int currToken = tokenizer.nextToken();
         
         //if the current token is equal to TT_EOF
         //then our program reaches the end of stream
         //no characters are available to be read
         //anymore.
         while(currToken != StreamTokenizer.TT_EOF){
           
           //After nextToken() is invoked, ttype will 
           //hold a value that determines the type
           //of the token that we got from the 
           //invocation of nextToken()
           if(tokenizer.ttype == StreamTokenizer.TT_NUMBER){
             //nval field holds the number value of the
             //current token
             System.out.println("number: " + tokenizer.nval);
           }
           else if(tokenizer.ttype == StreamTokenizer.TT_WORD){
             //sval field holds the string value of the 
             //current token if the curren token is
             //considered as a word
             System.out.println("word: " + tokenizer.sval);
           }
           else{
             //if ttype is not a number or a word then, 
             //current token is just an ordinary character
             System.out.println("character: " + 
                                (char)currToken);
           }
           
           currToken = tokenizer.nextToken();
         }
     }
  }
}
Content of test.txt
This is a t3st!
3 Two 1 Zero

Result
word: This
word: is
word: a
word: t3st
character: !
number: 3.0
word: Two
number: 1.0
word: Zero

Now, change the content of test.txt to this
"This" is a t3st!
And the result would be
character: "
word: is
word: a
word: t3st
character: !

Where's the word "This"? you might ask. Streamtokenizer considers words that are wrapped around quotes(single or double) as String. To get that "This" word, change this code snippet
else if(tokenizer.ttype == StreamTokenizer.TT_WORD){
  //sval field holds the string value of the 
  //current token if the curren token is
  //considered as a word
  System.out.println("word: " + tokenizer.sval);
}
To this
else if(tokenizer.ttype == StreamTokenizer.TT_WORD ||
        tokenizer.ttype == '\'' ||
        tokenizer.ttype == '"'){
  //sval field holds the string value of the 
  //current token if the curren token is
  //considered as a word
  System.out.println("word: " + tokenizer.sval);
}
And the result would be
word: This
word: is
word: a
word: t3st
character: !

Not all characters are considered as "ordinary" characters by the StreamTokenizer. To make StreamTokenizer recognize those characters, we need to manually specify them as ordinary characters by using the ordinaryChar() method. You can call this method after instantiating a StreamTokenizer object.
Example
StreamTokenizer tokenizer = new StreamTokenizer(reader);
tokenizer.ordinaryChar('/');

In addition, an instance has four flags. These flags indicate:
  • Whether line terminators are to be returned as tokens or treated as white space that merely separates tokens.
  • Whether C-style comments are to be recognized and skipped.
  • Whether C++-style comments are to be recognized and skipped.
  • Whether the characters of identifiers are converted to lowercase.
StreamTokenizer recognizes C++ and C comment style. StreamTokenizer skips those comments if it sees one. For example, when tokenizer recognizes the "\\" or the C++ comment style, tokenizer will skip text past "\\" until our program moves on to the next line. when tokenizer recognizes the "/* */" or the C comment style, tokenizer will skip all the characters within "/* */".

By default, tokenizer ignores those comments. Though, we can enable flags to make tokenizer recognize C++ and C comments by calling the slashSlashComments() for C++ comment style and slashStarComments() for C comment style.

Invoke lowerCaseMode() method to enable/disable a flag that determines whether or not word token are automatically lowercased. Call eolIsSignificant() method to enable/disable a flag that determines whether or not ends of line are treated as tokens.

DataInputStream and DataOutputStream

DataInputStream reads primitive data types that are written by DataOutputStream. DataOutputStream writes primitive data types for DataInputStream to read. DataInputStream reads written data in machine-independent way. Thus, a java application from different machine can use DataInputStream to read the written data that we have written in our machine.
import java.io.File;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    File file = new File("C:\\test\\test.dat");
    try(FileInputStream fis =
        new FileInputStream(file);
        DataInputStream das = 
        new DataInputStream(fis);
        FileOutputStream fos =
        new FileOutputStream(file);
        DataOutputStream dos =
        new DataOutputStream(fos)){
        
        float[] numbers = {2.5f,4.2f,6.6f,
                           8.1f,10.0f};
        boolean bool = true;
        
        //Write primitive types
        dos.writeInt(numbers.length);
        dos.writeBoolean(bool);
        for(int i = 0; i < numbers.length; i++)
          dos.writeFloat(numbers[i]);
        
        //Read primitive types
        //read the source data according to
        //the order of how the source data
        //have been written
        int ln = das.readInt();
        
        System.out.println("int: " + ln);
        System.out.println("boolean: " + das.readBoolean());
        System.out.print("floats: ");
        for(int i = 0; i < ln; i++)
          System.out.print(das.readFloat() + " ");
        
    }
  }
}

Result
int: 5
boolean: true
floats: 2.5 4.2 6.6 8.1 10.0
ObjectOutputStream and ObjectInputStream

ObjectOutputStream and ObjectInputStream are used for serializing/deserializing objects. The usage of these two classes are demonstrated in the Serialization and Deserialization topic.