Java IO - Fish-In-A-Suit/Conquest GitHub Wiki

Files and Streams

File object

  • Creation: via constructor File(String pathname), which creates a new File instance by converting the given pathname string into an abstract pathname
  • Check for existence: via method public boolean exists(): file.exists()
  • Check if it represents a directory: public boolean isDirectory(): file.isDirectory()

Writing to a file

Writing to a file can be acheieved through an OutputStream instance and using write(byte[] b) method, which writes b.length bytes from the specified byte array to that output stream. Furthermore, a FileOutputStream (direct subclass of OutputStream) instance has to be used, since we will be writing to a file.

A FileOutputStream can be created with the following constructors:

  • FileOutputStream(File file), which creates a file output stream to write to the file represented by the specified File object
  • FileOutputStream(String name), which creates a file output stream to write to the file with the specified name (path)

This instance (of FileOutputStream) can be created like this (filePath is a String representing the file path to which we wish to write):

OutputStream outStream = new FileOutputStream(filePath);

Then, having created the output stream for the specified file, we have to write to it. For this, we use the write(byte[] bytes) method, where bytes.length of bytes from the specified byte array are written to the outputstream on which this method is called. To get a byte array from a String variable, call the getBytes() method on it, which transforms the String into an array of bytes:

outStream.write(text.getBytes());

text represents the String variable which we wish to write to the file.

Whenever we finish writing to a file (using an output stream), we need to close it, so as to release any system resources associated with that stream. Make sure to call the void close() method!

outStream.close();

Here is an example of a method which writes text (text parameter) to a specified file (which is accessed as thorugh it's string path - path parameter):

	public static void writeToFile(String filePath, String text) throws IOException {
		OutputStream outStream = new FileOutputStream(filePath);
		outStream.write(text.getBytes());
		outStream.close();
	}

Using InputStream

The InputStream class is the base class (superclass) of all input streams in the Java IO API. InputStream subclasses include the FileInputStream, BufferedInputStream and the PushbackInputStream among others. It's used for reading byte based data (one byte at a time).

Example:

InputStream inputstream = new FileInputStream("c:\\data\\input-text.txt");

int data = inputstream.read();
while(data != -1) {
  //do something with data...
  doSomethingWithData(data);

  data = inputstream.read();
}
inputstream.close();

Using FileReader to read the contents of a file

The FileReader class (java.io.FileReader) makes it possible to read the contents of a file as a stream of characters. It works much like the FileInputStream except the FileInputStream reads bytes, whereas the FileReader reads characters. The FileReader is intended to read text, in other words. One character may correspond to one or more bytes depending on the character encoding scheme.

Creation:

  • FileReader(File file), which creates a new FileReader, given the File to read from
  • FileReader(String fileName), which creates a new FileReader, given the name of the file to read from

This is an example of a method, which reads a file using FileReader given a file path (filePath). It also displays the time it took to read the contents of the file:

	public static void readFileFilereader(String filePath) throws IOException {
		double start = System.nanoTime();
		FileReader fileReader = new FileReader(filePath);
		
		int data = fileReader.read();
		while(data != -1) {
			System.out.println("[FileUtilities]: (char)data = " + (char)data + ", data = " + data);
			data = fileReader.read();
		}
		fileReader.close();
		double finish = System.nanoTime();
		double operationTime = (finish - start) / 1000_000;
		System.out.println("It took " + operationTime + " milliseconds for the file playerData.txt to be read by using FileReader");
	}

First, a new FileReader instance is created by using a specified filePath. First, a while loop is introduced. int data contains the **int **values of characters which are read by the read() method. When the read() method reaches the end of the file, it returns the value -1. Check data against that -1 value inside the while loop: "While data doesn't equal -1 (when the end of the file is not reached), read from a file. If the end of the file is reached (data takes the value -1), then exit the while loop and close the reader.

To read a single character at a time, the method public int read() throws IOException is used. Each character (a, 5, #, etc) is represented by a specific int value. To convert that int value back to a human-readable character, cast that int value to char: System.out.println("[FileUtilities]: (char)data = " + (char)data + ", data = " + data);.

To close the reader call it's close() method.: fileReader.close()

I've created a .txt file, the content of which is Adrian. Then, I've specified the file path to the above method which uses FileReader. The result is the following:

[FileUtilities]: (char)data = A, data = 65
[FileUtilities]: (char)data = d, data = 100
[FileUtilities]: (char)data = r, data = 114
[FileUtilities]: (char)data = i, data = 105
[FileUtilities]: (char)data = a, data = 97
[FileUtilities]: (char)data = n, data = 110
It took 0.207968 milliseconds for the file playerData.txt to be read by using FileReader

Using a BufferedReader to read from a file

The BufferedReader class (java.io.BufferedReader) provides buffering to your Reader instances. Buffering can speed up IO quite a bit. Rather than read one character at a time from the network or disk, the BufferedReader reads a larger block at a time. This is typically much faster, especially for disk access and larger data amounts.

To add buffering to a Reader instance, simply wrap it in a BufferedReader:

BufferedReader bufferedReader = new BufferedReader(new FileReader("c:\\data\\input-file.txt"));

Then, you can either use a read() or readLine() method to read from a file.

Using BufferedReader with read() method:

	public static void readFileBufferedReader(String filePath) throws IOException {
		double start = System.nanoTime();
		BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
		//String line = bufferedReader.readLine();
		int data = bufferedReader.read();
		while(data != -1) {
			System.out.println("[FileUtilities]: (char)data = " + (char)data + ", data = " + data);
			data = bufferedReader.read();
		}
		bufferedReader.close();
		double finish = System.nanoTime();
		double operationTime = (finish - start) / 1000_000;
		System.out.println("It took " + operationTime + " milliseconds for the file playerData.txt to be read by using FileReader");
		
	}

As above, I've "fed" this method the same file which has Adrian written in it. The output was the following:

[FileUtilities]: (char)data = A, data = 65
[FileUtilities]: (char)data = d, data = 100
[FileUtilities]: (char)data = r, data = 114
[FileUtilities]: (char)data = i, data = 105
[FileUtilities]: (char)data = a, data = 97
[FileUtilities]: (char)data = n, data = 110
It took 0.19315 milliseconds for the file playerData.txt to be read by using FileReader

Now we can see that using the read() method of BufferedReader compared to FileReader is not the same. In the case of BufferedReader, the operational time is 0.19315 milliseconds, while in the case of a FileReader is 0.207968 milliseconds. Therefore, on this example, using the read() method of BufferedReader is for 0.016607 milliseconds faster. Given that it has already accumulated such an advantage by only reading one word, we can see that BufferedReader is the go-to for reading from files!

This is an example of using the readLine() method of BufferedReader:

	public static void readFileBRReadLine(String filePath) throws IOException {
		double start = System.nanoTime();
		BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
		
		String line = bufferedReader.readLine();
		while (line != null) {
			System.out.println("[FileUtilities]: line = " + line);
			line = bufferedReader.readLine();
		}
		bufferedReader.close();
		double finish = System.nanoTime();
		double operationTime = (finish - start) / 1000_000;
		System.out.println("It took " + operationTime + " milliseconds for the file playerData.txt to be read by using FileReader");
	}

Again, I've fed the same file to the method and the result was:

[FileUtilities]: line = Adrian
It took 0.115737 milliseconds for the file playerData.txt to be read by using FileReader

In comparison to the above methods (which used FileReader's and Bufferedreader's read() method), the readLine() is the fastest.

Using BufferedReader to read files

How not to use BufferedReader to read files

Constructing a BufferedReader to read files would be by many considered as follows:

InputStream filePathInputStream = loadPathAsInputStream("/shaders/vertexShader.vs/");

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(filePathInputStream));

String code = bufferedReader.readLine();
while(code != null) {
    code = bufferedReader.readLine;
}
bufferedReader.close();

private loadPathAsInputStream() {
    InputStream result = null;

    try {
         result = Class.forName(*THE_NAME_OF_THIS_CLASS_GOES_HERE*.class.getName()).getResourceAsStream(file);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return result;
}

First, this creates an InputStream from the path of the file, which is then used by BufferedReader to read line by line. However, with every loop of the while loop, the code variable, which was supposed to hold all of the code of the file which we want to read when BufferedReader reaches the end of the file, is assigned a new value with every new cycle through the loop. At the end, when BufferedReader reaches the end of the file to be read, variable code is assigned null, which is absolutely what no one wants.

Trying to change the code from code = bufferedReader.readLine; to code += bufferedReader.readLine; doesn't work either.

Using StringBuilder to append lines to a String through every loop.

The above example just changes the content of the final String through every cycle of the loop. Each cycle, it's previous value is deleted and a new value is inserted. This is not what one wants when reading from files. The desired goal is to accumulate (append) new lines to an existing String without deleting it's previous value(s). This can be achieved with StringBuilder.

//Let this class be called FileUtilitites

public static String readFile(String filePath) {
    String line = null;
    StringBuilder stringBuilder = new StringBuilder();
    String lineSeparator = System.getProperty("line.separator");

    InputStream filePathStream = loadPathAsInputStream(filePath);
    
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(filePathStream));

    while((line = bufferedReader.readLine()) != null) {
        stringBuilder.append(line);

        while((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(lineSeparator);
            stringBuilder.append(line);
        }
    }
    bufferedReader.close();
    return stringBuilder.toString();
}

private InputStream loadPathAsInputStream(String inPath) {
    InputStream result = null;
 
    try {
        result = Class.forName(FileUtilities.class.getName()).getResourceAsStream(inPath);
    } catch (ClassNotFoundException e) {
        System.out.println("Couldn't convert file path of file " + path + " to InputStream..." + "\n" + e.printStackTrace());
    }

    return result;
}

The method public static void readFile(String filePath) takes in a path of a file, reads it, stores it's contents inside a String, which it then returns. First, the method-local variables are declared and initialized. Take note of the lineSeaparator, which is used to insert line breaks after reading each line of a file. Each operating system has it's own specific character to represent a line separator, so it's best to use System.getProperty("line.separator") to acquire the system-specific line separator.

Then, the private filePath parameter is sent to the InputStream loadPathAsInputStream(String inPath) method, which creates an InputStream connection to the file with that address. Then, this InputStream is used by the first method to create a new BufferedReader instance (which in turn uses InputStreamReader to read the bytes from the file which is represented by InputStream).

Now all there is to do is to read the file! This is done with a nested while loop. The condition we are checking is if BufferedReader ever reads the end of the file (a line which bufferedReader object reads is represented as String variable line). In this case, it returns null --> check when that occurs as the condition for the while loop: while ((line = bufferedReader.readLine()) != null).

In the first section of the while loop, the first line of the file is read. Then, it makes sense that the line has to be broken, so the new call to readLine() starts in a new line and not in the same as the previous call. This would look like so:

while ((line = bufferedReader.readLine()) != null) {
    stringBuilder.append(line);
    stringBuilder.append(lineSeparator);
}

This is not satisfactory. When the end of the file is reached, no more line breaks should be appended, as this might cause bugs. The above code, however, does just that. When the final line of code is read, it is followed by an appended line separator. To get rid of this issue, create a nested while loop with the same condition as the "parent" while loop, just make it so that the call to separate the lines is made first and the call to append a new line second:

while ((line = bufferedReader.readLine()) != null) {
    stringBuilder.append(line); //this appends the first line
			
    while((line = bufferedReader.readLine()) != null) {
	    stringBuilder.append(lineSeparator);
	    stringBuilder.append(line);
    }			
}

When the final line is reached, no more line breaks are added, which is AWESOME.

References