Chapter 14 I/O Notes Flashcards
Referencing Files and Directories
- I/O - java.io.File class
- NIO.2 - java.nio.file.Path interface.
Conceptualizing the File System
* file
* directory
* file system
* root directory
* path
* file separators
* absolute path
* relative path
* path symbol
* symbolic link
- A
file
within the storage device holds data. Files are organized into hierarchies using directories. - A
directory
is a location that can contain files as well as other directories. - The
file system
is in charge of reading and writing data within a computer. Different operating systems use different file systems to manage their data. -
root directory
is the topmost directory in the file system, ex: Windows C:\ and Linux / - A
path
is a representation of a file or directory within a file system. -
File Separators
, Unix-based systems use the forward slash, /, for paths, whereas Windows-based systems use the backslash, \ - The
absolute path
of a file or directory is the full path from the root directory to the file or directory, - the
relative path
of a file or directory is the path from the current working directory to the file or directory. - A
path symbol
is one of a reserved series of characters with special meaning in some file systems.- . A reference to the current directory
- .. A reference to the parent of the current directory
- A
symbolic link
is a special file within a file system that serves as a reference or pointer to another file or directory.
- I/O APIs do not support symbolic links,
- NIO.2 includes full support for creating, detecting, and navigating symbolic links within the file system.
I/O APIs do not support symbolic links
Operating System File Separators
Java offers a system property to retrieve the local separator character for the current environment:
System.out.print(System.getProperty(“file.separator”));
Creating a File or Path
-
I/O, this is the
java.io.File class
-
NIO.2, it is the
java.nio.file.Path interface
.
Creating a File
File zooFile1 = new File("/home/tiger/data/stripes.txt"); File zooFile2 = new File("/home/tiger", "data/stripes.txt"); File parent = new File("/home/tiger"); File zooFile3 = new File(parent, "data/stripes.txt"); System.out.println(zooFile1.exists());
The File class
is created by calling its constructor
.
This code shows three different constructors:
~~~
File zooFile1 = new File(“/home/tiger/data/stripes.txt”);
File zooFile2 = new File(“/home/tiger”, “data/stripes.txt”);
File parent = new File(“/home/tiger”);
File zooFile3 = new File(parent, “data/stripes.txt”);
System.out.println(zooFile1.exists());
~~~
All three create a File object that points to the same location on disk.
If we passed null as the parent to the final constructor, it would be ignored, and the method would behave the same way as the single String constructor.
For fun, we also show how to tell if the file exists on the file system.
Creating a Path
//Path static method of() Path zooPath1 = Path.of("/home/tiger/data/stripes.txt"); Path zooPath2 = Path.of("/home", "tiger", "data", "stripes.txt"); //Paths factory class static method get() Path zooPath3 = Paths.get("/home/tiger/data/stripes.txt"); Path zooPath4 = Paths.get("/home", "tiger", "data", "stripes.txt"); System.out.println(Files.exists(zooPath1));
- Since Path is an interface, we can’t create an instance directly. After all, interfaces don’t have constructors!
- obtain a Path object is to use a static factory method defined on Path or Paths.
All four of these examples point to the same reference on disk:
~~~
//Path static method of()
Path zooPath1 = Path.of(“/home/tiger/data/stripes.txt”);
Path zooPath2 = Path.of(“/home”, “tiger”, “data”, “stripes.txt”);
//Paths factory class static method get()
Path zooPath3 = Paths.get(“/home/tiger/data/stripes.txt”);
Path zooPath4 = Paths.get(“/home”, “tiger”, “data”, “stripes.txt”);
System.out.println(Files.exists(zooPath1));
~~~
Both methods allow passing a varargs parameter to pass additional path elements. The values are combined and automatically separated by the operating system–dependent file separator. We also show the Files helper class, which can check if the file exists on the file system.
NOTE
> [!NOTE]
both the I/O and NIO.2 classes can interact with a URI.
A uniform resource identifier (URI) is a string of characters that identifies a resource.
It begins with a schema that indicates the resource type, followed by a path value such as file:// for local file systems and http://, https://, and ftp:// for remote file systems.
- both the I/O and NIO.2 classes can interact with a URI.
- A uniform resource identifier (URI) is a string of characters that identifies a resource.
Switching between File and Path
When working with newer applications, you should rely on NIO.2’s Path interface, as it contains a lot more features.
File file = new File("rabbit"); Path nowPath = file.toPath(); File backToFile = nowPath.toFile();
Obtaining a Path from the FileSystems
Class
- The FileSystems class creates instances of the abstract FileSystem class.
- The latter includes methods for working with the file system directly.
- Both Paths.get() and Path.of() are shortcuts for this FileSystem method.
Path zooPath1 = FileSystems.getDefault().getPath("/home/tiger/data/stripes.txt"); Path zooPath2 = FileSystems.getDefault().getPath("/home", "tiger", "data", "stripes.txt");
Reviewing I/O and NIO.2 Relationships
FIGURE 14.3 I/O and NIO.2 class and interface relationships
* FileSystems create FileSystem
* FileSystem create Path
* Paths create Path
* Files uses Path
* Path covert java.net.URI
* Path covert java.io.File
NOTE
> [!NOTE]
The java.io.File is the I/O class,
while Files is an NIO.2 helper class.
Files operates on Path instances, not java.io.File instances.
- java.io.File
- java.io.nio.file.Files NIO.2 helper class
- Files operates on Path instances
TABLE 14.2 Options for creating File and Path
java.io.File
* public File(String pathname)
* public File(File parent, String child)
* public File(String parent, String child)
* public Path toPath()
java.nio.file.Path
* public default File toFile()
* public static Path of(String first, String… more)
* public static Path of(URI uri)
java.nio.file.Paths
* public static Path get(String first, String… more)
* public static Path get(URI uri)
java.nio.file.FileSystem
* public Path getPath(String first, String… more)
java.nio.file.FileSystems
* public static FileSystem getDefault()
Operating on File and Path
Answer
Using Shared Functionality
TABLE 14.3 Common File
and Path
operations
* Gets name of file/directory
* getName()
* getFileName()
* Retrieves parent directory or null if there is none
* getParent()
* getParent()
* Checks if file/directory is absolute path
* isAbsolute()
* isAbsolute()
TABLE 14.4 Common File
and Files
operations
* Deletes file/directory
* delete()
* deleteIfExists(Path p) throws IOException
* Checks if file/directory exists
* exists()
* exists(Path p, LinkOption… o)
* Retrieves absolute path of file/directory
* getAbsolutePath()
* toAbsolutePath()
* Checks if resource is directory
* isDirectory()
* isDirectory(Path p, LinkOption… o)
* Checks if resource is file
* isFile()
* isRegularFile(Path p, LinkOption… o)
* Returns the time the file was last modified
* lastModified()
* getLastModifiedTime(Path p, LinkOption… o) throws IOException
* Retrieves number of bytes in file
* length()
* size(Path p) throws IOException
* Lists contents of directory
* listFiles()
* list(Path p) throws IOException
* Creates directory
* mkdir()
* createDirectory(Path p, FileAttribute… a) throws IOException
* Creates directory including any nonexistent parent directories
* mkdirs()
* createDirectories(Path p, FileAttribute… a) throws IOException
* Renames file/directory denoted
* renameTo(File dest)
* move(Path src, Path dest, CopyOption… o) throws IOException
TABLE 14.3 Common File
and Path
operations
TABLE 14.3 Common File
and Path
operations
* Gets name of file/directory
* getName()
* getFileName()
* Retrieves parent directory or null if there is none
* getParent()
* getParent()
* Checks if file/directory is absolute path
* isAbsolute()
* isAbsolute()
TABLE 14.4 Common File
and Files
operations
TABLE 14.4 Common File
and Files
operations
* Deletes file/directory
* delete()
* deleteIfExists(Path p) throws IOException
* Checks if file/directory exists
* exists()
* exists(Path p, LinkOption… o)
* Retrieves absolute path of file/directory
* getAbsolutePath()
* toAbsolutePath() <–this is Path interface method
* Checks if resource is directory
* isDirectory()
* isDirectory(Path p, LinkOption… o)
* Checks if resource is file
* isFile()
* isRegularFile(Path p, LinkOption… o)
* Returns the time the file was last modified
* lastModified()
* getLastModifiedTime(Path p, LinkOption… o) throws IOException
* Retrieves number of bytes in file
* length()
* size(Path p) throws IOException
* Lists contents of directory
* listFiles()
* list(Path p) throws IOException
* Creates directory
* mkdir()
* createDirectory(Path p, FileAttribute… a) throws IOException
* Creates directory including any nonexistent parent directories
* mkdirs()
* createDirectories(Path p, FileAttribute… a) throws IOException
* Renames file/directory denoted
* renameTo(File dest)
* move(Path src, Path dest, CopyOption… o) throws IOException
I/O API example:
10: public static void io() { 11: var file = new File("C:\\data\\zoo.txt"); 12: if (file.exists()) { 13: System.out.println("Absolute Path: " + file.getAbsolutePath()); 14: System.out.println("Is Directory: " + file.isDirectory()); 15: System.out.println("Parent Path: " + file.getParent()); 16: if (file.isFile()) { 17: System.out.println("Size: " + file.length()); 18: System.out.println("Last Modified: " + file.lastModified()); 19: } else { 20: for (File subfile : file.listFiles()) { 21: System.out.println(" " + subfile.getName()); 22: } } } }
If the path provided points to a valid file, the program outputs something similar to the following due to the if statement on line 16:
~~~
Absolute Path: C:\data\zoo.txt
Is Directory: false
Parent Path: C:\data
Size: 12382
Last Modified: 1650610000000
~~~
if the path provided points to a valid directory, such as C:\data, the program outputs something similar to the following, thanks to the else block:
~~~
Absolute Path: C:\data
Is Directory: true
Parent Path: C:\
employees.txt
zoo.txt
zoo-backup.txt
~~~
NOTE
> [!NOTE]
we used two backslashes (\\)
in the path String, such as C:\data\zoo.txt.
When the compiler sees a \\
inside a String expression, it interprets it as a single \
value.
compiler sees a \\
inside a String expression, it interprets it as a single \
value.
NIO.2 example:
~~~
25: public static void nio() throws IOException {
26: var path = Path.of(“C:\data\zoo.txt”);
27: if (Files.exists(path)) {
28: System.out.println(“Absolute Path: “ + path.toAbsolutePath());
29: System.out.println(“Is Directory: “ + Files.isDirectory(path));
30: System.out.println(“Parent Path: “ + path.getParent());
31: if (Files.isRegularFile(path)) {
32: System.out.println(“Size: “ + Files.size(path));
33: System.out.println(“Last Modified: “
34: + Files.getLastModifiedTime(path));
35: } else {
36: try (Stream<Path> stream = Files.list(path)) {
37: stream.forEach(p ->
38: System.out.println(" " + p.getName()));
39: } } } }
~~~</Path>
More APIs in NIO.2 throw IOException than the I/O APIs did. In this case, Files.size(), Files.getLastModifiedTime(), and Files.list() throw an IOException.
Second, lines 36–39 use a Stream and a lambda instead of a loop. Since streams use lazy evaluation, this means the method will load each path element as needed, rather than the entire directory at once.
Closing the Stream
Stream object inside a try-with-resources?
~~~
36: try (Stream<Path> stream = Files.list(path)) {
37: stream.forEach(p ->
38: System.out.println(" " + p.getName()));
39: }
~~~</Path>
- The NIO.2 stream-based methods open a connection to the file system that must be properly closed;
- otherwise, a resource leak could ensue. A resource leak within the file system means the path may be locked from modification long after the process that used it is completed.
Handling Methods That Declare IOException
Many of the methods presented in this chapter declare IOException. Common causes of a method throwing this exception include the following:
* Loss of communication to the underlying file system.
* File or directory exists but cannot be accessed or modified.
* File exists but cannot be overwritten.
* File or directory is required but does not exist.
Methods that access or change files and directories, such as those in the Files class, often declare IOException. There are exceptions to this rule, as we will see. For example, the method Files.exists() does not declare IOException. If it did throw an exception when the file did not exist, it would never be able to return false! As a rule of thumb, if a NIO.2 method declares an IOException, it usually requires the paths it operates on to exist.
Providing NIO.2 Optional Parameters
TABLE 14.5 Common NIO.2 method arguments
-
LinkOption.NOFOLLOW_LINKS
-Do not follow symbolic links. -
StandardCopyOption.ATOMIC_MOVE
- Move file as atomic file system operation. -
StandardCopyOption.COPY_ATTRIBUTES
-Copy existing attributes to new file. -
StandardCopyOption.REPLACE_EXISTING
-Overwrite file if it already exists. -
StandardOpenOption.APPEND
-If file is already open for write, append to the end. -
StandardOpenOption.CREATE
-Create new file if it does not exist. -
StandardOpenOption.CREATE_NEW
-Create new file only if it does not exist; fail otherwise. -
StandardOpenOption.READ
-Open for read access. -
StandardOpenOption.TRUNCATE_EXISTING
-If file is already open for write, erase file and append to beginning. -
StandardOpenOption.WRITE
-Open for write access. -
FileVisitOption.FOLLOW_LINKS
-Follow symbolic links.
what the following call to Files.exists() with the LinkOption does in the following code snippet?
Path path = Paths.get("schedule.xml"); boolean exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
- The Files.exists() simply checks whether a file exists.
- But if the parameter is a symbolic link, the method checks whether the target of the symbolic link exists, instead.
- Providing
LinkOption.NOFOLLOW_LINKS
means the default behavior will be overridden, and the method will check whether the symbolic link itself exists.
For example, the Files.move() method takes a CopyOption vararg so it can take enums of different types, and more options can be added over time.
public static Path copy(Path source, Path target, CopyOption... options) throws IOException
LinkOption and StandardCopyOption Implemented CopyOption
void copy(Path source, Path target) throws IOException { Files.move(source, target, LinkOption.NOFOLLOW_LINKS, StandardCopyOption.ATOMIC_MOVE); }
Answer
Interacting with NIO.2 Paths
Path
instances are immutable
.
In the following example, the Path operation on the second line is lost since p is immutable:
~~~
Path p = Path.of(“whale”);
p.resolve(“krill”);
System.out.println(p); // whale
~~~
Many of the methods available in the Path interface transform the path value in some way and return a new Path object, allowing the methods to be chained. We demonstrate chaining in the following example, the details of which we discuss in this section of the chapter:
~~~
Path.of(“/zoo/../home”).getParent().normalize().toAbsolutePath();
~~~
Viewing the Path
String toString()
int getNameCount()
Path getName(int index)
The Path interface contains three methods to retrieve basic information about the path representation.
The toString()
method returns a String representation of the entire path. In fact, it is the only method in the Path interface to return a String.
The getNameCount()
and getName()
methods are often used together to retrieve the number of elements in the path and a reference to each element, respectively. These two methods do not include the root directory as part of the path.
Path path = Paths.get("/land/hippo/harry.happy"); System.out.println("The Path Name is: " + path); for(int i=0; i<path.getNameCount(); i++) System.out.println(" Element " + i + " is: " + path.getName(i));
The code prints the following:
~~~
The Path Name is: /land/hippo/harry.happy
Element 0 is: land
Element 1 is: hippo
Element 2 is: harry.happy
~~~
The getNameCount()
and getName()
methods do not consider the root part of the path.
~~~
var p = Path.of(“/”);
System.out.print(p.getNameCount()); // 0
System.out.print(p.getName(0)); // IllegalArgumentException
~~~
Notice that if you try to call getName()
with an invalid index, it will throw an exception at runtime.
Creating Part of the Path
~~~
var p = Paths.get(“/mammal/omnivore/raccoon.image”);
System.out.println(“Path is: “ + p);
for (int i = 0; i < p.getNameCount(); i++) {
System.out.println(“ Element “ + i + “ is: “ + p.getName(i));
}
System.out.println();
System.out.println(“subpath(0,3): “ + p.subpath(0, 3));
System.out.println(“subpath(1,2): “ + p.subpath(1, 2));
System.out.println(“subpath(1,3): “ + p.subpath(1, 3));
~~~
The Path interface includes the subpath()
method to select portions of a path. It takes two parameters: an inclusive beginIndex and an exclusive endIndex.
The output of this code snippet is the following:
Path is: /mammal/omnivore/raccoon.image Element 0 is: mammal Element 1 is: omnivore Element 2 is: raccoon.image subpath(0,3): mammal/omnivore/raccoon.image subpath(1,2): omnivore subpath(1,3): omnivore/raccoon.image
Like getNameCount() and getName(), subpath() is zero-indexed and does not include the root. Also like getName(), subpath() throws an exception if invalid indices are provided.
~~~
var q = p.subpath(0, 4); // IllegalArgumentException
var x = p.subpath(1, 1); // IllegalArgumentException
~~~
The first example throws an exception at runtime, since the maximum index value allowed is 3. The second example throws an exception since the start and end indexes are the same, leading to an empty path value.
Accessing Path Elements
- The Path interface contains numerous methods for retrieving particular elements of a Path, returned as Path objects themselves.
- The
getFileName()
method returns the Path element of the current file or directory, - while
getParent()
returns the full path of the containing directory. - The getParent() method returns null if operated on the root path or at the top of a relative path.
- The getRoot() method returns the root element of the file within the file system, or null if the path is a relative path.
Consider the following method, which prints various Path elements:
public void printPathInformation(Path path) { System.out.println("Filename is: " + path.getFileName()); System.out.println(" Root is: " + path.getRoot()); Path currentParent = path; while((currentParent = currentParent.getParent()) != null) System.out.println(" Current parent is: " + currentParent); System.out.println(); }
The while loop in the printPathInformation() method continues until getParent() returns null. We apply this method to the following three paths:
printPathInformation(Path.of("zoo")); printPathInformation(Path.of("/zoo/armadillo/shells.txt")); printPathInformation(Path.of("./armadillo/../shells.txt"));
This sample application produces the following output:
Filename is: zoo Root is: null Filename is: shells.txt Root is: / Current parent is: /zoo/armadillo Current parent is: /zoo Current parent is: / Filename is: shells.txt Root is: null Current parent is: ./armadillo/.. Current parent is: ./armadillo Current parent is: .
Resolving Paths
resolve()
- The resolve() method provides overloaded versions that let you pass either a Path or String parameter.
- The object on which the resolve() method is invoked becomes the basis of the new Path object, with the input argument being appended onto the Path.
For the exam, you should be cognizant of
* mixing absolute and relative paths with the resolve() method.
* If an absolute path is provided as input to the method, that is the value returned.
* Simply put, you cannot combine two absolute paths using resolve().
apply resolve() to an absolute path and a relative path:
~~~
Path path1 = Path.of(“/cats/../panther”);
Path path2 = Path.of(“food”);
System.out.println(path1.resolve(path2));
~~~
The code snippet generates the following output:
/cats/../panther/food
what if it had been an absolute path?
Path path3 = Path.of("/turkey/food"); System.out.println(path3.resolve("/tiger/cage"));
Since the input parameter is an absolute path, the output would be the following:
/tiger/cage
For the exam, you should be cognizant of mixing absolute and relative paths with the resolve() method. If an absolute path is provided as input to the method, that is the value returned. Simply put, you cannot combine two absolute paths using resolve()
.
TIP
> [!TIP]
On the exam, when you see resolve()
, think concatenation.
On the exam, when you see resolve()
, think concatenation.
Relativizing a Path
relativize()
The Path interface includes a relativize() method for constructing the relative path from one Path to another, often using path symbols.
- If both path values are relative, the relativize() method computes the paths as if they are in the same current working directory.
- Alternatively, if both path values are absolute, the method computes the relative path from one absolute location to another, regardless of the current working directory.
- The relativize() method requires both paths to be absolute or relative and throws an exception if the types are mixed.
- On Windows-based systems, it also requires that if absolute paths are used, both paths must have the same root directory or drive letter
What do you think the following examples will print?
var path1 = Path.of("fish.txt"); var path2 = Path.of("friendly/birds.txt"); System.out.println(path1.relativize(path2)); System.out.println(path2.relativize(path1));
The examples print the following:
../friendly/birds.txt ../../fish.txt
The idea is this: if you are pointed at a path in the file system, what steps would you need to take to reach the other path? For example, to get to fish.txt from friendly/birds.txt, you need to go up two levels (the file itself counts as one level) and then select fish.txt.
The following example demonstrates this property when run on a Windows computer:
Path path3 = Paths.get("E:\\habitat"); Path path4 = Paths.get("E:\\sanctuary\\raven\\poe.txt"); System.out.println(path3.relativize(path4)); System.out.println(path4.relativize(path3));
This code snippet produces the following output:
..\sanctuary\raven\poe.txt ..\..\..\habitat
The relativize() method requires both paths to be absolute or relative and throws an exception if the types are mixed.
Path path1 = Paths.get("/primate/chimpanzee"); Path path2 = Paths.get("bananas.txt"); path1.relativize(path2); // IllegalArgumentException
On Windows-based systems, it also requires that if absolute paths are used, both paths must have the same root directory or drive letter. For example, the following would also throw an IllegalArgumentException on a Windows-based system:
Path path3 = Paths.get("C:\\primate\\chimpanzee"); Path path4 = Paths.get("D:\\storage\\bananas.txt"); path3.relativize(path4); // IllegalArgumentException
The relativize() method requires both paths to be absolute or relative and throws an exception if the types are mixed.
On Windows-based systems, it also requires that if absolute paths are used, both paths must have the same root directory or drive letter.
Normalizing a Path
Java provides the normalize() method to eliminate unnecessary redundancies in a path.
We can apply normalize() to some of our previous paths.
var p1 = Path.of("./armadillo/../shells.txt"); System.out.println(p1.normalize()); // shells.txt var p2 = Path.of("/cats/../panther/food"); System.out.println(p2.normalize()); // /panther/food var p3 = Path.of("../../fish.txt"); System.out.println(p3.normalize()); // ../../fish.txt
- The first two examples apply the path symbols to remove the redundancies,
- but what about the last one? That is as simplified as it can be.
- The
normalize()
method does not remove all of the path symbols, only the ones that can be reduced.
Consider the following example:
var p1 = Paths.get("/pony/../weather.txt"); var p2 = Paths.get("/weather.txt"); System.out.println(p1.equals(p2)); // false System.out.println(p1.normalize().equals(p2.normalize())); // true
The normalize()
method also allows us to compare equivalent paths.
The equals() method returns true if two paths represent the same value. In the first comparison, the path values are different. In the second comparison, the path values have both been reduced to the same normalized value, /weather.txt. This is the primary function of the normalize() method: to allow us to better compare different paths.
Retrieving the Real File System Path
toRealPath()
-
verify that the path exists within the file system using
toRealPath()
. - This method is similar to
normalize()
in that it eliminates any redundant path symbols. - It is also similar to
toAbsolutePath()
, in that it will join the path with the current working directory if the path is relative. - Unlike those two methods, though, toRealPath() will throw an exception if the path does not exist.
- In addition, it will follow symbolic links, with an optional LinkOption varargs parameter to ignore them.
Let’s say that we have a file system in which we have a symbolic link from /zebra to /horse.
What do you think the following will print, given a current working directory of /horse/schedule?
~~~
System.out.println(Paths.get(“/zebra/food.txt”).toRealPath());
System.out.println(Paths.get(“.././food.txt”).toRealPath());
~~~
The output of both lines is the following:
/horse/food.txt
In this example, the absolute and relative paths both resolve to the same absolute file, as the symbolic link points to a real file within the file system.
We can also use the toRealPath() method to gain access to the current working directory as a Path object.
System.out.println(Paths.get(".").toRealPath());
Reviewing NIO.2 Path APIs
TABLE 14.6 Path APIs
- File path as string - public String toString()
- Single segment - public Path getName(int index)
- Number of segments - public int getNameCount()
- Segments in range - public Path subpath(int beginIndex, int endIndex)
- Final segment - public Path getFileName()
- Immediate parent - public Path getParent()
- Top-level segment - public Path getRoot()
- Concatenate paths - public Path resolve(String p)
public Path resolve(Path p) - Construct path to one provided - public Path relativize(Path p)
- Remove redundant parts of path - public Path normalize()
- Follow symbolic links to find path on file system - public Path toRealPath()
Creating, Moving, and Deleting Files and Directories
Answer
Making Directories
public static Path createDirectory(Path dir, FileAttribute<?>… attrs) throws IOException public static Path createDirectories(Path dir, FileAttribute<?>… attrs) throws IOException
To create a directory, we use these Files methods:
public static Path createDirectory(Path dir, FileAttribute<?>… attrs) throws IOException public static Path createDirectories(Path dir, FileAttribute<?>… attrs) throws IOException
createDirectory()
* The createDirectory()
method will create a directory
* and throw an exception if it already exists or if the paths leading up to the directory do not exist.
createDirectories()
* The createDirectories()
method creates the target directory along with any nonexistent parent directories leading up to the path.
* If all of the directories already exist, createDirectories()
will simply complete without doing anything.
* This is useful in situations where you want to ensure a directory exists and create it if it does not.
Both of these methods also accept an optional list of FileAttribute<?>
values to apply to the newly created directory or directories.
The following shows how to create directories:
Files.createDirectory(Path.of("/bison/field")); Files.createDirectories(Path.of("/bison/field/pasture/green"));
- The first example creates a new directory, field, in the directory /bison, assuming /bison exists; otherwise, an exception is thrown.
- Contrast this with the second example, which creates the directory green along with any of the following parent directories if they do not already exist, including bison, field, and pasture.
Copying Files
public static Path copy(Path source, Path target, CopyOption… options) throws IOException
The Files class provides a method for copying files and directories within the file system.
public static Path copy(Path source, Path target, CopyOption… options) throws IOException
The method copies a file or directory from one location to another using Path objects.
A shallow copy means that the files and subdirectories within the directory are not copied.
A deep copy means that the entire tree is copied, including all of its content and subdirectories.
The following shows an example of copying a file and a directory:
~~~
Files.copy(Paths.get(“/panda/bamboo.txt”), Paths.get(“/panda-save/bamboo.txt”));
Files.copy(Paths.get(“/turtle”), Paths.get(“/turtleCopy”));
~~~
When directories are copied, the copy is shallow.
A deep copy typically requires recursion, where a method calls itself.
public void copyPath(Path source, Path target) { try { Files.copy(source, target); if(Files.isDirectory(source)) try (Stream<Path> s = Files.list(source)) { s.forEach(p -> copyPath(p, target.resolve(p.getFileName()))); } } catch(IOException e) { // Handle exception } }
The method first copies the path, whether a file or a directory. If it is a directory, only a shallow copy is performed. Next, it checks whether the path is a directory and, if it is, performs a recursive copy of each of its elements. What if the method comes across a symbolic link? Don’t worry: the JVM will not follow symbolic links when using the list() method.
Copying and Replacing Files
- By default, if the target already exists, the copy() method will throw an exception.
- You can change this behavior by providing the StandardCopyOption enum value
REPLACE_EXISTING
to the method. - The following method call will overwrite the movie.txt file if it already exists:
Files.copy(Paths.get("book.txt"), Paths.get("movie.txt"), StandardCopyOption.REPLACE_EXISTING);
For the exam, you need to know that without the REPLACE_EXISTING
option, this method will throw an exception if the file already exists.
Copying Files with I/O Streams
public static long copy(InputStream in, Path target, CopyOption… options) throws IOException public static long copy(Path source, OutputStream out) throws IOException
The Files class includes two copy() methods that operate with I/O streams.
public static long copy(InputStream in, Path target, CopyOption… options) throws IOException public static long copy(Path source, OutputStream out) throws IOException
The first method reads the contents of an I/O stream and writes the output to a file. The second method reads the contents of a file and writes the output to an I/O stream. These methods are quite convenient if you need to quickly read/write data from/to disk.
The following are examples of each copy() method:
try (var is = new FileInputStream("source-data.txt")) { // Write I/O stream data to a file Files.copy(is, Paths.get("/mammals/wolf.txt")); } Files.copy(Paths.get("/fish/clown.xsl"), System.out);
While we used FileInputStream in the first example, the I/O stream could have been any valid I/O stream including website connections, in-memory stream resources, and so forth. The second example prints the contents of a file directly to the System.out stream.
Copying Files into a Directory
For the exam, it is important that you understand how the copy() method operates on both files and directories.
What do you think is the result of executing the following process?
var file = Paths.get("food.txt"); var directory = Paths.get("/enclosure"); Files.copy(file, directory);
If you said it would create a new file at /enclosure/food.txt, you’re way off. It throws an exception. The command tries to create a new file named /enclosure. Since the path /enclosure already exists, an exception is thrown at runtime.
On the other hand, if the directory did not exist, the process would create a new file with the contents of food.txt, but the file would be called /enclosure
. Remember, we said files may not need to have extensions, and in this example, it matters.
the correct way to copy the file into the directory is to do the following:
~~~
var file = Paths.get(“food.txt”);
var directory = Paths.get(“/enclosure/food.txt”);
Files.copy(file, directory);
~~~
Answer
Moving or Renaming Paths with move()
public static Path move(Path source, Path target, CopyOption… options) throws IOException
The Files class provides a useful method for moving or renaming files and directories.
public static Path move(Path source, Path target, CopyOption… options) throws IOException
The following sample code uses the move() method:
Files.move(Path.of("C:\\zoo"), Path.of("C:\\zoo-new")); Files.move(Path.of("C:\\user\\addresses.txt"), Path.of("C:\\zoo-new\\addresses2.txt"));
- The first example renames the zoo directory to a zoo-new directory, keeping all of the original contents from the source directory.
- The second example moves the addresses.txt file from the directory user to the directory zoo-new and renames it addresses2.txt.
Similarities between move()
and copy()
- Like copy(), move() requires
REPLACE_EXISTING
to overwrite the target if it exists; otherwise, it will throw an exception. - Also like copy(), move() will not put a file in a directory if the source is a file and the target is a directory. Instead, it will create a new file with the name of the directory.
Performing an Atomic Move
Files.move(Path.of("mouse.txt"), Path.of("gerbil.txt"), StandardCopyOption.ATOMIC_MOVE);
Another enum value that you need to know for the exam when working with the move() method is the StandardCopyOption value ATOMIC_MOVE.
~~~
Files.move(Path.of(“mouse.txt”), Path.of(“gerbil.txt”),
StandardCopyOption.ATOMIC_MOVE);
~~~
You may remember the atomic property from Chapter 13, “Concurrency,” and the principle of an atomic move is similar. An atomic move is one in which a file is moved within the file system as a single indivisible operation. Put another way, any process monitoring the file system never sees an incomplete or partially written file. If the file system does not support this feature, an AtomicMoveNotSupportedException
will be thrown.
Note that while ATOMIC_MOVE
is available as a member of the StandardCopyOption type, it will likely throw an exception if passed to a copy() method.
Deleting a File with delete() and deleteIfExists()
public static void delete(Path path) throws IOException public static boolean deleteIfExists(Path path) throws IOException
The Files class includes two methods that delete a file or empty directory within the file system.
~~~
public static void delete(Path path) throws IOException
public static boolean deleteIfExists(Path path) throws IOException
~~~
- To delete a directory, it must be empty. Both of these methods throw an exception if operated on a nonempty directory.
- In addition, if the path is a symbolic link, the symbolic link will be deleted, not the path that the symbolic link points to.
- The methods differ on how they handle a path that does not exist.
- The delete() method throws an exception if the path does not exist,
- while the deleteIfExists() method returns true if the delete was successful or false otherwise.
- Similar to createDirectories(), deleteIfExists() is useful in situations where you want to ensure that a path does not exist and delete it if it does.
Here we provide sample code that performs delete() operations:
~~~
Files.delete(Paths.get(“/vulture/feathers.txt”));
Files.deleteIfExists(Paths.get(“/pigeon”));
~~~
- The first example deletes the feathers.txt file in the vulture directory, and it throws a NoSuchFileException if the file or directory does not exist.
- The second example deletes the pigeon directory, assuming it is empty. If the pigeon directory does not exist, the second line will not throw an exception.
Comparing Files with isSameFile() and mismatch()
Since a path may include path symbols and symbolic links within a file system, the equals() method can’t be relied on to know if two Path instances refer to the same file. Luckily, there is the isSameFile() method.
- This method takes two Path objects as input,
- resolves all path symbols,
- and follows symbolic links.
- Despite the name, the method can also be used to determine whether two Path objects refer to the same directory.
- While most uses of isSameFile() will trigger an exception if the paths do not exist, there is a special case in which it does not.
- If the two path objects are equal in terms of equals(), the method will just return true without checking whether the file exists.
Assume that the file system exists, as shown in Figure 14.4, with a symbolic link from /animals/snake to /animals/cobra.
Given the structure defined in Figure 14.4, what does the following output?
System.out.println(Files.isSameFile( Path.of("/animals/cobra"), Path.of("/animals/snake"))); System.out.println(Files.isSameFile( Path.of("/animals/monkey/ears.png"), Path.of("/animals/wolf/ears.png")));
Since snake is a symbolic link to cobra, the first example outputs true. In the second example, the paths refer to different files, so false is printed.
Sometimes you want to compare the contents of the file rather than whether it is physically the same file. For example, we could have two files with text hello. The mismatch() method was introduced in Java 12 to help us out here. It takes two Path objects as input. The method returns -1 if the files are the same; otherwise, it returns the index of the first position in the file that differs.
~~~
System.out.println(Files.mismatch(
Path.of(“/animals/monkey.txt”),
Path.of(“/animals/wolf.txt”)));
~~~
Suppose monkey.txt contains the name Harold and wolf.txt contains the name Howler. The previous code prints 1 in that case because the second position is different, and we use zero-based indexing in Java. Given those values, what do you think this code prints?
~~~
System.out.println(Files.mismatch(
Path.of(“/animals/wolf.txt”),
Path.of(“/animals/monkey.txt”)));
~~~
The answer is the same as the previous example. The code prints 1 again. The mismatch() method is symmetric and returns the same result regardless of the order of the parameters.
Introducing I/O Streams
* I/O stream
* Buffered
- The contents of a file may be accessed or written via an I/O stream, which is a list of data elements presented sequentially.
- The I/O stream is so large that once we start reading it, we have no idea where the beginning or the end is. We just have a pointer to our current position in the I/O stream and read data one block at a time.
- Each type of I/O stream segments data into a wave or block in a particular way.
- For example, some I/O stream classes read or write data as individual bytes. Other I/O stream classes read or write individual characters or strings of characters.
- On top of that, some I/O stream classes read or write larger groups of bytes or characters at a time, specifically those with the word Buffered in their name.
> [!NOTE]
Although the java.io API is full of I/O streams that handle characters, strings, groups of bytes, and so on, nearly all are built on top of reading or writing an individual byte or an array of bytes at a time.
Higher-level I/O streams exist for convenience as well as performance.
Although I/O streams are commonly used with file I/O, they are more generally used to handle the reading/writing of any sequential data source. For example, you might construct a Java application that submits data to a website using an output stream and reads the result via an input stream.
- Answer
Understanding I/O Stream Fundamentals
- The contents of a file may be accessed or written via an I/O stream, which is a list of data elements presented sequentially.
- The I/O stream is so large that once we start reading it, we have no idea where the beginning or the end is. We just have a pointer to our current position in the I/O stream and read data one block at a time.
- Each type of I/O stream segments data into a wave or block in a particular way.
- For example, some I/O stream classes read or write data as individual bytes. Other I/O stream classes read or write individual characters or strings of characters.
- On top of that, some I/O stream classes read or write larger groups of bytes or characters at a time, specifically those with the word Buffered in their name.
Understanding I/O Stream Fundamentals
- The contents of a file may be accessed or written via an I/O stream,
- which is a list of data elements presented sequentially.
- The I/O stream is so large that once we start reading it, we have no idea where the beginning or the end is. We just have a pointer to our current position in the I/O stream and read data one block at a time.
- Each type of I/O stream segments data into a wave or block in a particular way.
- For example, some I/O stream classes read or write data as individual bytes. Other I/O stream classes read or write individual characters or strings of characters.
- On top of that, some I/O stream classes read or write larger groups of bytes or characters at a time, specifically those with the word Buffered in their name.
I/O Streams Can Be Big
When writing code where you don’t know what the I/O stream size will be at runtime, it may be helpful to visualize an I/O stream as being so large that all of the data contained in it could not possibly fit into memory. For example, a 1 TB file could not be stored entirely in memory by most computer systems (at the time this book is being written). The file can still be read and written by a program with very little memory, since the I/O stream allows the application to focus on only a small portion of the overall I/O stream at any given time.
Learning I/O Stream Nomenclature
The java.io API provides numerous classes for creating, accessing, and manipulating I/O streams—so many that it tends to overwhelm many new Java developers. Stay calm! We review the major differences between each I/O stream class and show you how to distinguish between them. Even if you come across a particular I/O stream on the exam that you do not recognize, the name of the I/O stream often gives you enough information to understand exactly what it does. The goal of this section is to familiarize you with common terminology and naming conventions used with I/O streams. Don’t worry if you don’t recognize the particular stream class names used in this section or their function; we cover how to use them in detail in this chapter.
Storing Data as Bytes
Data is stored in a file system (and memory) as a 0 or 1, called a bit. Since it’s really hard for humans to read/write data that is just 0s and 1s, they are grouped into a set of 8 bits, called a byte.
Byte Streams vs. Character Streams
The java.io API defines two sets of I/O stream classes for reading and writing I/O streams:
byte I/O streams and character I/O streams.
Differences between Byte and Character I/O Streams
* Byte I/O streams read/write binary data (0s and 1s) and have class names that end in InputStream or OutputStream.
* Character I/O streams read/write text data and have class names that end in Reader or Writer.
The API frequently includes similar classes for both byte and character I/O streams, such as FileInputStream and FileReader.
The difference between the two classes is based on how the bytes are read or written.
It is important to remember that even though character I/O streams do not contain the word Stream in their class name, they are still I/O streams. The use of Reader/Writer in the name is just to distinguish them from byte streams.
- The byte I/O streams are primarily used to work with binary data, such as an image or executable file,
- while character I/O streams are used to work with text files.
- For example, you can use a Writer class to output a String value to a file without necessarily having to worry about the underlying character encoding of the file.
- The character encoding determines how characters are encoded and stored in bytes in an I/O stream and later read back or decoded as characters. Although this may sound simple, Java supports a wide variety of character encodings, ranging from ones that may use one byte for Latin characters, UTF-8 and ASCII for example, to using two or more bytes per character, such as UTF-16.
- For the exam, you don’t need to memorize the character encodings, but you should be familiar with the names.
Character Encoding in Java
Charset usAsciiCharset = Charset.forName("US-ASCII"); Charset utf8Charset = Charset.forName("UTF-8"); Charset utf16Charset = Charset.forName("UTF-16");
In Java, the character encoding can be specified using the Charset class
by passing a name value to the static Charset.forName() method
, such as in the following examples:
~~~
Charset usAsciiCharset = Charset.forName(“US-ASCII”);
Charset utf8Charset = Charset.forName(“UTF-8”);
Charset utf16Charset = Charset.forName(“UTF-16”);
~~~
Java supports numerous character encodings, each specified by a different standard name value.
Input vs. Output Streams
-
Most
InputStream
classes have a correspondingOutputStream
class, and vice versa. - For example, the FileOutputStream class writes data that can be read by a FileInputStream.
- There are exceptions to this rule. For the exam, you should know that
- PrintWriter has no accompanying PrintReader class.
- Likewise, the PrintStream is an OutputStream that has no corresponding InputStream class. It also does not have Output in its name.
Low-Level vs. High-Level Streams
- Another way that you can familiarize yourself with the java.io API is by segmenting I/O streams into low-level and high-level streams.
- A low-level stream connects directly with the source of the data, such as a file, an array, or a String.
- Low-level I/O streams process the raw data or resource and are accessed in a direct and unfiltered manner. For example, a FileInputStream is a class that reads file data one byte at a time.
- Alternatively, a high-level stream is built on top of another I/O stream using wrapping.
- Wrapping is the process by which an instance is passed to the constructor of another class, and operations on the resulting instance are filtered and applied to the original instance.
- For example, take a look at the FileReader and BufferedReader objects in the following sample code:
~~~
try (var br = new BufferedReader(new
FileReader(“zoo-data.txt”))) {
System.out.println(br.readLine());
}
~~~ - In this example, FileReader is the low-level I/O stream, whereas BufferedReader is the high-level I/O stream that takes a FileReader as input. Many operations on the high-level I/O stream pass through as operations to the underlying low-level I/O stream, such as read() or close(). Other operations override or add new functionality to the low-level I/O stream methods. The high-level I/O stream may add new methods, such as readLine(), as well as performance enhancements for reading and filtering the low-level data.
-
High-level I/O streams can also take other high-level I/O streams as input. For example, although the following code might seem a little odd at first, the style of wrapping an I/O stream is quite common in practice:
~~~
try (var ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(“zoo-data.txt”)))) {
System.out.print(ois.readObject());
}
~~~
In this example, the low-level FileInputStream interacts directly with the file, which is wrapped by a high-level BufferedInputStream to improve performance. Finally, the entire object is wrapped by another high-level ObjectInputStream, which allows us to interpret the data as a Java object.
For the exam, the only low-level stream classes you need to be familiar with are the ones that operate on files. The rest of the nonabstract stream classes are all high-level streams.
Stream Base Classes
The java.io library defines four abstract classes that are the parents of all I/O stream classes defined within the API:
1. InputStream,
2. OutputStream,
3. Reader,
4. and Writer.
The constructors of high-level I/O streams often take a reference to the abstract class.
For example, BufferedWriter takes a Writer object as input, which allows it to take any subclass of Writer.
One common area where the exam likes to play tricks on you is mixing and matching I/O stream classes that are not compatible with each other. For example, take a look at each of the following examples and see whether you can determine why they do not compile:
~~~
new BufferedInputStream(new FileReader(“z.txt”)); // DOES NOT COMPILE
new BufferedWriter(new FileOutputStream(“z.txt”)); // DOES NOT COMPILE
new ObjectInputStream(new FileOutputStream(“z.txt”)); // DOES NOT COMPILE
new BufferedInputStream(new InputStream()); // DOES NOT COMPILE
~~~
- The first two examples do not compile because they mix Reader/Writer classes with InputStream/OutputStream classes, respectively.
- The third example does not compile because we are mixing an OutputStream with an InputStream. Although it is possible to read data from an InputStream and write it to an OutputStream, wrapping the I/O stream is not the way to do so. As you see later in this chapter, the data must be copied over.
- Finally, the last example does not compile because InputStream is an abstract class, and therefore you cannot create an instance of it.
Decoding I/O Class Names
TABLE 14.7 The java.io abstract stream base classes
* InputStream - Abstract class for all input byte streams
* OutputStream - Abstract class for all output byte streams
* Reader - Abstract class for all input character streams
* Writer - Abstract class for all output character streams
TABLE 14.8 The java.io concrete I/O stream classes
* FileInputStream - Low - Reads file data as bytes
* FileOutputStream - Low - Writes file data as bytes
* FileReader - Low - Reads file data as characters
* FileWriter - Low - Writes file data as characters
* BufferedInputStream- High- Reads byte data from existing InputStream in buffered manner, which improves efficiency and performance
* BufferedOutputStream- High- Writes byte data to existing OutputStream in buffered manner, which improves efficiency and performance
* BufferedReader- High- Reads character data from existing Reader in buffered manner, which improves efficiency and performance
* BufferedWriter- High- Writes character data to existing Writer in buffered manner, which improves efficiency and performance
* ObjectInputStream- High- Deserializes primitive Java data types and graphs of Java objects from existing bInputStream
* ObjectOutputStream- High- Serializes primitive Java data types and graphs of Java objects to existing OutputStream
* PrintStream- High- Writes formatted representations of Java objects to binary stream
* PrintWriter- High- Writes formatted representations of Java objects to character stream
Reading and Writing Files
Both InputStream and Reader declare a read() method to read byte data from an I/O stream. Likewise, OutputStream and Writer both define a write() method to write a byte to the stream:
In both examples, -1 is used to indicate the end of the stream.
~~~
void copyStream(InputStream in, OutputStream out) throws IOException {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
}
void copyStream(Reader in, Writer out) throws IOException {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
}
~~~
why do the methods use int instead of byte?
the byte data type has a range of 256 characters. They needed an extra value to indicate the end of an I/O stream. The authors of Java decided to use a larger data type, int, so that special values like -1 would indicate the end of an I/O stream. The output stream classes use int as well, to be consistent with the input stream classes.
Reading and writing one byte at a time isn’t a particularly efficient way of doing this. Luckily, there are overloaded methods for reading and writing multiple bytes at a time. The offset and length values are applied to the array itself. For example, an offset of 3 and length of 5 indicates that the stream should read up to five bytes/characters of data and put them into the array starting with position 3.
Let’s look at an example:
~~~
10: void copyStream(InputStream in, OutputStream out) throws IOException {
11: int batchSize = 1024;
12: var buffer = new byte[batchSize];
13: int lengthRead;
14: while ((lengthRead = in.read(buffer, 0, batchSize))> 0) {
15: out.write(buffer, 0, lengthRead);
16: out.flush();
17: }
~~~
- Instead of reading the data one byte at a time, we read and write up to 1024 bytes at a time on line 14.
- The return value lengthRead is critical for determining whether we are at the end of the stream and knowing how many bytes we should write into our output stream.
- Unless our file happens to be a multiple of 1024 bytes, the last iteration of the while loop will write some value less than 1024 bytes.
- For example, if the buffer size is 1,024 bytes and the file size is 1,054 bytes, the last read will be only 30 bytes. If we ignored this return value and instead wrote 1,024 bytes, 994 bytes from the previous loop would be written to the end of the file.
- We also added a flush() method on line 16 to reduce the amount of data lost if the application terminates unexpectedly. When data is written to an output stream, the underlying operating system does not guarantee that the data will make it to the file system immediately. The flush() method requests that all accumulated data be written immediately to disk. It is not without cost, though. Each time it is used, it may cause a noticeable delay in the application, especially for large files. Unless the data that you are writing is extremely critical, the flush() method should be used only intermittently. For example, it should not necessarily be called after every write, as it is in this example.
Let’s try again using high-level streams.
~~~
26: void copyTextFile(File src, File dest) throws IOException {
27: try (var reader = new BufferedReader(new FileReader(src));
28: var writer = new BufferedWriter(new FileWriter(dest))) {
29: String line = null;
30: while ((line = reader.readLine()) != null) {
31: writer.write(line);
32: writer.newLine();
33: } } }
~~~
- The key is to choose the most useful high-level classes.
- In this case, we are dealing with a File, so we want to use a FileReader and FileWriter.
- Both classes have constructors that can take either a String representing the location or a File directly.
- If the source file does not exist, a FileNotFoundException, which inherits IOException, will be thrown.
- If the destination file already exists, this implementation will overwrite it. We can pass an optional boolean second parameter to FileWriter for an append flag if we want to change this behavior.
void copyTextFile(File src, File dest) throws IOException { try (var reader = new BufferedReader(new FileReader(src)); var writer = new PrintWriter(new FileWriter(dest))) { String line = null; while ((line = reader.readLine()) != null) writer.println(line); } }
> [!NOTE]
It may surprise you that you’ve been regularly using a PrintStream throughout this book.
Both System.out and System.err are PrintStream objects. Likewise, System.in, often useful for reading user input, is an InputStream.
Unlike the majority of the other I/O streams we’ve covered, the methods in the print stream classes do not throw any checked exceptions. If they did, you would be required to catch a checked exception any time you called System.out.print()!
The line separator is \n or \r\n, depending on your operating system.
The println() method takes care of this for you. If you need to get the character directly, either of the following will return it for you:
~~~
System.getProperty(“line.separator”);
System.lineSeparator();
~~~
PrintStream
Using I/O Streams
Answer
Enhancing with Files
The NIO.2 APIs provide even easier ways to read and write a file using the Files class.
Let’s start by looking at three ways of copying a file by reading in the data and writing it back:
~~~
private void copyPathAsString(Path input, Path output) throws IOException {
String string = Files.readString(input);
Files.writeString(output, string);
}
private void copyPathAsBytes(Path input, Path output) throws IOException {
byte[] bytes = Files.readAllBytes(input);
Files.write(output, bytes);
}
private void copyPathAsLines(Path input, Path output) throws IOException {
List<String> lines = Files.readAllLines(input);
Files.write(output, lines);
}
~~~</String>
That’s pretty concise! You can read a Path as a String, a byte array, or a List. Be aware that the entire file is read at once for all three of these, thereby storing all of the contents of the file in memory at the same time. If the file is significantly large, you may trigger an OutOfMemoryError when trying to load all of it into memory. Luckily, there is an alternative. This time, we print out the file as we read it.
~~~
private void readLazily(Path path) throws IOException {
try (Stream<String> s = Files.lines(path)) {
s.forEach(System.out::println);
}
}
~~~</String>
Now the contents of the file are read and processed lazily, which means that only a small portion of the file is stored in memory at any given time. Taking things one step further, we can leverage other stream methods for a more powerful example.
~~~
try (var s = Files.lines(path)) {
s.filter(f -> f.startsWith(“WARN:”))
.map(f -> f.substring(5))
.forEach(System.out::println);
}
~~~
This sample code searches a log for lines that start with WARN:, outputting the text that follows. Assuming that the input file sharks.log is as follows:
~~~
INFO:Server starting
DEBUG:Processes available = 10
WARN:No database could be detected
DEBUG:Processes available reset to 0
WARN:Performing manual recovery
INFO:Server successfully started
~~~
Then the sample output would be the following:
~~~
No database could be detected
Performing manual recovery
~~~
As you can see, we have the ability to manipulate files in complex ways, often with only a few short expressions.
Files.readAllLines() vs. Files.lines()
For the exam, you need to know the difference between readAllLines() and lines(). Both of these examples compile and run:
~~~
Files.readAllLines(Paths.get(“birds.txt”)).forEach(System.out::println);
Files.lines(Paths.get(“birds.txt”)).forEach(System.out::println);
~~~
The first line reads the entire file into memory and performs a print operation on the result, while the second line lazily processes each line and prints it as it is read. The advantage of the second code snippet is that it does not require the entire file to be stored in memory at any time.
You should also be aware of when they are mixing incompatible types on the exam. Do you see why the following does not compile?
~~~
Files.readAllLines(Paths.get(“birds.txt”))
.filter(s -> s.length()> 2)
.forEach(System.out::println);
~~~
The readAllLines() method returns a List, not a Stream, so the filter() method is not available.
Combining with newBufferedReader() and newBufferedWriter()
Sometimes you need to mix I/O streams and NIO.2. Conveniently, Files includes two convenience methods for getting I/O streams.
~~~
private void copyPath(Path input, Path output) throws IOException {
try (var reader = Files.newBufferedReader(input);
var writer = Files.newBufferedWriter(output)) {
String line = null; while ((line = reader.readLine()) != null) writer.write(line); writer.newLine(); } } } ~~~
You can wrap I/O stream constructors to produce the same effect, although it’s a lot easier to use the factory method. The first method, newBufferedReader(), reads the file specified at the Path location using a BufferedReader object.
Reviewing Common Read and Write Methods
TABLE 14.9 Common I/O read and write methods
TABLE 14.10 Common Files NIO.2 read and write methods
Serializing Data
- Serialization is the process of converting an in-memory object to a byte stream.
- deserialization is the process of converting from a byte stream into an object.
- Serialization often involves writing an object to a stored or transmittable format, while deserialization is the reciprocal process.
- Java provides built-in mechanisms for serializing and deserializing I/O streams of objects directly to and from disk, respectively.
Applying the Serializable Interface
- To serialize an object using the I/O API, the object must implement the java.io.Serializable interface.
- The Serializable interface is a marker interface, which means it does not have any methods.
- Any class can implement the Serializable interface since there are no required methods to implement.
> [!NOTE]
Since Serializable is a marker interface with no abstract members, why not just apply it to every class?
Generally speaking, you should only mark data-oriented classes serializable.
Process-oriented classes, such as the I/O streams discussed in this chapter or the Thread instances you learned about in Chapter 13, are often poor candidates for serialization, as the internal state of those classes tends to be ephemeral or short-lived.
The purpose of using the Serializable interface is to inform any process attempting to serialize the object that you have taken the proper steps to make the object serializable.
All Java primitives and many of the built-in Java classes that you have worked with throughout this book are Serializable.
For example, this class can be serialized:
~~~
import java.io.Serializable;
public class Gorilla implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private Boolean friendly;
private transient String favoriteFood;
// Constructors/Getters/Setters/toString() omitted
}
~~~
In this example, the Gorilla class contains three instance members (name, age, friendly) that will be saved to an I/O stream if the class is serialized. Note that since Serializable is not part of the java.lang package, it must be imported or referenced with the package name.
What about the favoriteFood field that is marked transient? Any field that is marked transient will not be saved to an I/O stream when the class is serialized. We discuss that in more detail next.
Maintaining a serialVersionUID
It’s a good practice to declare a static serialVersionUID variable in every class that implements Serializable. The version is stored with each object as part of serialization. Then, every time the class structure changes, this value is updated or incremented.
Perhaps our Gorilla class receives a new instance member Double banana, or maybe the age field is renamed. The idea is a class could have been serialized with an older version of the class and deserialized with a newer version of the class.
The serialVersionUID helps inform the JVM that the stored data may not match the new class definition. If an older version of the class is encountered during deserialization, a java.io.InvalidClassException may be thrown. Alternatively, some APIs support converting data between versions.
Marking Data transient
The transient modifier can be used for sensitive data of the class, like a password. There are other objects it does not make sense to serialize, like the state of an in-memory Thread. If the object is part of a serializable object, we just mark it transient to ignore these select instance members.
What happens to data marked transient on deserialization? It reverts to its default Java values, such as 0.0 for double, or null for an object. You see examples of this shortly when we present the object stream classes.
Marking static fields transient has little effect on serialization. Other than the serialVersionUID, only the instance members of a class are serialized.
Ensuring That a Class Is Serializable
Since Serializable is a marker interface, you might think there are no rules to using it. Not quite! Any process attempting to serialize an object will throw a NotSerializableException if the class does not implement the Serializable interface properly.
How to Make a Class Serializable
- The class must be marked Serializable.
- Every instance member of the class must be
- serializable,
- marked transient,
- or have a null value at the time of serialization.
Be careful with the second rule. For a class to be serializable, we must apply the second rule recursively. Do you see why the following Cat class is not serializable?
~~~
public class Cat implements Serializable {
private Tail tail = new Tail();
}
public class Tail implements Serializable {
private Fur fur = new Fur();
}
public class Fur {}
~~~
Cat contains an instance of Tail, and both of those classes are marked Serializable, so no problems there. Unfortunately, Tail contains an instance of Fur that is not marked Serializable.
Either of the following changes fixes the problem and allows Cat to be serialized:
~~~
public class Tail implements Serializable {
private transient Fur fur = new Fur();
}
public class Fur implements Serializable {}
~~~
We could also make our tail or fur instance members null, although this would make Cat serializable only for particular instances, rather than all instances.
Serializing Records
Do you think this record is serializable?
record Record(String name) {}
It is not serializable because it does not implement Serializable. A record follows the same rules as other types of classes with respect to whether it can be serialized. Therefore, this one can be:
` record Record(String name) implements Serializable {}`
Storing Data with ObjectOutputStream and ObjectInputStream
The ObjectInputStream class is used to deserialize an object, while the ObjectOutputStream is used to serialize an object. They are high-level streams that operate on existing I/O streams. While both of these classes contain a number of methods for built-in data types like primitives, the two methods you need to know for the exam are the ones related to working with objects.
// ObjectInputStream
public Object readObject() throws IOException, ClassNotFoundException
// ObjectOutputStream
public void writeObject(Object obj) throws IOException
Note the parameters, return types, and exceptions thrown. We now provide a sample method that serializes a List of Gorilla objects to a file:
void saveToFile(List<Gorilla> gorillas, File dataFile)
throws IOException {
try (var out = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(dataFile)))) {
for (Gorilla gorilla : gorillas)
out.writeObject(gorilla);
}
}</Gorilla>
Pretty easy, right? Notice that we start with a file stream, wrap it in a buffered I/O stream to improve performance, and then wrap that with an object stream. Serializing the data is as simple as passing it to writeObject().
Once the data is stored in a file, we can deserialize it by using the following method:
List<Gorilla> readFromFile(File dataFile) throws IOException,
ClassNotFoundException {
var gorillas = new ArrayList<Gorilla>();
try (var in = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(dataFile)))) {
while (true) {
var object = in.readObject();
if (object instanceof Gorilla g)
gorillas.add(g);
}
} catch (EOFException e) {
// File end reached
}
return gorillas;
}</Gorilla></Gorilla>
Ah, not as simple as our save method, was it? When calling readObject(), null and -1 do not have any special meaning, as someone might have serialized objects with those values. Unlike our earlier techniques for reading methods from an input stream, we need to use an infinite loop to process the data, which throws an EOFException when the end of the I/O stream is reached.
Understanding the Deserialization Creation Process
ans
Interacting with Users
ans
Printing Data to the User
ans
Global Icon Real World Scenario
ans
Using Logging APIs
ans
Reading Input as an I/O Stream
ans
Closing System Streams
ans
Acquiring Input with Console
ans
Obtaining Underlying I/O Streams
ans
Formatting Console Data
ans
Using Console with a Locale
ans
Reading Console Data
ans
Reviewing Console Methods
ans
Working with Advanced APIs
ans
Manipulating Input Streams
ans
Marking Data
ans
Skipping Data
ans
Reviewing Manipulation APIs
ans
Discovering File Attributes
ans
Checking for Symbolic Links
ans
Checking File Accessibility
ans
Improving Attribute Access
ans
Understanding Attribute and View Types
ans
Retrieving Attributes
ans
Modifying Attributes
ans
Traversing a Directory Tree
ans
Don’t Use DirectoryStream and FileVisitor
ans
Selecting a Search Strategy
ans
Walking a Directory
ans
Applying a Depth Limit
ans
Avoiding Circular Paths
ans
Searching a Directory
ans
Review of Key APIs
ans
Summary
ans
Exam Essentials
ans
Review Questions
ans