Often, our programs will need to use external resources. These could include:
In this module, we will first look at some simple usage of resource files in Java.
Second, we will look at some basic File I/O in Java for reading and writing to Plain Text files (like .txt or .csv).
Often, when making a program or website, we will need assets. These assets could include images, like this picture of my cat Chloe sitting on my shoulder despite my wishes that she not:
If I wanted to make this a company logo (and who wouldn’t), then this image would likely appear throughout any app I made. And so, I need to store that image with the app’s project code-base. And, in fact, the above picture is in this website’s code base.
Of course, this website is largely built from markdown files, not .java files. So the way I store and utilize the image in a Java project is a little different.
As we don’t want to get into GUIs just yet, let’s start simple.
Let’s make a simple text file in IntelliJ. I’m going to use my project from the IntelliJ lecture.
First, I open up a text editor and write the file contents:
A Haiku for my cat:
Chloe, calico,
Why do you stand on shoulders?
I'm trying to work!
Now, wherever I saved that file, I’m going to copy and paste (or click and drag)
the file into my project’s src/main/resources
folder:
The following code is a complete program to open and read a resource file:
import java.io.*;
public class FileResourceExample {
private static final String RESOURCE_FILENAME = "chloe.txt";
public static void main(String[] args) {
FileResourceExample example = new FileResourceExample();
example.run();
}
public void run() {
try {
InputStream inputStream = getResourceAsInputStream(RESOURCE_FILENAME);
BufferedReader bufferedReader = getBufferedReaderForInputStream(inputStream);
printBufferedReaderContents(bufferedReader);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private InputStream getResourceAsInputStream(String resourceFilename) throws FileNotFoundException {
ClassLoader loader = FileResourceExample.class.getClassLoader();
InputStream stream = loader.getResourceAsStream(resourceFilename);
if (stream == null) {
throw new FileNotFoundException("Resource file: " + resourceFilename + " is not found.");
}
return stream;
}
private BufferedReader getBufferedReaderForInputStream(InputStream inputStream) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
return new BufferedReader(inputStreamReader);
}
private void printBufferedReaderContents(BufferedReader bufferedReader) throws IOException {
String currentLine = bufferedReader.readLine();
while(currentLine != null) {
System.out.println(currentLine);
currentLine = bufferedReader.readLine();
}
}
}
Note that I broke up the above code into several functions so that we can focus on each step individually. It is not necessary to write this as several functions.
Now that we have this resource, we can access it using the following function:
private InputStream getResourceAsInputStream(String resourceFilename) throws FileNotFoundException {
ClassLoader loader = FileResourceExample.class.getClassLoader();
InputStream stream = loader.getResourceAsStream(resourceFilename);
if (stream == null) {
throw new FileNotFoundException("Resource file: " + resourceFilename + " is not found.");
}
return stream;
}
Specifically, these lines here give us an InputStream
that accesses the “haiku.txt” file (in this case, stored in the variable resourceFilename
)
ClassLoader loader = FileResourceExample.class.getClassLoader();
InputStream stream = loader.getResourceAsStream(resourceFilename);
You’ll notice the filename is on the second line. We start by getting a ClassLoader
- the ClassLoader is a way to load resources from our project, including our “haiku.txt” resource file. Using the getResourceAsStream(resourceFilename)
function, we get an InputStream from our file.
You’ll notice we have some extra code here:
if (stream == null) {
throw new FileNotFoundException("Resource file: " + resourceFilename + " was not found.");
}
return stream;
It’s important to be aware that getResourceAsStream
*does not throw FileNotFoundException
if the resource file isn’t found. Rather, it simply returns a null
InputStream. Personally, I think this is bad design, but nobody at Java asked me (which is probably a good idea). But this *does mean you need to do a null check on your InputStream, or be prepared to handle a `NullPointerException.
This gives us the String variable inputStream
which we can then use to read via a BufferedReader
as follows:
private BufferedReader getBufferedReaderForInputStream(InputStream inputStream) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
return new BufferedReader(inputStreamReader);
}
Note that you may often see people combine these two lines:
return new BufferedReader(new InputStreamReader(filename));
However, we’ll discuss later how this could be seen as bad style in many contexts because it makes the code harder to read and understand if you use this nest constructor call approach. That said, it’s common enough for these “repetitive” file IO situations that we tend to overlook it.
Now, we can simply read the file via a BufferedReader. We cover reading from a BuffedReader below.
A BufferedReader has one key method: readLine()
null
One simple use-case of a BufferedReader is to print the contents of the file. One way to print the contents of a file is:
String currentLine = bufferedReader.readLine();
while(currentLine != null) {
System.out.println(currentLine);
currentLine = bufferedReader.readLine();
}
You may often see this using a for
instead.
for (String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) {
System.out.println(line);
}
I generally recommend using a for
loop over a while
loop whenever feasible, as it’s much harder to accidentally create an infinite loop with a for
loop.
Of course, you don’t have to print. For example, if you are reading a csv file, you may split the line into pieces by commas and use the file contents to create or populate a list of data structures. Or any number of other things you could do to parse a file.
Another approach for reading a file is to use Files.lines()
, which is a functional programming approach. We will discuss this when going over functional programming and lambda bodies, later on. However, to show you what it would look like:
bufferedReader.lines()
.forEach(line -> System.out.println(line));
Remember that our source folder has both a main
and test
folder. In general,
any resources in main
are accessible by either main
or test
. Resources in
test
are only accessibly by classes in test
.
For example, let’s say you write a class that reads a CSV file and produces
an ArrayList of some class. You can include an example csv file in the
test
resources to test with, but that file won’t be confused as part of the
main
resources of the project.
When we discuss testing in that unit, it’s worth noting that our test classes will be able to access both test
and main
resources, while our main
classes will not be able to access test
resources.
If you program can take in a filename from the user, such as via command-line, text entry, file browser window, etc., you should never that file with ClassLoader
. In that case, it’s sufficient to just create a BufferedReader
from the filename the user gives you.
Something like:
public ArrayList<String> getLinesFromFile(String filename) {
try {
FileReader fileReader = new FileReader(filename);
BufferedReader bufferedReader = new BufferedReader(fileReader);
ArrayList<String> lines = new ArrayList<>();
String line = bufferedReader.readLine();
while (line != null) {
lines.add(line);
line = bufferedReader.readLine();
}
return lines;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
You only use Resources when you are trying to access a resource file that you would include in a distribution of the project.
Consider the puzzle game (Picross S)[https://www.youtube.com/watch?v=ph0ADToB-1c], which involves solving puzzles called nonograms [https://en.wikipedia.org/wiki/Nonogram].
Image Source: Wikipedia
If you are a programmer of the game Picross S, which seems like the better approach for development?
1) Create a Java class for every single puzzle that describes how the puzzle works and hard-code what the data values are for each row/column 2) Describe a generic puzzle class that works for all puzzles, and store the data of each row and column as a file.
Saw we wanted to implement 300 puzzles. In the first case, we would need at least 300 classes. In the second case, we’d need probably two classes (one to read in the file, and the other to model the state of the puzzle) and 300 resource files. These files might be formatted something like (using the puzzle above):
width: 30
height: 20
rows:
8, 7, 5, 7
5, 4, 3, 3
3, 3, 2, 3
...
columns:
1
1
2
...
Again, this is hypothetical. (As a note, If I were implementing this, I would likely use something like JSON, rather than the text above, but that’s beyond what we’ve covered so far). The point, however, is that this allows us to simply create 300 puzzle files with a consistent, human-readable format. This lets us separate how the game works from the individual puzzles in the game, and allows them to be edited separately from the source code.
Resource files, such as images, configurations, data, etc. are a vital tool in software development. Learning how to store and utilize resource files appropriately is a skill worth learning. For with that skill, you can stand on mountaintops…
Or at least on the top of scratching posts.